我正在阅读 Knuth 的《数学排版》(美国数学学会会刊1(2):337-372) 中的“随机化”一节引起了我的注意。我在此引用了其中的大部分内容。
我想报告一下我用随机数做的一个小实验。有人可能会抱怨我设计的字母太完美了,太像电脑了,所以它们缺乏“个性”。为了抵消这种影响,我们可以在绘制每个字母时在放置笔的位置的选择中建立一定程度的随机性,图 21 显示了发生的情况。关键笔位置的坐标是独立选择的,服从正态分布,标准差不断增加,因此第三个示例的标准差是第二个的两倍,第四个示例的标准差是第二个的三倍,依此类推。请注意,这两个米每一行(第一行除外)的 都不同,A和吨's,因为每个字母都是随机抽取的。
当偏差足够大时,结果就会变得有些荒谬,我不希望人们说我以对数学的嘲弄来结束这次演讲。所以让我们通过图 22 来结束本演讲,该图显示了当随机性程度受到一定控制时,各种字体中得到的结果。我认为可以说,这个最后例子中的字母具有一种温暖和魅力,让人很难相信它们是由计算机按照严格的数学规则生成的。也许过去美好的时光里数学印刷看起来如此好看的原因就是字体不完美且不一致。
我的问题是,有没有一种通用的方法可以实现这种效果?我在LaTeX 伴侣或者XeTeX 伴侣。我知道有些字体(例如 PunkNova)会这样做,但我正在寻找一种方法来使用我选择的任何字体获得此输出。Knuth 通过直接修改 Metafont 文件来实现这一点,我希望可以避免走这条路。
答案1
我已经思考这个问题好几个星期了,最后我觉得我已经接近你可能也会喜欢的结果了。我甚至尝试过使用加工解决这个问题,结果很棒的动画作为副产品,但它并没有让我更接近解决方案。但回到正题……
不幸的是,我发布的解决方案(也是我最好的也是唯一的尝试)不支持将扭曲的字形绘制为文本,而是绘制为图画。此外,还有一些工作需要在 LaTeX 之外完成,但大多数工作都是在 LaTeX(LuaTeX + TikZ)中完成的。
上图分别展示了一个未扭曲的字形(上行左侧的字符“a”)、一个扭曲的字形(上行右侧的字符“a”)、一个由扭曲的字形组成的单词(中间一行)和一个特殊字符(Omega),这些都可以在答案末尾的代码中找到。
现在我将描述我实现这些扭曲所遵循的过程。我提到,除了 LaTeX 之外,还有一些工作要做,那就是使用 FontForge 将字体文件转换为 SVG。我在问题的答案中找到了如何做到这一点的解决方案:我们可以从字体文件中提取构成字符的点吗?
将以下内容复制到名为font2svg.pe
“项目”文件夹的文件中。
#!/usr/bin/env fontforge
Open($1)
Generate($1:t:r + ".svg")
并使用以下命令从您想要使用的字体(我选择了 cmr10)创建一个 SVG 文件。
fontforge font2svg.pe /usr/local/texlive/2014/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb
请注意,字体在文件系统上的位置可能因您安装的 LaTeX 和使用的操作系统而异,但这会在您的项目文件夹中生成一个 SVG 文件。剩下的就是处理生成的 SVG 文件,其中包含字形的数据(名称、unicode 代码、宽度和轮廓),我将在下面描述。
该函数read_font_data(file)
以文件名作为参数(生成的 SVG 文件),并将字形的数据提取到关联数组中,该数组可以使用 unicode 代码寻址,并包含特定字符的宽度和轮廓数据。请注意,并非所有字形都有宽度或轮廓数据,虽然会进行一些基本的错误检查,但代码并非万无一失。
该函数random_in_interval(lower_boundary, upper_boundary)
接受两个浮点参数,并将返回它们之间的随机浮点数。边界越接近 1,随机化程度就越小。这将在需要随机化字形轮廓时使用。
该函数scale_and_randomize(glyph, scale_factor, lower_boundary, upper_boundary)
将采用字形、比例因子、下限和上限,后两者将用于随机化。需要缩放是因为 TikZ 的默认测量单位是厘米(我认为),而字形的轮廓数据可能包含较大的值,而 TikZ 会将其解释为厘米。请注意,比例因子可能会根据您使用的字体和所需的大小而有所不同。
函数print_glyph(glyph, scale_factor, lower_boundary, upper_boundary)
和return_glyph
(后者采用相同的参数)唯一的不同之处在于,它print_glyph
会将用于打印字形的 TikZ 绘图命令(使用svg.path
库)传递给 LaTeX,而return_glyph
只会将绘图命令作为字符串返回,该字符串可以在传递给 LaTeX 之前在 Lua 中进一步使用。
其余函数仅使用前面描述的print_glyph
和return_glyph
函数来打印上面的图片。
就是这样。希望这能满足您的需求。
\documentclass[10pt, a4paper]{article}
\usepackage[T1]{fontenc}
\usepackage{luacode}
\usepackage{tikz}
\usetikzlibrary{svg.path, positioning}
\pagestyle{empty}
\tikzset{%
glyph node/.style={%
inner sep=0pt,%
outer sep=0pt%
},%
glyph outline/.style={%
line width=0pt%
}%
}
\begin{luacode*}
function read_font_data(file)
local glyphs = {}
local fd = io.open(file, "r")
local content = fd:read("*all")
fd.close()
for glyph in string.gmatch(content, "<glyph[^/>]*") do
local glyph_tag = string.gsub(glyph, "\n", " ")
local unicode = string.match(glyph_tag, "unicode=\"[^\"]*")
local outline = string.match(glyph_tag, "d=\"[^\"]*")
local width = string.match(glyph_tag, "horiz%-adv%-x=\"[^\"]*")
if unicode ~= nil and #unicode >= 10 then
unicode = string.sub(unicode, 10, #unicode)
end
if outline ~= nil and #outline > 4 then
outline = string.sub(outline, 4, #outline)
end
if width ~= nil and #width >= 14 then
width = string.sub(width, 14, #width)
end
if unicode ~= nil then
glyphs[unicode] = {width, outline}
end
end
return glyphs
end
-- returns a random float number between the specified boundaries (floats)
function random_in_interval(lower_boundary, upper_boundary)
return ((math.random() * (upper_boundary - lower_boundary)) + lower_boundary)
end
-- note: scaling is applied before randomization
function scale_and_randomize(glyph, scale_factor, lower_boundary, upper_boundary)
local width = glyph[1]
local outline = glyph[2]
local previous_was_number = false
local processed_outline = ""
local number = ""
if width ~= nil then
width = width * scale_factor
end
if outline ~= nil then
for i = 1, #outline, 1 do
local char = string.sub(outline, i, i)
if previous_was_number then
if string.match(char, '%d') ~= nil or
char == "." then
number = number .. char
else
-- scale and randomize
number = number * scale_factor
number = number * random_in_interval(lower_boundary, upper_boundary)
number = string.format("%.3f", number)
processed_outline = processed_outline .. number .. char
number = ""
previous_was_number = false
end
else
if string.match(char, '%d') ~= nil or
char == "-" then
number = number .. char
previous_was_number = true
else
processed_outline = processed_outline .. char
previous_was_number = false
end
end
end
end
return {width, processed_outline}
end
function print_glyph(glyph, scale_factor, lower_boundary, upper_boundary)
local randomized_glyph = scale_and_randomize(glyph, scale_factor, lower_boundary, upper_boundary)
local width = randomized_glyph[1]
local outline = randomized_glyph[2]
if outline ~= nil then
tex.sprint("\\filldraw[glyph outline] svg \"" .. outline .. "\";")
end
end
function return_glyph(glyph, scale_factor, lower_boundary, upper_boundary)
local randomized_glyph = scale_and_randomize(glyph, scale_factor, lower_boundary, upper_boundary)
local width = randomized_glyph[1]
local outline = randomized_glyph[2]
if outline ~= nil then
return "\\filldraw[glyph outline] svg \"" .. outline .. "\";"
else
return ""
end
end
function draw_sample_glyphs(glyphs)
tex.sprint("\\begin{tikzpicture}")
tex.sprint("\\node[glyph node, matrix, anchor=south west] (a1) {" ..
return_glyph(glyphs["a"], 0.05, 1, 1) ..
"\\\\};")
tex.sprint("\\node[glyph node, matrix, anchor=south west, right=7.5mm of a1] (a2) {" ..
return_glyph(glyphs["a"], 0.05, 0.8, 1.2) ..
"\\\\};")
tex.sprint("\\end{tikzpicture}")
end
function draw_sample_text(glyphs)
local horizontal_space = "0.5mm"
local vertical_space = "1.25mm"
local scale = 0.05
local lower_boundary = 0.9
local upper_boundary = 1.1
tex.sprint("\\begin{tikzpicture}")
tex.sprint("\\node[glyph node, matrix] (m1) {" ..
return_glyph(glyphs["m"], scale, lower_boundary, upper_boundary) ..
"\\\\};")
tex.sprint("\\node[glyph node, matrix, right=" .. horizontal_space ..
" of m1] (a1) {" ..
return_glyph(glyphs["a"], scale, lower_boundary, upper_boundary) ..
"\\\\};")
tex.sprint("\\node[glyph node, matrix, right=" .. horizontal_space ..
" of a1] (t1) {" .. "\\raisebox{" .. vertical_space .. "}{" ..
return_glyph(glyphs["t"], scale, lower_boundary, upper_boundary) ..
"}" .. "\\\\};")
tex.sprint("\\node[glyph node, matrix, right=" .. horizontal_space ..
" of t1] (h1) {" .. "\\raisebox{" .. vertical_space .. "}{" ..
return_glyph(glyphs["h"], scale, lower_boundary, upper_boundary) ..
"}" .. "\\\\};")
tex.sprint("\\node[glyph node, matrix, right=" .. horizontal_space ..
" of h1] (e1) {" ..
return_glyph(glyphs["e"], scale, lower_boundary, upper_boundary) ..
"\\\\};")
tex.sprint("\\node[glyph node, matrix, right=" .. horizontal_space ..
" of e1] (m2) {" ..
return_glyph(glyphs["m"], scale, lower_boundary, upper_boundary) ..
"\\\\};")
tex.sprint("\\node[glyph node, matrix, right=" .. horizontal_space ..
" of m2] (a2) {" ..
return_glyph(glyphs["a"], scale, lower_boundary, upper_boundary) ..
"\\\\};")
tex.sprint("\\node[glyph node, matrix, right=" .. horizontal_space ..
" of a2] (t2) {" .. "\\raisebox{" .. vertical_space .. "}{" ..
return_glyph(glyphs["t"], scale, lower_boundary, upper_boundary) ..
"}" .. "\\\\};")
tex.sprint("\\node[glyph node, matrix, right=" .. horizontal_space ..
" of t2] (i1) {" .. "\\raisebox{" .. vertical_space .. "}{" ..
return_glyph(glyphs["i"], scale, lower_boundary, upper_boundary) ..
"}" .. "\\\\};")
tex.sprint("\\node[glyph node, matrix, right=" .. horizontal_space ..
" of i1] (c1) {" ..
return_glyph(glyphs["c"], scale, lower_boundary, upper_boundary) ..
"\\\\};")
tex.sprint("\\node[glyph node, matrix, right=" .. horizontal_space ..
" of c1] (s1) {" ..
return_glyph(glyphs["s"], scale, lower_boundary, upper_boundary) ..
"\\\\};")
tex.sprint("\\end{tikzpicture}")
end
function draw_sample_glyph(glyphs)
tex.sprint("\\begin{tikzpicture}")
print_glyph(glyphs["Ω"], 0.05, 0.95, 1.05)
tex.sprint("\\end{tikzpicture}")
end
function main()
local cmr10_glyphs = {}
math.randomseed(os.time())
cmr10_glyphs = read_font_data("cmr10.svg")
tex.sprint("\\noindent")
draw_sample_glyphs(cmr10_glyphs)
tex.sprint("\\\\[2cm]")
draw_sample_text(cmr10_glyphs)
tex.sprint("\\\\[2cm]")
draw_sample_glyph(cmr10_glyphs)
end
\end{luacode*}
\begin{document}
\luadirect{main()}
\end{document}