如何改善顶部浮动后的间距?

如何改善顶部浮动后的间距?

语境

我正在尝试创建一个宏(此处称为\myCommand),用于分隔两个段落并根据上下文添加不同的垂直空间。但我想这是一个普遍的问题,也可能发生在诸如这样的宏中\section{...}

通常情况下,我想在开始和结束处插入一个值为 的空格\myLength(为了便于说明,我们将取= 30 pt)。 如果宏位于页面顶部,在分页符之后,则将被忽略,并且不会添加垂直空间,这正是我想要的。 但是,当宏位于分页符之后且前面有一个或多个顶部浮动时,就会出现问题,就像上一种情况一样。在这种情况下,我希望宏插入垂直线以保持一致性。\myLength\myCommand
\vspace{\myLength}
\vspace*{\dimexpr\myLength-\textfloatsep}


例子

举个例子来说明一下:

下面,宏的行为正确:当它位于分页符后的页面顶部时,不会在宏的开头插入空格;而当它不在页面顶部时,\vspace{\myLength}会正确地插入到宏的开头。

在此处输入图片描述

但是,当宏在分页符之后落在页面顶部并且前面有一个顶部浮动时,空间会完全不平衡。

在此处输入图片描述

为了说明该问题具有普遍性,而非特定于\myCommand,以下是与 相同的问题\section{...}

在此处输入图片描述

这是一个不起作用的最小示例:

\documentclass{article}
\usepackage{lipsum, mwe}

\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate

\newcommand{\myCommand}{\par
    \vspace{\myLength}
    {\centering\LARGE * * *\par}
    \vspace{\myLength}
}

\begin{document}
\lipsum[1]

\lipsum[1]

\lipsum[1]

\lipsum[2]

\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}

\myCommand

\lipsum[1]

\lipsum[2]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1]
\end{document}

目标

为了克服上面提到的问题,我希望仅在以下情况下使用值为的垂直空间\vspace*{\dimexpr\myLength-\textfloatsep}(以补偿顶部浮动插入的垂直空间): 在页面的开始处(分页符之后)并且存在顶部浮动。\textfloatsep

在所有其他情况下,使用都可以\vspace{\myLength}完美地工作。


试验

我的(迄今为止未成功的)方法是使用两个条件在正确的位置插入正确的垂直空格:

  • 第一个条件是测试当前页面上是否存在顶部浮动,
  • 第二个条件是检查我们是否位于分页符之后的新页面的顶部。

如果两个条件都满足,那么我们确实位于页面的开头(分页符之后)并且存在顶部浮动;然后我们插入\vspace*{\dimexpr\myLength-\textfloatsep}。在所有其他情况下,我们插入\vspace{\myLength}

感谢之前的一个问题,我已经能够找到一种检测页面中是否存在页面顶部浮动的方法:如何从文档主体判断页面是否有顶部浮动?

然而,第二个条件对我来说确实带来了一个问题,我想尝试使用\pagetotal,但它似乎不合适:如何访问真的\pagetotal 的值是多少?

我的宏\myCommand如下所示:

\makeatletter
\usepackage{refcount}
%From : https://tex.stackexchange.com/questions/712713/how-to-determine-from-the-document-body-whether-or-not-a-page-has-a-top-float
\def \@combinefloats {%
    \ifx \@toplist\@empty%
    \else%
        \@cflt%
        \immediate\write\@auxout{\string\global\string\@namedef{pageWithTopFloat-\thepage}{1}}%
    \fi%
    \ifx \@botlist\@empty \else \@cflb \fi%
}

\newcounter{ifTopFloatCnt}

\long\def\ifTopFloat#1#2{%
    \global\advance\c@ifTopFloatCnt\@ne%
    \label{\the\c@ifTopFloatCnt @ifTopFloat}\nopagebreak%
    \ifcsname pageWithTopFloat-\getpagerefnumber{\the\c@ifTopFloatCnt @ifTopFloat}\endcsname%
        #1%
    \else%
        #2%
    \fi%
}

\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate

\newcommand{\mycommand}{\par%
    \ifTopFloat{%
        \ifdim\pagetotal=0pt% If there is a top float AND you are at the start of a new page
            \vspace*{\dimexpr\myLength-\textfloatsep}
            {\centering\LARGE * * *\par}
        \else% If there is a top float BUT you are not at the start of a new page
            \vspace{\myLength}
            {\centering\LARGE * * *\par}
        \fi
    }{% If there is no top float
        \vspace{\myLength}
        {\centering\LARGE * * *\par}
    }    
    \vspace{\myLength}
}
\makeatother

我的方法肯定不正确。

答案1

这是一个基于 LuaTeX 的解决方案。如果命令中的所有内容\removefromtop位于页面顶部,则将被删除,否则将保留。我在这里使用规则以使效果更明显,但您可以将其替换为\vspace(或任何其他垂直模式材料)作为最终文档。

\documentclass{article}

\usepackage{luacode}
\begin{luacode*}
    local attr = luatexbase.new_attribute("removefromtop")
    token.set_char("removefromtopattr", attr, "global")

    local cancel_output = false
    local last_cancelled = false

    luatexbase.add_to_callback("pre_output_filter", function (head)
        local outputpenalty = tex.outputpenalty
        if outputpenalty <= -10002 and outputpenalty >= -10005 then
            return true
        end

        if token.get_macro("@toplist") ~= "" then
            return true
        end

        for n in node.traverse(head) do
            if node.get_attribute(n, attr) == 1 then
                head = node.remove(head, n)
                cancel_output = true
            elseif n.id ~= node.id("penalty") and
                   n.id ~= node.id("glue")    and
                   n.id ~= node.id("kern")    and
                   n.id ~= node.id("mark")    and
                   n.id ~= node.id("whatsit") and
                   n.id ~= node.id("ins")
            then
                break
            end
        end

        if last_cancelled then
            last_cancelled = false
            for n in node.traverse_id(node.id("glue"), head) do
                if n.subtype == 10 then
                    n.width = -tex.baselineskip.width
                end
            end
        end

        if cancel_output then
            return head
        else
            return true
        end
    end, "remove_from_top")

    luatexbase.add_to_callback("buildpage_filter", function (info)
        if status.output_active then
            return
        end

        if not tex.output:match("%}%}") then
            tex.runtoks(function()
                tex.sprint[[\output\expandafter{\the\output}]]
            end)
        end

        tex.triggerbuildpage()

        if not (status.output_active and cancel_output) then
            return
        end

        cancel_output = false
        last_cancelled = true

        local level = 0
        repeat
            local t = token.get_next()
            if t.command == 1 then
                level = level + 1
            elseif t.command == 2 then
                level = level - 1
            end
        until level == 0

        local box = node.copy_list(tex.box[255])
        node.write(box)
        tex.setbox("global", 255, nil)
    end, "remove_from_top")
\end{luacode*}

\newcommand{\removefromtop}[1]{%
    \vbox attr \removefromtopattr=1 {%
        #1%
    }%
}

\usepackage{lipsum, mwe}

\newlength{\myLength}
\setlength{\myLength}{20pt}% exaggerated value to illustrate

\newcommand{\myCommand}{
    \removefromtop{
        \hrule height \myLength width \textwidth depth 0pt
    }
    \nobreak
    \vbox{
        {\centering\LARGE * * *\par}
        \hrule height \myLength width \textwidth depth 0pt
    }
}

\begin{document}
\lipsum[1]

\lipsum[1]

\lipsum[1]

\lipsum[2]

\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}

\myCommand

\lipsum[1]

\lipsum[2]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1]

\lipsum[2]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1-3]

\myCommand

\lipsum[1-9]

\myCommand

\lipsum[1]

\begin{figure}[t]
    \centering
    \includegraphics{example-image}
\end{figure}

\end{document}

输出

代码相当复杂,我还没有对其进行过多测试,因此我预计更复杂的文档可能会出现一些错误。

怎么运行的

首先,我们定义一个自定义“属性”,用它来标记内容\removefromtop

接下来,我们进入buildpage_filter,它在 TeX 将内容添加到“主垂直列表”之前被调用。这里的棘手之处在于我们调用tex.triggerbuildpage() 里面这个回调会触发 TeX 刷新其“最近的贡献”并尝试分页。

如果触发了分页符,则控件将跳转到pre_output_filter。在这里,我们添加了另一个回调,它首先检查这是否是“真正的”分页符(而不是 LaTeX 用来刷新浮动的假分页符)以及当前页面上没有顶部浮动。如果是这种情况,我们将遍历页面的内容并删除任何用我们的自定义属性标记的初始内容。如果我们标记了任何内容,那么我们会将此信号返回给我们buildpage_filter并返回新的页面内容。

一旦pre_output_filter完成,控制权将在之后恢复tex.triggerbuildpage(),但现在,我们在输出例程内。如果最近的pre_output_filter成功删除了一些内容,并且我们在第一个输出例程内,那么我们将删除 TeX 输入堆栈上的所有标记。由于我们在输出例程内,这会清空标记\output列表的当前扩展。然后我们清除\box255(当前页面的内容),将其内容移回最近的贡献列表,然后返回。

为什么要这么做?

在触发输出例程之前,我们无法知道框是否\removefromtop位于页面顶部。但是,当我们移除框时\removefromtop,我们会缩短页面,因此如果我们只是挂起pre_output_filter移除框,那么所有页面都会太短。

相反,我们让输出例程正常触发,然后如果我们删除任何框\removefromtop,我们会将当前页面的内容推回到最近的贡献列表中,取消输出例程,并让 TeX 正常进行以将更多内容添加到页面底部。

答案2

我认为问题出在浮动后添加的垂直空间。因此,在下面的建议中,我修改了使用\myLength并添加了 a \baselineskip

\documentclass{article}
\usepackage{lipsum, mwe}

\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate

% Below, I increase the separation between float and text and reduce
% the flexibility of the space
% You can see the default values by adding \the\textfloatsep inside
% the document environment
\newlength{\myFloatskip}
\setlength{\myFloatskip}{\the\myLength}
\addtolength{\myFloatskip}{\baselineskip}
\setlength{\textfloatsep}{\the\myFloatskip plus 0.0pt minus 0.0pt}

\newcommand{\myCommand}{\par%
    \vspace{\myLength}%
    {\centering\LARGE * * *\par}%
    \vspace{\myLength}%
}

\begin{document}

\lipsum[1]

\lipsum[1]

\lipsum[1]

\lipsum[2]

\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}

\myCommand

\lipsum[2]

\lipsum[3]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1]
\end{document}

答案3

经过几天的研究,我想我已经找到了解决问题的方法。思路如下:

在输出例程中,该\@cflt命令在适当的情况下将顶部浮动与正文组合在一起。我修改了此命令,以便在辅助文件中记录表示文本正文开始位置(即,紧接着任何页面顶部浮动)的 Y 坐标。当我运行 时\myCommand,我执行相同的操作,并将表示宏开始位置的 Y 坐标保存在辅助文件中。

在下一次编译时,我比较两个 Y 坐标。如果它们相等,则\myCommand就在顶部浮点之后,我可以\vspace*{\dimexpr\myLength-\textfloatsep}按照问题中的要求插入。

结果如下:

在此处输入图片描述

这比原来的结果更加和谐(尽管可以进行微小的调整):

在此处输入图片描述


然而,在测试过程中,我发现应用程序本身存在一个问题。实际上,宏插入的空间取决于宏在页面中的位置,而宏在页面中的位置又取决于宏插入的空间等。存在循环依赖。为了避免这种情况,我想您必须\myCommand根据您想要如何使用它来阻止分页符在之前或之后出现。我愿意接受改进建议。

例如,在我的 MWE 中,如果对于我的第 4 段,我使用\lipsum[2]而不是\lipsum[3],则会出现不稳定性,并且文档的汇编依次给出下面的情况 A(左)和 B(右):

在此处输入图片描述在此处输入图片描述

如果我们从案例 A 开始,则宏将出现在第 2 页正文的顶部。
因此,在下一次编译时,我们将\myLength-\textfloatsep在 的开头插入一个等于 的空格\myCommand。现在,垂直空间\myLength-\textfloatsep小于垂直空间\myLength,因此有足够的空间让命令位于第 1 页,我们处于案例 B 中。
在下一次编译时,由于我们不再位于顶部浮动之后,因此\myCommand插入一个等于 的垂直空间\myLength。这一次,第 1 页上没有足够的空间,因此\myCommand被发送回第 2 页的开头,因此我们回到案例 A。


妇女权利委员会:

\documentclass{article}
\usepackage{lipsum, mwe, refcount, iftex}

\ifluatex% For compatibility with LuaTeX, which I generally use.
    \let\pdfsavepos\savepos
    \let\pdflastypos\lastypos
\fi

\makeatletter
\def \@cflt{%
    \let \@elt \@comflelt
    \setbox\@tempboxa \vbox{}%
    \@toplist
    \setbox\@outputbox \vbox{%
        \boxmaxdepth \maxdepth
        \unvbox\@tempboxa
        \vskip -\floatsep
        \topfigrule
        \vskip \textfloatsep
        \pdfsavepos% <--- Added
        \write\@auxout{\string\global\string\@namedef{@pageWithTopFloatYPos-\thepage}{\the\pdflastypos}}% <--- Added. We save the Y position of the top of "\@outputbox", i. e. the body of the text.
        \unvbox\@outputbox
    }%
    \let\@elt\relax
    \xdef\@freelist{\@freelist\@toplist}%
    \global\let\@toplist\@empty
}

\newcounter{@ifTopFloatCnt}

\long\def\ifTopFloat#1#2{% A conditional to see if there is a top float on the current page. See https://tex.stackexchange.com/questions/712713/how-to-determine-from-the-document-body-whether-or-not-a-page-has-a-top-float.
    \stepcounter{@ifTopFloatCnt}
    \label{@ifTopFloat-\the@ifTopFloatCnt}\nopagebreak%
    \ifcsname @pageWithTopFloatYPos-\getpagerefnumber{@ifTopFloat-\the@ifTopFloatCnt}\endcsname%
        #1%
    \else%
        #2%
    \fi%
}

\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate

\newcounter{@myCommandCnt}
\newcounter{@myCommandCntAux}

\newcommand{\myCommand}{\par%
    \stepcounter{@myCommandCnt}%
    \pdfsavepos%
    \write\@auxout{%
        \string\stepcounter{@myCommandCntAux}%
        ^^J% New line in the auxiliary file
        \string\global\string\@namedef{@myCommandYPos-\string\the@myCommandCntAux}{\the\pdflastypos}% Save the Y coordinate.
    }%
    \ifTopFloat{%
        \ifnum \@nameuse{@myCommandYPos-\the@myCommandCnt} = 
            \csname @pageWithTopFloatYPos-\getpagerefnumber{@ifTopFloat-\the@ifTopFloatCnt}\endcsname% If there's a top float AND you're right after it
            \vspace*{\dimexpr\myLength-\textfloatsep}
            {\centering\LARGE * * * (a)\par}
        \else% If there is a top float BUT you are not right after it
            \vspace{\myLength}
            {\centering\LARGE * * * (b)\par}
        \fi
    }{% If there is no top float
        \vspace{\myLength}
        {\centering\LARGE * * * (c)\par}
    }    
    \vspace{\myLength}
}
\makeatother

\begin{document}
\lipsum[1]

\lipsum[1]

\lipsum[1]

\lipsum[3]% <--- If the current line is uncommented and the next line is commented, stability is achieved.
%\lipsum[2]% <--- If the previous line is commented and the current line is uncommented, instability occurs.

\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}

\myCommand

\lipsum[1]

\lipsum[2]

\begin{figure}[t]
\centering
\includegraphics{example-image-9x16}
\end{figure}

\myCommand

\lipsum[2]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1]
\end{document}

相关内容