原文地址:https://www.latex-project.org/publications/2014-FMi-TUB-tb111mitt-float-placement.pdf
整理翻译如下:
2012 年,在 TeX.stackexchange 上问了一个“如何影响 LaTeX 中浮动环境(如图表)的位置”的问题。由于此前有许多关于此主题的问题,作者决定深入探讨该主题,并解释大多数人在成功使用它时所面临的许多神秘之处。
一旦作者的答案出现在网上,人们要求将其转化为一篇文章,而作者傻傻地回答说:“只有这个答案成为‘伟大的’答案(获得 100 票)才行。”撰写本文时,该答案已获得 222 票,因此作者最好兑现承诺。
要回答这个问题,首先必须理解 LaTeX 的标准浮动体位置的基本规则。一旦理解了这些规则,就可以进行调整,例如通过修改浮动体参数或添加某些修改或扩展基本功能的包。
每个 LaTeX 浮动体都属于一个类别。默认情况下,LaTeX 知道两个类别,即 figure 和 table。文档类或包可以添加更多的类别。一个浮动体所属的类别会影响它的某些定位方面,例如它的默认定位规范(如果没有在浮动体本身上被覆盖的话)。
浮动环境的放置算法中的一个重要属性是,LaTeX 永远不会违反同一类浮动体的排列顺序。例如,在一个文档中有 figure 1、table 1 和 figure 2,那么 figure 1 总是会先于 figure 2 排列。然而,属于不同浮动体类别的 table 1 将独立放置,因此可以出现在图像之前、之后或之间。
LaTeX 知道两个浮动区域在一列中,即该列的顶部区域和底部区域。在两列布局中,它还知道跨越两列的顶部区域。在两列模式下,不存在用于页面宽度的底部区域。
此外,LaTeX 可以创建浮动列和浮动页,即只包含浮动的列或页面。最后,LaTeX 可以将浮动嵌入到文本中(但仅在单个浮动上指定如此)。
为了将一个浮动体定位到这些区域之一,可以将一个浮动体位置说明符作为浮动体的可选参数提供。如果没有提供这样的可选参数,则使用默认的位置说明符(这取决于浮动体的类别,如上所述,但通常允许浮动体在所有区域中定位,如果不受其他限制)。
一个浮动体放置说明符可以包含以下任意顺序的字符:
!
这表示通常适用的某些限制应该被忽略(稍后将进行讨论)h
指示浮动体允许在当前位置插入(“here”),而不考虑其他因素。t
指定浮动对象可以放在顶部区域b
该字符表示浮动体可以放置在底部区域。p
表示浮动体可以放置在浮动页或浮动列区域上。这些字符在可选参数中的排列顺序不会影响算法尝试放置浮动体的顺序!详细的顺序将在第 3.2 节中讨论。这是一个常见的误解,例如人们认为 bt 意味着应该首先尝试底部区域。
然而,如果一个字母不出现在可选参数中,则对应的区域将不会被尝试。
大约有 20 个参数影响浮动体的放置。基本上它们定义了:
将插入多少空间
一个指向浮动体的文档中的点(例如“见图 X”)被称为“引用点”,浮动体应该放在(主要)引用点附近,因为它在文档中的位置影响了浮动体在输出中的位置,这是因为它决定了 LaTeX 何时首次看到浮动体。重要的是要理解,如果将浮动体放在段落中间,算法的参考点是下一个断行或分页符,即源代码中实际放置位置后面的段落中。
由于技术和实际原因,通常最好将所有浮动体放置在段落之间(即在具有引用的段落之后),即使这使得引用点和参考点略有不同。
有了这些知识,我们现在可以深入探讨算法的行为了。
首先,我们必须理解 LaTeX 的所有排版算法都被设计成避免任何形式的回溯。这意味着 LaTeX 会读取文档源代码,格式化它发现的内容,然后(大致上)立即排版。这种设计选择的原因是为了限制复杂性(尽管仍然相当高)以及保持合理的速度(请记住,这是来自 80 年代初期)。
对于浮动体来说,这意味着该算法是贪心的,即它遇到浮动体时将立即尝试将其放置,如果成功,它将永远不会改变其决策。这意味着它可能选择一个在后来的数据中可能被视为劣质的解决方案。
例如,如果一个图形被允许放置在顶部或底部区域,LaTeX 可能会决定将此图形放置在顶部区域。如果这个图形后面跟着两个只允许放置在顶部的表格,这两个表格可能就放不下了。在这种情况下可能可行的一种解决方案(但没有尝试)是将图形放置在底部区域,将两个表格放置在顶部区域。
以下是该算法运行的基本步骤:
\clearpage
命令,LaTeX 就会开始新的一页,并放松所有限制性浮动体的条件,将所有在等待队列中的浮动体输出到浮动体页面上。在双栏模式下,相同的算法被使用,只不过它是在列的级别上进行操作,例如当一列完成后,LaTeX 将查看保留队列并生成浮动列等。
每当 LaTeX 在源代码中遇到一个浮动体环境时,它会首先检查保持队列,以查看是否已经存在相同类别的浮动体在队列中。如果是这种情况,不允许放置并且该浮动体立即进入保持队列。
如果没有,LaTeX 就会查看此浮动体的放置说明符,即可选参数中的显式说明符或来自浮动体类的默认说明符。默认的每个浮动体类在文档类文件(例如 article.cls
)中设置,通常解析为 tbp
,但不能保证。
!
,则算法将忽略与浮动放置区域中可容纳的浮动数量或区域最大尺寸相关的任何限制。否则,参数定义的限制适用。h
。t
,如果已指定,则算法将尝试将浮动放置在顶部区域。如果没有其他阻止此操作的限制,则将浮动放置在那里,并停止浮动处理。b
是否存在,如果存在,则会尝试将浮动放置在底部区域中(遵守 !
未给出时适用的任何限制)。p
说明符(如果存在)不会在上述过程中使用。仅在下一页或下一列的边界处清空暂存队列时才会查看它。这就结束了在文档中遇到浮动体时的处理过程。
在列或页处理完之后,LaTeX 会查看等待队列并尽可能地清空。为此,它会首先尝试生成浮动页。
任何参与浮动页(或列)的浮动体都必须在其浮动体定位说明符中具有 p
。否则,浮动体将无法放置在浮动页上,并且还将防止同类的任何延迟浮动体被放置在浮动页上!
如果浮动可以放在浮动页上,它将被标记为包含在浮动页上,但是处理器可能仍会中止尝试,如果浮动页没有足够填满“足够”的内容(取决于浮动页的参数设置)。仅在文档的最后,或者当发出\clearpage
命令时,这些限制才会被解除,即使浮动没有 p 并且是该页上唯一的浮动,也会被放置在浮动页上。
创建浮动页的过程会一直进行,直到算法没有更多的浮动体需要放置或由于参数设置而无法生成浮动页。在后一种情况下,到目前为止尚未放置的所有浮动体都会被考虑包含在下一页(或下一栏)的顶部和底部区域中。
这里的处理过程与上述描述的相同,但是:
• h
说明符不再有任何意义(因为我们现在已经远离了原来的“此处”),因此被忽略。
• 此时的浮动对象不是来自源文档,而是从等待队列中一个接一个地获取。
任何未能被放置的浮动体都会被放回到等待队列中,这样当 LaTeX 准备查看文档中的进一步文本输入时,等待队列可能已经包含浮动体。这样做的一个结果是,遇到文档中的浮动体时,它可能立即被推迟,因为同一种浮动体的较早的浮动体已经被保留。
有四个计数器控制可以放置多少个浮动体到不同的区域中:
topnumber
可以位于⽂本页顶部的浮动体的最⼤数⽬(缺省值为 2)。
bottomnumber
可以位于⽂本页底部的浮动体的最⼤数⽬(缺省值为 1)。
totalnumber
可以位于⽂本页中的浮动体的最⼤数⽬(缺省值为 3)。
dbltopnumber
是双列模式下置于正文列上方的最大全宽浮动体数量(默认为 2)。
区域的大小受到参数的控制(可通过 \renewcommand
进行更改),这些参数定义了区域的最大(或最小)大小,表示为页面高度的分数:
\topfraction
(默认值 0.7)表示顶部区域的最大尺寸;
\bottomfraction
(默认值 0.3)表示底部区域的最大尺寸;
\dbltopfraction
(默认值 0.7)表示双列浮动对象顶部区域的最大尺寸;
\textfraction
(默认值 0.2)表示正文区域的最小尺寸,即不能被浮动对象占据的区域。
在一个区域内分隔浮动体以及浮动体区域与文本区域之间的间距是通过以下参数来定义的(所有这些参数都是可伸缩或可收缩的橡胶长度)。它们的默认值取决于文档字体大小,并且当使用类选项例如 11pt 或 12pt 时会更改。我们只展示 10pt 的默认值:
\floatsep
(默认 12pt plus 2pt minus 2pt)浮动体与文本之间的垂直间距。\textfloatsep
(默认 20pt plus 2pt minus 4pt)在文本之上或之下的浮动体之间的垂直间距。\intextsep
(默认 12pt plus 2pt minus 2pt)在文本和顶部或底部浮动体之间的垂直间距。\dblfloatsep
(默认 12pt plus 2pt minus 2pt)在双栏模式下,两个浮动体之间的垂直间距。\dbltextfloatsep
(默认 20pt plus 2pt minus 4pt)在双栏模式下,在文本之上或之下的两个浮动体之间的垂直间距。对于嵌入式浮动对象(已放置在“此处”),其与周围文本的间隔由\intextsep
参数控制,默认值为 12pt 加 2pt 减 2pt。
在浮动页或浮动栏的情况下(即仅包含浮动体的页面或页面的栏),参数如topfraction 等不适用。相反,它们的创建是通过floatpagefraction(默认值为 0.5)进行控制,即需要被浮动体占据的页面(或栏)的最小部分才能允许形成一个浮动页(或栏)。
float 环境在源代码中的位置决定了它在最终文档中出现的最早时间点。它可能在一定程度上向后移动,因为它可能被放置在当前页面的顶部区域中;请参见第 6.3 节以了解如何更改此设置。但是,由于 LaTeX 不会回溯,因此它不能出现在比周围文本更早的页面上,因为早期的页面已经排版完成。
因此,通常情况下,float 会在其第一次引用附近的源代码中放置(即类似于“请参见图 5”之类的文本),因为这将确保 float 出现在同一页或后一页。但是,在某些情况下,您可能希望将 float 放置在前一页(如果从引用仍然可以看到该页面)。这只能通过将 float 移动到源代码中的早期位置来实现。
当 LaTeX 在双栏模式下遇到一个全页宽的浮动环境(在环境名称末尾带有 *,例如 figure *),它会立即将该浮动环境移到延迟队列中。这种行为的原因再次在于其算法的“贪婪”行为:如果 LaTeX 当前正在组装该页面的第二列,那么第一列已经被组装并存储了起来;请记住,由于 LaTeX 不会回溯,因此没有办法将浮动对象适配到当前页面上。为了保持算法的简单性,即使在工作于第一列的情况下(理论上即使没有回溯也可以做得更好),它也会执行相同的操作。
因此,为了将这样的浮动体放置到当前页面上,必须手动将其移动到源代码中较早的位置——在当前页面开始之前。如果这样做,显然任何进一步的文档更改都可能使这个调整过时;因此,这样的调整最好在文档制作的最后阶段(如果有的话)进行——当所有材料都已编写完成并且关注于微调视觉外观时。
还要注意,基本算法在这个区域有一个错误 2:它维护两个独立的等待队列:一个是单列队列,一个是双列队列。因此,浮动体的顺序未必得到保留,浮动体可能会按照不正确的顺序进行排版。如果发生这种情况,则必须手动将双列浮动体移动到文档中较早(或较晚)的位置,或加载 fixltx2e 宏包,该宏包实现了这个问题的修正。
这不太是算法的后果,而是其实现方式的事实。对于双列浮动,唯一可能的位置是顶部区域或浮动页。因此,如果有人为这样的浮动添加了 h 或 b 浮动位置说明符,则会被忽略。作为一个特殊而重要的例子,{figure*}[b]意味着这个浮动将不会被排版,直到遇到 \clearpage
或到达文档的末尾。
这可能很显而易见,但值得重申:任何浮动参数都会限制 LaTeX 放置浮动的能力。限制程度取决于设置:总有一种设置方式可以不影响放置,但这样做会导致放置位置看起来相当糟糕。
默认情况下,LaTeX 的设置相当宽松。例如,为了接受浮动页,浮动对象必须占用可用页面的至少一半。换句话说,这意味着这样的页面允许一半是空的(在大多数情况下显然不是最佳放置方式)。
经常发生的情况是,用户试图改善这些设置,然后当所有浮动对象突然堆积在文档末尾时感到惊讶。以这个例子为例:如果将参数 \floatpagefraction
改为要求浮动页面占页面的 0.8,那么占用大约 0.75 页面的浮动对象将无法自己形成浮动页。因此,如果没有其他可以添加的并且实际上适合剩余空间的浮动对象,则该浮动对象将被推迟,与此类别的所有其他浮动对象一起。更糟糕的是,这个特定的浮动对象过大,无法放入下一个顶部区域,因为默认最大允许区域为 0.7(来自 \topfraction
)。结果,您的所有浮动对象都要推迟到下一个 \clearpage
。
因此,在撰写文档时最好不要干扰这些参数,或者至少不要以一种使算法更难将浮动体放置在其引用附近的方式来这样做。对于校对,比起避免半空的页面,有一个与引用位置相邻的图表更为重要。有关微调已经完成的文档的可能性将在下面讨论。
在这里得出的另一个结论是,一些浮动体参数之间存在依赖关系;在更改它们的值时考虑这些依赖关系非常重要。
很多人可能会感到惊讶,但是由于算法的设计方式,h 位置参数并不是无条件的命令。如果需要一个无条件的命令,可以使用扩展包(例如 float 包)提供的 H 参数,它真正意味着“这里”(如果需要,会首先启动一个新页面)。
正如上面提到的,算法尝试按照一种编程难以改变的顺序将浮动体放置到可用的浮动区域中:“此处”(“here”)、“页顶”(“top”)、“页底”(“bottom”)以及在页边界上,首先是“浮动页”(“page”),仅在不再可能时,才会是下一页的“页顶”和“页底”。
因此,指定[bt]
并不意味着先尝试底部,然后才是顶部。它只是意味着允许该浮动体进入顶部或底部区域(但不进入浮动页),就像[tb]
一样。
这并不是算法的结果,而是其实现的一部分:每当 LaTeX 试图决定浮动体(或\marginpar !
)的放置位置时,它必须触发输出例程来完成此操作。在此过程中,所有页面上的脚注都将从它们在排版区中的当前位置中移除,并作为 TeX 准备页面生成的一部分被收集到\footins
盒子中。
但是,在放置浮动体(或推迟浮动体)之后,LaTeX 将页面材料返回到排版区,由于 TeX 输出例程的行为,排版区现在已经发生了变化:所有脚注都已从其原始位置中取出。因此,LaTeX 必须将脚注放回去,但是它只能将它们放在一个地方(不再知道它们的起源)。它的做法是在排版区的末尾重新插入脚注(确切地说是脚注文本)。有一些很好的理由要这样做,其中之一是,LaTeX 希望所有返回的材料仍适合当前页面。
然而,如果由于某种原因在较早的位置最终进行分页,那么脚注将出现在错误的页面或列上。这是一个相当不太可能发生的情况,LaTeX 会努力使其几乎不可能发生,但如果发生了,请检查所选的分页点附近是否有浮动体,并移动浮动体或使用显式分页指导算法。在 TeX.stackexchange 的另一个问题中可以找到此行为的示例[4]。实际上,值得强调讨论的特定情况:不要在标题后直接放置浮动体,除非它是始终在新页面上开始的标题。原因是标题通常形成非常大的对象(因为标题会阻止直接在其后面进行分页)。但是,在此中间放置一个浮动体意味着在 LaTeX 做出决定之前将触发输出例程,脚注将移动到错误的位置。
根据请求,以下是有关现有文档的一些信息。该算法及其实现在文件 ltoutput.dtx 中作为 LaTeX 核心源的一部分进行了记录。它可以独立或作为整个内核的一部分进行排版(即通过排版 source2e.tex - 如果仍然存在校验和错误,请忽略,非常抱歉)。
这份文档是一个有趣的历史文物。其中的部分展示了半格式化的伪代码,可以追溯到 LaTeX 2.09,换句话说,它来自 Leslie Lamport 的原始文档。实际代码使用 doc 样式进行文档化,在某些部分上或多或少地得到了适当的文档化(从头开始),可以追溯到 1994 年左右,当时 Chris Rowley 和我对 LaTeX2ε(当前版本)的原始算法进行了调整和扩展。
它还相当公开地记录了算法和/或其实现的各种问题 - 在许多情况下,由于许多依赖项以及当然,由于对当前行为的依赖,我们不敢对其进行更改,以避免影响过多现有文档的危险。4 最后,您将找到有关该算法的注释的列表,但是代码文档中也散布着注释、问题和任务(?:-)。
该文件的一个有趣方面(我全然忘记了)是它包含了追踪算法行为的所有代码。它输出非常原始和详细,并且出于这个原因,我当时没有公开提供它。但即使在当前形式下,它也可以为算法的行为以及某些决策的制定提供有趣的见解。
因此,在撰写本文时,我有所顾虑,现在最新的 LaTeX 发行版(2014 年 5 月)提供了 fltrace 包,您可以加载它以跟踪一些奇怪的浮动体放置决策,或者仅仅为了更好地理解算法。它提供了命令 \tracefloats
和 \tracefloatsoff
来启动或停止跟踪算法,以及 \tracefloatvals
命令来显示本文中讨论的各种浮动体参数的当前值。
由于该包与内核代码相同,只是添加了跟踪功能,如果您加载任何其他操纵内核代码的包,它可能有效也可能无效。在这种情况下,最好首先加载 fltrace。
在最后一节中,我们讨论一些策略,以绕过或解决常见的问题。这并不是全面的,您可能会在其他出版物中找到进一步的信息,例如《LaTeX Companion》[2],该书专门介绍了浮动体的章节。
有时候需要确保浮动体出现在文档正文的特定位置,即使这会导致一些部分为空的页面。如上所述,h 选项并不提供此功能,但是有些扩展提供了此功能,例如 float 宏包为此提供了 H 选项。
另一个选择是使用 caption 宏包的\captionof
命令,它可以生成一个常规的浮动体标题(包括在图表清单中的条目等),但无需使用周围的浮动体环境。
如上所述,标准算法不支持双列浮动对象出现在页面底部。如果您加载stfloat
s 包,则会添加此缺失的功能,但不包括第一页。
默认情况下,LaTeX 浮动算法允许浮动体在其引用之前出现,只要浮动体和引用位于同一页上;更准确地说,它允许浮动体出现在遇到浮动体的列的顶部区域。
这种做法提高了浮动体从调用位置可见的机会,避免了它被放在后面的页面。然而,对于一些期刊来说,这种做法太宽松了,他们要求浮动体严格在调用之后放置,即在调用列中,只有底部区域是一个有效的放置选项。为了满足这一要求,flafter 包实现了此策略。
如果您的文档中只有很少的浮动体,那么这种策略可能效果良好。但对于有大量浮动体的文档来说,浮动体的放置就变得更加困难,您可能会发现所有的浮动体都出现在文档或章节的结尾,或者您可能会收到“太多未处理的浮动体”错误。
有时候防止浮动体出现在某一页上是有帮助的,例如,防止新部分中的浮动体移动到当前页的顶部区域,而不禁止稍后在后面的页顶部区域内放置。为了进行这种微调,LaTeX 提供了命令 \suppressfloats[placement]
。可选参数可以是 t
或 b
,防止在当前页的相应区域内进一步放置。如果没有参数,则推迟当前页面上所有剩余的浮动体。
标准的 LaTeX 已经实现了名为 \clearpage
的浮动体屏障。两侧的浮动体不会互相出现。它的工作方式是输出所有延迟的浮动体,如果需要则生成浮动体页,然后开始一个新页。虽然这适用于将浮动体保留在一个章节中(因为章节通常在新页上开始),但有些情况下希望有一个不那么显眼的屏障,即一个可以在不强制新页面或部分渗透的屏障。
placeins 包提供了这个功能,它实现了一个名为 \FloatBarrier
的命令,它不会引入分页符。通过包选项,可以改变行为,允许浮动体从一侧迁移到另一侧,只要它们仍然出现在同一页上。
如果某个浮动体过大而无法适应某个特定区域,或者该区域已经包含了最大数量的浮动体,但你仍想将当前浮动体强制放置在此处,则在浮动体的可选参数中添加 ! 是一个不错的选择。它会忽略所有针对该特定浮动体实施的限制参数,使其总是被放置,除非已经有相同浮动类别的延迟浮动体,或者在添加该浮动体时,允许的区域比可用空间还要大。
由于尝试的顺序仍然相同(先顶部,然后底部),因此您可能需要使用 [!b] 将浮动对象强制放置在底部区域,因为 [!tb] 通常已经成功将其放置在顶部区域中。缺点当然是,如果浮动对象不适合,则仅会在以下页面的底部区域中出现。因此,任何后来的文本更改都可能会对您的放置决策造成麻烦。
有许多方法可以微调浮动体位置算法的行为,其中大多数在本文中已经讨论过。然而,还有一种“调整”可能性,实际上是最大的可能性:更改文档文本。因此,最后的建议是:在完全编写文本并且文档接近完成之前,不要开始操纵参数或更改位置说明符或在文档中移动浮动体。这是一种浪费精力的行为,而且在文本更改后,最初提供的限制可能不再适用,从而导致较差的浮动体位置。