我编写了一个原型 LaTeX 包和相关的查看框架,允许在允许与图形交互的环境中查看 LaTeX 文档。图形可能有可以拖动的点、接受键盘的数字输入、使用各种 GUI 小部件等。目的是让 LaTeX 像平常一样生成 PDF,但允许将 PDF 连同一些其他文件一起加载到一个特殊的查看器中,该查看器用交互式图形替换 PDF 中的所有静态图形。显然,静态和交互式这两个版本除了是交互式还是静态之外,在各个方面都是相同的。我有一个概念验证,使用网络浏览器作为查看和交互的平台。还有一个不太完善的 Java 版本,但似乎没那么有用,所以它被放弃了。
框架的 LaTeX 端如下所示,它可以工作,但还有一个问题。正如在使用 latex3 设置 tikzpicture 的高度,图形的面积不稳定。那里提供的答案适用于给定的 MWE,但不适用于完整上下文。
回顾一下,intfig
环境采用文件名(不带 .tikz 后缀)和图形必须具有的高度。环境查找给定的 .tikz 文件。如果文件存在,则加载;如果不存在,则显示一个带有“不可用”消息的框。如果文件不存在且未加载,则图形有一个高度;如果文件可用,则图形似乎比预期高一行文本,好像图形\par
后面有一个多余的内容。
我希望得到有关图形放置问题的具体帮助以及改进的一般建议,因为我远非 LaTeX3 专家。
下面的代码中有一些关于系统如何工作的补充说明。
\documentclass[10pt]{article}
% To precisely locate figures within a document.
\usepackage{zref-savepos}
\usepackage{zref-thepage}
\usepackage{zref-abspage}
\usepackage{zref-user}
\usepackage{zref-pagelayout}
% To treat environment verbatim.
\usepackage{xsim}
\ExplSyntaxOn
% The inner and outer margin widths. There should be a way to
% determine these programmatically instead of having the user provide
% their best estimate, but I don't know how.
\dim_new:N \l_intfig_innermargin_dim
\dim_new:N \l_intfig_outermargin_dim
\dim_set:Nn \l_intfig_innermargin_dim { 0 pt }
\dim_set:Nn \l_intfig_outermargin_dim { 0 pt }
\NewDocumentCommand\SetInnerMargin { m }
{
\dim_set:Nn \l_intfig_innermargin_dim { #1 }
}
\NewDocumentCommand\SetOuterMargin { m }
{
\dim_set:Nn \l_intfig_outermargin_dim { #1 }
}
% The figures file is open throughout the run.
\iow_new:N \g_figurefile_iow
\iow_open:Nn \g_figurefile_iow { figures.aux }
% Name for the figure, as given by the user.
\str_new:N \l_intfig_figname_str
% Manditory height given by the user.
\dim_new:N \l_intfig_height_dim
% Optional height given by the user (if the interactive height differs.
\dim_new:N \l_intfig_interactiveHeight_dim
% Scratch values used for calculation of figure position.
\tl_new:N \l_intfig_tempa_tl
\tl_new:N \l_intfig_tempb_tl
\tl_new:N \l_intfig_tempc_tl
% And several booleans.
\bool_new:N \l_intfig_hasintht_bool
\bool_new:N \l_intfig_external_bool
\bool_new:N \l_intfig_nostatic_bool
\bool_new:N \l_intfig_done_bool
% This is for the figures file: the position of the figure on the page.
\dim_new:N \l_intfig_pagepos_dim
% I don't really understand the whole concept of these "variants."
\cs_generate_variant:Nn \file_if_exist:nTF {V}
\cs_generate_variant:Nn \file_input:n {V}
\NewDocumentEnvironment{intfig} { > { \SplitArgument { 1 } { , } } m!o }
{
% Not sure if this is needed. Are there any local variables at all?
\group_begin:
% Parse manditory arguments.
\intfig_manargs #1
% The optional arguments.
\bool_set_false:N \l_intfig_hasintht_bool
\bool_set_false:N \l_intfig_external_bool
\bool_set_false:N \l_intfig_nostatic_bool
\bool_set_false:N \l_intfig_done_bool
\tl_if_blank:nTF { #2 } {} {
\clist_set:Nn \l_intfig_optclist_clist { #2 }
\intfig_optargs { }
}
% Prepare the data for the figures file.
% The page number, obtained from a previous run.
\str_set:Nx \l_intfig_filespec_str {
\use:c { zref@extractdefault } { \l_intfig_figname_str -pageno} { abspage } { 0 }
}
% The inner and outer margins.
\str_put_right:Nx \l_intfig_filespec_str { ~ \dim_to_decimal_in_bp:n \l_intfig_innermargin_dim }
\str_put_right:Nx \l_intfig_filespec_str { ~ \dim_to_decimal_in_bp:n \l_intfig_outermargin_dim }
% And the text width.
\str_put_right:Nx \l_intfig_filespec_str { ~ \dim_to_decimal_in_bp:n \textwidth }
% Calculation of figure location, taking into account the
% possibility that the figure has been bumped to the next page.
% Take the location of the bottom edge and subract the figure height.
\tl_set:Nn \l_intfig_tempa_tl { \zposy { \l_intfig_figname_str } }
% Convert the figure height to sp.
\tl_set:Nn \l_intfig_tempb_tl { \dim_to_decimal_in_sp:n{ \l_intfig_height_dim } }
% Add
\tl_set:Nn \l_intfig_tempc_tl { \int_eval:n { \l_intfig_tempa_tl + \l_intfig_tempb_tl } }
% Convert to a dim
\dim_set:Nn \l_intfig_pagepos_dim { \l_intfig_tempc_tl sp }
% And (whew!) write it out, in bp.
\str_put_right:Nx \l_intfig_filespec_str { ~ \dim_to_decimal_in_bp:n \l_intfig_pagepos_dim }
% The latex height of the figure (in bp)
\str_put_right:Nx \l_intfig_filespec_str { ~ \dim_to_decimal_in_bp:n \l_intfig_height_dim }
% The interactive height, which may be the latex height repeated.
\bool_if:NTF \l_intfig_hasintht_bool
{ \str_put_right:Nx \l_intfig_filespec_str { ~ \dim_to_decimal_in_bp:n \l_intfig_interactiveHeight_dim }
} % else
{
\str_put_right:Nx \l_intfig_filespec_str { ~ \dim_to_decimal_in_bp:n \l_intfig_height_dim }
}
% The figure name.
\str_put_right:Nx \l_intfig_filespec_str { ~ \l_intfig_figname_str }
% And a boolean
\bool_if:NTF \l_intfig_done_bool
{
\str_put_right:Nx \l_intfig_filespec_str { ~ true }
}
{
\str_put_right:Nx \l_intfig_filespec_str { ~ false }
}
% String is ready. Write out the line.
\iow_now:Nx \g_figurefile_iow { \l_intfig_filespec_str }
% Based on the optional arguments, write the body of the environment to a
% file, and/or read a tikz file to replace the current body (which may be
% blank). Set two booleans to indicate whether to do these two things.
\bool_set_false:N \l_intfig_writebody_bool
\bool_set_false:N \l_intfig_readtikz_bool
\bool_if:NTF \l_intfig_done_bool
{
\bool_if:NTF \l_intfig_nostatic_bool {} {
\bool_set_true:N \l_intfig_readtikz_bool
}
}
{
\bool_if:NTF \l_intfig_external_bool
{
\bool_if:NTF \l_intfig_nostatic_bool {}
{ \bool_set_true:N \l_intfig_readtikz_bool }
}
{
\bool_set_true:N \l_intfig_writebody_bool
\bool_if:NTF \l_intfig_nostatic_bool { }
{
\bool_set_true:N \l_intfig_readtikz_bool
}
}
}
% The output file name needs an extra ".fjs" on the end.
% Apparently, on Windows, you can't use a suffix for executable
% types. No .js, no .py, .pl, etc.
\str_set_eq:NN \l_intfig_jinfile_str \l_intfig_figname_str
\str_put_right:Nn \l_intfig_jinfile_str { .fjs }
% Whether to add an additional EOL to the output file depends on
% whether there were any optional arguments. This is mysterious to me.
\IfValueTF {#2}
{ \xsim_file_write_start:nn { \c_true_bool } }
{ \xsim_file_write_start:nn { \c_false_bool } }
{ \l_intfig_jinfile_str }
}{
% Post environment commands.
% Stop writing the body since it's done.
\xsim_file_write_stop:
% And replace the current body with an external tikz file.
\str_set_eq:NN \l_intfig_tikzfile_str \l_intfig_figname_str
\str_put_right:Nn \l_intfig_tikzfile_str { .tikz }
\file_if_exist:VTF \l_intfig_tikzfile_str {
% Load the tikz file here.
\file_input:V \l_intfig_tikzfile_str
}
{
% No tikz file exists. Display a big empty box.
\begin{tikzpicture}
\useasboundingbox (0pt,0pt) rectangle (\textwidth,\l_intfig_height_dim);
\draw[dashed] (0pt,0pt) rectangle ( \textwidth,\l_intfig_height_dim);
\node at (\textwidth / 2,\l_intfig_height_dim / 2) {The\ drawing\ is\ not\ available\ to\ load.};
\draw (5pt,5pt) rectangle ( \textwidth - 5,\l_intfig_height_dim - 5);
\end{tikzpicture}
}
% Note values for the next run.
\zsaveposy { \l_intfig_figname_str }
\zlabel{ \l_intfig_figname_str -pageno}
\group_end:
}
% Parser for manditory arguments.
\NewDocumentCommand{\intfig_manargs}{ m m }
{
\str_set:Nn \l_intfig_figname_str { #1 }
\dim_set:Nn \l_intfig_height_dim { #2 }
}
% Parser for optional arguments.
\NewDocumentCommand{\intfig_optargs} { }
{
\seq_set_from_clist:NN \l_intfig_optseq_seq { \l_intfig_optclist_clist }
\seq_get_left:NN \l_intfig_optseq_seq \l_intfig_firstvalue_tl
\intfig_if_length:VTF \l_intfig_firstvalue_tl
{
\dim_set:Nn \l_intfig_interactiveHeight_dim \l_intfig_firstvalue_tl
\bool_set_true:N \l_intfig_hasintht_bool
}
{
% If it's not a dimension, then ignore it.
}
% Check whether each possible boolean flag has been set.
\clist_if_in:NnTF \l_intfig_optclist_clist { external }
{
\bool_set_true:N \l_intfig_external_bool
}{}
\clist_if_in:NnTF \l_intfig_optclist_clist { nostatic }
{
\bool_set_true:N \l_intfig_nostatic_bool
}{}
\clist_if_in:NnTF \l_intfig_optclist_clist { done }
{
\bool_set_true:N \l_intfig_done_bool
}{}
}
% Regex to determine whether an input is a dimension. I can't say that
% I really understand what's going on here, other than a bit of copy
% and paste and help from stack overflow.
\prg_new_protected_conditional:Nnn \intfig_if_length:n { T, F, TF }
{
\regex_match:nnTF
% Note that I only allow positive values.
{ \A [+]? ((\d+(\.\d*)?)|(\.\d+)) \s* (pt|pc|in|bp|cm|mm|dd|cc|sp|ex|em) \Z}
{ #1 } % test string
{ \prg_return_true: }
{ \prg_return_false: }
}
\prg_generate_conditional_variant:Nnn \intfig_if_length:n { V } { T, F, TF }
\ExplSyntaxOff
\usepackage{tikz}
\begin{document}
\begin{intfig}{bezier,200bp}
\\ Code for an external framework goes here.
\\ This is saved to bezier.fjs to be loaded by the framework.
\\ It could be JavaScript or Java (or whatever the framework is written to expect).
\end{intfig}
Here, {\tt bezier} is the ``name'' of the figure, and {\tt 200bp} is
the figure's height. The {\tt intfig} environment writes the body of
the environment out to a file ({\tt bezier.fjs} in this case). Then it
looks for a file, {\tt bezier.tikz}. If that file exists, it is
inserted; if it doesn't exist, then a ``not available'' message
appears in a figure of the given height.
In addition to swapping the body, {\tt intfig} writes information
about the page layout to {\tt figures.aux}: the page on which the
figure appears, together with its position on the page, the margin
sizes, text width, vertical location of the figure on the page, figure
height, figure name, and certain boolean values.
The interactive viewer might produce the following
as {\tt bezier.tikz}. If this file exists, it will be loaded as the
body of the {\tt intfig} above.
\begin{verbatim}
\begin{tikzpicture}[yscale=-1]
\useasboundingbox (0bp,0bp) rectangle (343.71109bp,200bp);
\draw[line width=4bp] (63.56039999999999bp, 27.39158999999995bp) -- (23.560399999999987bp, 57.39158999999995bp) -- (96.56039999999999bp, 121.39158999999995bp) -- (199.5604bp, 79.39158999999995bp) ;
\draw[line width=4bp] (63.56039999999999bp, 27.39158999999995bp) .. controls (23.560399999999987bp, 57.39158999999995bp) and (96.56039999999999bp, 121.39158999999995bp) .. (199.5604bp, 79.39158999999995bp);
\fill (63.56039999999999bp,27.39158999999995bp) ellipse [x radius=3bp,y radius =3bp];
\fill (23.560399999999987bp,57.39158999999995bp) ellipse [x radius=3bp,y radius =3bp];
\fill (96.56039999999999bp,121.39158999999995bp) ellipse [x radius=3bp,y radius =3bp];
\fill (199.5604bp,79.39158999999995bp) ellipse [x radius=3bp,y radius =3bp];
\end{tikzpicture}
\end{verbatim}
\end{document}