较长的文章
我想向你们介绍我的长期任务,希望有一天能够得到解决。
我们有两个字形(任何字体、字形、位置、变换),一个固定,一个可移动,还有一些(随机选择的)方向。如何找到这两个字形接触的第二个字形的位置?
我正在使用手动定位,它不能用于以下任务词云幅度或其他优化任务(用较小的字形填充字形,如沙普帕包可以处理基本形状等)。我没有对词云(也称为标签云)方法和算法进行广泛的研究,但我的第一印象是算法使用单词或字形的边界框。但我可能错了(我是),请看这个例子维基百科。我们可以清楚地看到字母里面有单词,所以他们使用了一些复杂的算法。这是一个很好的起点:http://www.jasondavies.com/wordcloud/about/。 更多关于算法。
给计算机科学家的提示。此任务属于碰撞检测(处理图像)。它不仅在可视化方面有广泛的用途。我附上了一个词云示例和填充(任何)空格/字形示例:
让我来说明一下这种情况。让我们从拉丁现代字母系列中选择字母a
和b
,从左到右方向,不进行变换。我们得到(在这个例子中我使用的是\kern
):
更新:从数学角度来看,当我们将第二个字形进一步向左移动时,可能会有更多的解决方案。例如,当我们ab
在第一个示例中排版时,我们得到了第二个解决方案:
让我们尝试另一个示例,对字母b
和方向进行一些 20 度的变换(我使用 TikZ 进行字形移动):
找到交点后,我们可以将第二个字形向后移动,以在该方向上的两个字形之间保留一定量的空白。
这是另一个可用于两步优化的示例。第一步是找到一个交叉点,在设置锚点后,我们可以缩放(或旋转)第二个字形,然后找到下一个交叉点。这是一个相当复杂的例子:
我们有什么选择?
我对第二个字形进行了手动变换(位置、旋转、缩放)。这不是实用的方法。
我的第一个(糟糕的)想法是获取一系列光栅图形,然后计算非白色像素的数量。如果字母a
和b
排版的像素数与两个字母排版在一起的像素数相同,那就证明它们还没有接触。这在计算上是无效的。更新:当我们尝试移动字母时词云生成器的工作原理,看起来他们使用了栅格化和这种技术。他们使用螺旋路径来找到放置下一个单词的最佳位置。这可能不是一个糟糕的想法。如果我们能够即时栅格化矢量图形并在像素级别对其进行处理,那么这将是 TeX.SX 的一个有趣问题。我在考虑 Lua(TeX)及其工具(或者 TikZ 和一个新的外部化库,也许?)。我附上了 Jason Davies 的一个例子网站。他的主要网站和那些例子很鼓舞人心,http://www.jasondavies.com/。红色表示字形没有接触,而橙色表示它们重叠。
我希望可以关闭字距调整并将第二个字形移动其左轴承。但这还不够,因为我们还试图考虑字形变换。
当我注意到字形由贝塞尔曲线组成时,我开始相信我离目标越来越近了,这些曲线以控制点的形式存储在字体文件 (PFB、TTF、OTF) 中。然后我们可以从字体文件中获取贝塞尔曲线。我附上了一个来自Metapost 手册,请参阅第 50 页了解更多详细信息,其中可以看到控制点。Metapost 的原生输出格式是 PostScript,我们可以通过 Metapost 或其他转换工具轻松获得 SVG 和 PNG。我们可以使用编辑将 PostScript 格式导出为更多矢量格式。
我们运行以下几行:
mpost mal-mpost.mp
ps2pdf mal-mpost.56
pdfcrop --hires mal-mpost.56.pdf
的内容mal-mpost.mp
是这样的:
fontmapfile "=lm-ec.map";
beginfig(56);
picture q;
path p;
interim ahlength := 12bp;
interim ahangle := 25;
q := glyph "Dcaron" of "ec-lmr10" scaled .2;
for item within q:
p := pathpart item;
drawarrow p withcolor (.6,.9,.6)
withpen pencircle scaled 1.5;
for j=0 upto length p:
pickup pencircle scaled .7;
draw (point j of p -- precontrol j of p)
dashed evenly withcolor blue;
draw (point j of p -- postcontrol j of p)
dashed evenly withcolor blue;
pickup pencircle scaled 3;
draw precontrol j of p withcolor red;
draw postcontrol j of p withcolor red;
pickup pencircle scaled 2;
draw point j of p withcolor black;
endfor
endfor
endfig;
bye;
另一种方法是使用FontForge并将字形导出到单个 SVG 文件或一系列 SVG 文件(每个字形一个文件)。我们的输入将如下所示(剪切版本):
<path fill="#000000" stroke="none" d="
M 16.07 598.52
C 12.15 596.38 10.29 593.46 10.29 589.48
C 10.29 584.21 13.72 581.58 25.70 577.59
C 43.87 571.58 60.97 564.17 63.35 561.28
C 64.70 559.64 65.31 557.52 64.85 556.06
C 63.64 552.26 66.69 541.58 70.74 535.48
C 72.76 532.45 76.78 528.51 79.69 526.75
C 86.19 522.82 101.12 511.82 102.10 510.26
SVG 格式用于M
移动、L
直线和C
贝塞尔曲线。从上面的例子中我们可以看出,仅有控制点对我们来说是不够的,我们需要重建贝塞尔曲线。
有一个小小的改进空间。我们可以使用svg2tikz
脚本将其转换为 TikZ 代码。TikZ 代码如下所示(剪切版本):
\begin{tikzpicture}[y=0.80pt, x=0.8pt,yscale=-1, inner sep=0pt, outer sep=0pt]
\begin{scope}[shift={(0,0)},scale=1.000]
\path[fill=black] (16.0700,598.5200) .. controls (12.1500,596.3800) and
(10.2900,593.4600) .. (10.2900,589.4800) .. controls (10.2900,584.2100) and
(13.7200,581.5800) .. (25.7000,577.5900) .. controls (43.8700,571.5800) and
(60.9700,564.1700) .. (63.3500,561.2800) .. controls (64.7000,559.6400) and
(65.3100,557.5200) .. (64.8500,556.0600) .. controls (63.6400,552.2600) and
(66.6900,541.5800) .. (70.7400,535.4800) .. controls (72.7600,532.4500) and
(76.7800,528.5100) .. (79.6900,526.7500) .. controls (86.1900,522.8200) and
(101.1200,511.8200) .. (102.1000,510.2600) .. controls (102.4800,509.6300) and
我在想这可能是一种方法。一旦我们有了一系列曲线,我们就可以找到交点。我附上一段我认为可能有效的代码。这是两条路径的交点,我选择了左右方向。TikZ\t
中的变量保存有关点数(0、1、2、...)的信息。当\t=0
这意味着路径不接触时,任何其他值都意味着它们接触。我们可以得到交点的位置,请参阅 TikZ 手册和有关交点库的部分,请参阅 TikZ3 手册中的第 139 页及以上或第 987 页及以上。
我确信在其他程序(Metapost、PSTricks、Asymptote 等)中查找交叉点会比这更容易。我的想法是准备两组路径,每个字形一组,然后将一组中的所有路径与第二组中的路径进行比较。理论上,这应该可以确保我们不会错过任何交叉点。这需要进行一些实验。
LuaTeX 正在预处理字体文件,并在 Lua 文件中存储有关字形的信息,例如请参阅lmroman10-regular.lua
(剪切版本)。我们可以找到边界框和其他信息,但找不到贝塞尔曲线的控制点。这会大大增加 Lua 文件的大小。我们可以使用 FontForge 及其原生SFD 格式或者我们可以使用其他工具将 TTF/OTF 字体文件转换为 XML,例如泰特克斯(已测试),或者转换为 SVG,例如 ttftosvg(http://everythingfonts.com/ttf-to-svg)(未经测试)。我们将获得控制点,但它不会处理字形转换。
return {
["cache_version"]=2.749,
["descriptions"]={
[32]={
["boundingbox"]=1,
["index"]=103,
["name"]="space",
["width"]=333,
},
[33]={
["boundingbox"]={ 86, 0, 192, 716 },
["index"]=53,
["name"]="exclam",
["width"]=278,
},
[34]={
["boundingbox"]={ 102, 423, 272, 705 },
["index"]=93,
["name"]="quotedbl",
["slookups"]={
["ctx_tlig_1_1"]=8221,
},
["width"]=374,
},
我附上了文章中提到的片段的 TeX 代码。我们可以运行xelatex
和lualatex
直接处理 OTF 文件加载的引擎。
% run: xelatex or lualatex mal-intersection.tex
\documentclass[a4paper,landscape]{article}
\pagestyle{empty}
\usepackage{tikz}
\tikzset{inner sep=0pt, outer sep=0pt, yes/.style={green}, no/.style={red}}
\usetikzlibrary{intersections}
\usepackage{fontspec}
\setmainfont[Scale=15]{lmroman10-regular.otf}
\begin{document}
a{\color{red}b} % common typesetting
a\kern-6.5mm{\color{green}b} % manual solution
\begin{tikzpicture}
\begin{pgfinterruptboundingbox}
\draw[no,<-,line width=2mm] (5,4)--(8,4);
\end{pgfinterruptboundingbox}
\end{tikzpicture}%
\newpage
a{\color{red}b}\hspace{20mm} % common typesetting
a\kern-51mm{\color{green}b} % manual solution
\begin{tikzpicture}
\begin{pgfinterruptboundingbox}
\draw[no,<-,line width=2mm] (-12,4.5)--(-6,4.5);
\end{pgfinterruptboundingbox}
\end{tikzpicture}%
\newpage
\begin{tikzpicture}
\node{a};
\node[rotate=45,no] at (20:3cm){b};
\draw[no,<-,line width=2mm] (20:5cm)--(20:7cm);
\end{tikzpicture}%
\begin{tikzpicture}
\node{a};
\node[rotate=45,yes] at (20:2.5cm){b};
\end{tikzpicture}
\newpage
\begin{tikzpicture}
\node{a};
\node[scale=0.1,yshift=-40mm,no,rotate=-45]{b};
\draw[no,->,line width=2mm] (0,2mm)--(0,10mm);
\end{tikzpicture}%
\begin{tikzpicture}
\node{a};
\node[scale=0.1,yshift=-13.5mm,no,rotate=-45]{b};
\draw[no,<-,line width=2mm] (0,2mm)--(0,10mm);
\end{tikzpicture}%
\begin{tikzpicture}
\node{a};
\node[scale=0.1,yshift=-13.5mm,rotate=-45,yes,scale=3,xshift=8mm,yshift=-13mm]{b};
% + setting an anchor
\end{tikzpicture}%
\begin{tikzpicture}[line width=1mm]
\draw[name path=first] (-5,10) .. controls (0,5) and (0,-5) .. (-1,-1);
\draw[name path=second,no] (1,10) .. controls (0,5) and (0,-5) .. (1,-1);
\draw[no,<-,line width=2mm] (-20mm,70mm)--(0,70mm);
\end{tikzpicture} %
\begin{tikzpicture}[line width=1mm]
\draw[name path=first] (-5,10) .. controls (0,5) and (0,-5) .. (-1,-1);
\draw[name path=second,xshift=-8.1mm,yes] (1,10) .. controls (0,5) and (0,-5) .. (1,-1);
\draw[name intersections={of=first and second, name=i, total=\t}]
node {\typeout{total points: \t}}; % total points: 2 (-8.1mm)
\end{tikzpicture}%
\end{document}
我的建议
1. 对/错
之后\checkme{glyphs}{glyphs}
我们可能会得到:
True
,如果字形有一些交点。子问题:我们有多少个?False
,如果没有。子答案:我们得到了 0 个交点。
2.距离
之后\checkdistance{glyphs}{glyphs}{angle}
我们可能会得到:
- 某些尺寸,例如
2.3cm
,测量该方向上这些字形之间的最小距离。 - 我们应该得到所有的解决方案,例如当字形在字形内时,接触点将从字形的另一侧获得,等等。
\dimenmax
如果没有解决方案,或者找不到交点,我们应该收到警告。子问题可能是获得某个交点的最近角度变化是多少。
3. 图形表示
- 如果字形已经重叠,我们应该在那些点(贝塞尔曲线的交点)处得到标记。
- 我们可能会得到不同颜色的重叠区域。
- 我们应该在日志文件和/或终端中获取交叉点列表作为坐标列表。
- 子问题可能是没有交点的最小距离(及其角度)是多少(我们将分离重叠的字形)。
4. 填充空间
- 如何用字形填充一些空白空间(使用位置、旋转和/或比例)以获得最大的字形。
- 我们应该能够修复一些参数,例如位置,并让优化与其他两个参数(旋转、缩放)一起起作用。
5. 栅格化
- 到目前为止,我已经写下了一些使用矢量形式的想法,但也许可以先将我们的字形栅格化,然后像单词/标签云中的算法一样进行一些计算。在 TeX 之外,我们使用 GhostScript(或一些后端工具,如 ImageMagick 或 GraphicsMagick)来处理此任务。
- 我们也许能够使用 TikZ 及其外部化库来获取光栅图形(或者我们应该在 pstoedit 工具的帮助下使用 PDF -> Metapost 转换?),但如何即时处理这些图片?也许借助 ImageMagick 和
\write18
?
更新:演示
我找到了一个有趣的 JavaScript 演示,请尝试一下http://paperjs.org/examples/path-intersections/为自己。
答案1
初步版本:概念证明
{Fanfare} 当我写下这些话的时候,我激动得甚至无法呼吸(我感觉很好,医生,我真的很好)。:-) 嗯,对我来说这是一个重要的 TeX 时刻,你是其中的一部分!
我在读通过 PSTricks 或 Tikz 实现凸版印刷效果我注意到安德鲁·史黛西转换了一些星火字体以某种方式转换为 TikZ 路径/曲线 - 我不确定源代码是 OTF/PFB 文件还是其他一些源文件(PL、VPL、WFF、EOT,也许?)。也许svgtopgf.pl脚本来自http://bazaar.launchpad.net/~tex-sx/tex-sx/development/files/170已经使用过(我尝试过一些随机的 SVG 图片,但效果不佳)。如果不做进一步研究,很难猜测,但它确实有效!
与此同时,我读了这个问题在 TikZ 中使用文本勾勒(填充)字形轮廓所有主流图形引擎(Metapost、PSTricks、Asymptote)都应该能够在曲线级别加载字形。我不确定 TikZ 是否能加载 SVG 路径,这可能会有所帮助。
让我们回到我们的最小工作示例。
步骤 1:下载转换为路径的字体
我下载了两个文件,pgflibraryshapes.letters.dtx 和 stikz-正常路径.tex,到我的工作目录。
第 2 步:安装 dtx 文件(它包含支持 TeX 文件)
我已经通过运行此行一次来处理/安装了第一个文件:
tex pgflibraryshapes.letters.dtx
步骤 3:运行示例并使用它
我们可以运行任何主要的 LaTeX 引擎,例如
lualatex mal-letters.tex
这是 TeX 文件的内容:
% run: any major LaTeX engine mal-letters.tex
% Based on and inspired by:
% https://tex.stackexchange.com/q/62570/86
\documentclass{article}
\pagestyle{empty}
\usepackage{tikz}
\usetikzlibrary{shapes.letters}
\addtolength{\textheight}{2in}
%\usetikzlibrary{fadings} % I am cutting down the example to bare minimum...
%\usetikzlibrary{shadows.blur}
\usetikzlibrary{intersections}
\pgfkeys{
/pgf/letter/.cd,
load font={stikz}{normal},
size=4,
load encoding=char,
every letter/.append style={
fill, draw=red, line width=1pt,
},
}% End of \pgfkeys...
\makeatletter
\tikzset{
use letter path/.code={%
\pgfscope
\pgftransformscale{\letter@size}%
\letter@path{\letter@encode{#1}}%
\endpgfscope
}% end of use letter...
}% end of \tikzset...
\makeatother
\begin{document}
%\newcount\malrotate
%\malrotate=-10
%\loop
%\advance\malrotate by 10
\foreach \malrotate in {0,10,...,180} {% 45 0,10,...,180
\begin{tikzpicture}
\begin{scope}[xshift=20mm, yshift=-7mm, rotate=\malrotate]
\path[name path global=first, use letter path=T, fill=red];
% draw=green, line width=1pt,
\end{scope}
\begin{scope}[yshift=6mm, xshift=2mm, rotate=-\malrotate]
\path[name path global=second, use letter path=B, fill=blue, opacity=0.4];
% draw=green, line width=1pt,
\end{scope}
\fill[name intersections={of=first and second, name=i, total=\t}] [black]
\ifnum\t>0%
node{%
%\pgfmathparse{\t}%
%\global\let\mtotal=\pgfmathresult
\typeout{Number of intersection points: \t}%
}%
\foreach \s in {1,...,\t} {%
(i-\s) circle (1pt) node[above]{\footnotesize\s}%
}%
\else
node{\typeout{There are no intersection points!}}%
\fi;
%\typeout{Number of intersection points: \t};
\end{tikzpicture} %
}% End of \foreach...
%\ifnum\malrotate<180\repeat
\end{document}
好消息是它能正常工作,坏消息是我们在循环内(\foreach
或\loop
... \repeat
)绘制图片时得到的交叉点数量不正确,因此可能存在错误(使用时name path global
?或者支持文件中存在问题?我目前不知道。)。第一条和最后一条消息肯定是正确的。
Number of intersection points: 4
Number of intersection points: 9
Number of intersection points: 20
Number of intersection points: 32
Number of intersection points: 19
Number of intersection points: 17
Number of intersection points: 23
Number of intersection points: 15
Number of intersection points: 13
Number of intersection points: 10
Number of intersection points: 16
Number of intersection points: 19
Number of intersection points: 17
Number of intersection points: 26
Number of intersection points: 19
Number of intersection points: 24
Number of intersection points: 19
Number of intersection points: 7
There are no intersection points!
此示例加载两个字形并命名它们。它在范围环境的帮助下对它们进行转换,因为\path
据我所知,它不能直接在命令中完成(尽管我cm
还没有测试过该参数)。然后我们找到交点并使用它们(请参阅TikZ 手册,搜索intersections
库)。如果我们一次画一张图,那么点的数量是正确的。
好吧,前面还有很多工作要做(转换一些其他字体,在真实的例子上使用不同的参数进行设置和测试优化),但这是核心。
我知道我很快就会需要它的实际任务:
- 即将发布的会议论文集封面由接触字形组成的背景图片,它应该是此示例的改进版本如何(半)自动区分不同脚本的字形?,
- 一个字形位于另一个字形内的字形中……例如,字母 T 位于字母 A 内,字母 T 和字母 B 都位于字母 B 内,并且,
- 将一些单词放在箭头内,并在术语周围保留一些特定的空白(这是词云的常见情况),作为当地戏剧的广告。
我附上了该页面的预览和带有一些附加设置的特写镜头。
答案2
下面给出的代码有一些缺陷,但我把它发布在了 ConTeXt 邮件列表中,结果为这个问题提供了多个出色的解决方案。如果你觉得我下面的尝试很有趣,你一定要看看:
https://mailman.ntg.nl/pipermail/ntg-context/2018/092723.html
最近我开始对 MetaPost 和宏包 MetaFun 感兴趣。你可以用它做很多很棒的事情,例如绘制字体轮廓。
将字体轮廓获取为 MetaPost 路径实际上是在 ConTeXt 中实现的,所以我接受了挑战并使用 LuaTeX 和 MetaFun 解决了这个问题。
因为我是一个装裱师,所以这也是与字体无关的,也就是说,你可以即时更换字体,而不必从外部描摹轮廓等。不幸的是,我还不知道为什么,一些交叉点不见了。我使用了这里描述的技术:获取MetaPost中的所有交点
这是一个文件,但我将其分成几个部分来处理不同的语法突出显示。
\documentclass{article}
\usepackage{fontspec}
\setmainfont{Crimson}
\usepackage{luacode}
\begin{luacode*}
-- We need some utilities from ConTeXt
callbacks = callbacks or {}
callbacks.supported = callbacks.supported or {}
dofile(kpse.find_file("util-fmt.lua"))
dofile(kpse.find_file("node-ini.lua"))
dofile(kpse.find_file("font-mps.lua"))
dofile(kpse.find_file("font-shp.lua"))
-- That's a simple reimplemetation of ConTeXt's \showshape macro
function outlinepaths(character)
local fontid = font.current()
local shapedata = fonts.hashes.shapes[fontid] -- by index
local chardata = fonts.hashes.characters[fontid] -- by unicode
local shapeglyphs = shapedata.glyphs or { }
character = utf.byte(character)
local c = chardata[character]
if c then
if not c.index then
return {}
end
local glyph = shapeglyphs[c.index]
if glyph and (glyph.segments or glyph.sequence) then
local units = shapedata.units or 1000
local factor = 100/units
local paths = fonts.metapost.paths(glyph,factor)
return paths
end
end
end
\end{luacode*}
\usepackage{luamplib}
\mplibsetformat{metafun}
\everymplib{beginfig(0);}
\everyendmplib{endfig;}
\edef\letterhash{\string#}
\def\mpdefineoutlines#1{\directlua{
local char = "\luaescapestring{#1}"
local outlines = outlinepaths("#1")
local len = \letterhash outlines
tex.print("path " .. char .. "[];")
tex.print(char .. "n := " .. len .. ";")
for i, path in ipairs(outlines) do
tex.print(char .. "[" .. i .. "] := " .. path .. ";")
end
}}
\begin{document}
\begin{mplibcode}
pair shift; shift := (1cm,-1cm);
numeric angle; angle := 5;
\mpdefineoutlines{B}
\mpdefineoutlines{T}
nofill B2;
nofill B3;
eofill B1 withcolor .5[blue,white];
fill T1 shifted (shift) rotated (angle) withcolor .5[red,white];
path r;
numeric n; n := 0;
for i = 1 upto Bn:
for j = 1 upto Tn:
r := B[i];
forever:
pair q;
r := r cutbefore (T[j] shifted (shift) rotated (angle));
exitif length cuttings = 0;
r := subpath(epsilon, length r) of r;
q = point 0 of r;
n := n + 1;
dotlabel.urt(textext("\tiny" & decimal n), q);
endfor;
endfor ;
endfor ;
\end{mplibcode}
\end{document}
更多摆姿势
希腊字母怎么样?
或者数学符号?