-
Notifications
You must be signed in to change notification settings - Fork 0
/
svg-to-pdf.cld
executable file
·494 lines (415 loc) · 14.7 KB
/
svg-to-pdf.cld
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
#!/usr/bin/env -S context --once --debug
-- -*- mode: lua; -*-
-- vim: syntax=lua
-- unnamed-emoji
-- https://github.com/gucci-on-fleek/unnamed-emoji
-- SPDX-License-Identifier: MPL-2.0+
-- SPDX-FileCopyrightText: 2023 Max Chernoff
-- Converts a set of SVG files into a single PDF file.
-- We use the shebang at the start to allow us to run this file directly.
--
-- ConTeXt usually reruns a document multiple times to resolve all the
-- references, but this document safely compiles in a single pass, so we disable
-- this with "--once".
--
-- We unfortunately need to run with "--debug" to enable the debug library so
-- that we can override some of the MP to PDF code. This isn't supported by the
-- ConTeXt developers, but it's not like the rest of this code would be
-- supported either.
-----------------
--- Constants ---
-----------------
local lfont = font -- LuaTeX font library
-- Scaling factors
local t3_scale <const> = 0.001 -- Standard PDF T3 scale
local t3_to_sp <const> = t3_scale * tex.sp("10bp")
-- Raw PDF "destination" dictionary
local dest_formatter <const> =
string.formatters["<< /D [ %i 0 R /XYZ 0 0 null ] >>"]
-- Set the fill/draw colour in the PDF
local colour_tuple <const> = string.formatters["%0.2f %0.2f %0.2f"]
local fill_draw_colour <const> = string.formatters["%s RG %s rg"]
-- Misc. Formatters
local glyph_licence <const> = string.formatters["%%%% %s\n%s"]
local hex_component <const> = string.formatters["%04x"]
-- Unicode
local ZWJ <const> = 0x200d
local EMOJI_VAR <const> = 0xfe0f
-- Modules
local components_to_names = require "components-to-names"
local font = require "font-config"
-- Font size
context.setupbodyfont { "10pt" }
---------------------------------
--- Generic utility functions ---
---------------------------------
--- Extracts an upvalue from a function.
---
--- Uses the `debug` library.
---
--- @param func function
--- @param name string The name of the upvalue
--- @return any - The upvalue
local function get_upvalue(func, name)
local nups <const> = debug.getinfo(func).nups
for i = 1, nups do
local current <const>, value <const> = debug.getupvalue(func, i)
if current == name then
return value
end
end
end
--- Iterator `{ { "a", "b" } } --> 1, "a", "b"`
local function unpacked_next(t, i)
local n, v = next(t, i)
if type(v) == "table" then
return n, table.unpack(v)
elseif n then
return n, n, v
end
end
--- Returns the average of a table of numbers.
---
--- @param t table<number>
--- @return number
local function average(t)
local sum = 0
for _, v in ipairs(t) do
sum = sum + v
end
return sum / #t
end
--- Memoizes a function call/table index.
---
--- @param func function<any, any>? The function to memoize
--- @return table - The "function"
local function memoized_table(func)
if not func then
func = function(key) return key end
end
return setmetatable({}, { __index = function(cache, key)
local ret = func(key, cache)
cache[key] = ret
return ret
end, __call = function(self, arg)
return self[arg]
end })
end
----------------------------
--- Processing Functions ---
----------------------------
-- pdfLaTeX uses PDF v1.5 by default, but ConTeXt uses v1.7. Let's force
-- v1.5 here to be safe.
lpdf.formats.data.emoji = table.copy(lpdf.formats.default)
lpdf.formats.data.emoji.pdf_version = 1.5
context.setupbackend { format = "emoji" }
-- Register the dropin method "rawpdf" to allow us to inject raw PDF code
-- as the contents of a Type 3 glyph.
if not rawget(lpdf, "registerfontmethod") then
-- ConTeXt LMTX contains the code to register a new font method, but it's
-- commented out (disabled) by default, so we need to manually edit the file
-- to forcibly enable it.
error('Please uncomment "lpdf.registerfontmethod" in "lpdf-emb.lmt"!')
end
lpdf.registerfontmethod("rawpdf", function(filename, details)
return
details.properties.indexdata[1], -- Table of glyphs
t3_scale, -- Scale factor to export the glyph at
function(char) -- Code to convert the character to PDF
return char.code, char.width / t3_to_sp
end,
function() end, -- "Reset"
function() end -- Add any used resources to the font dict
end)
--- Add the provided glyph to the current font.
---
--- @param codepoint integer The Unicode codepoint to register
--- @param unicode string The glyph as a Unicode string
--- @param width number The width of the glyph in sp's
--- @param height number The height of the glyph in sp's
--- @param code string The raw PDF stream to use for the glyph
local function make_glyph(codepoint, unicode, width, height, code)
-- Data for this glyph
local spec <const> = {
width = width * t3_to_sp,
height = height * t3_to_sp,
depth = 0,
unicode = { utf8.codepoint(unicode or "", 1, -1, true) },
code = width .. " 0 d0 " .. code,
}
-- Data for the original font
local tfmdata <const> = fonts.hashes.identifiers[lfont.current()]
-- Add the character metrics
tfmdata.characters[codepoint] = spec
-- Add the character drawing code
fonts.dropins.swapone(
"rawpdf",
tfmdata,
{ code = spec },
codepoint
)
-- Finally, add the character to the font
fonts.constructors.addcharacters(
lfont.current(),
{ characters = { [codepoint] = spec } }
)
end
-- Metafun wants to use transparencies and gradients when drawing SVGs;
-- however, these add extra objects to the PDF file. For a few hundred
-- glyphs, this wouldn't be a big deal; but for a few thousand, this makes
-- loading the PDF file quite slow. We use this questionable hack to remove
-- any of these unwanted effects.
if not font.keep_effects then utilities.sequencers.prependaction(
get_upvalue(metapost.installplugin, "processoractions"), -- sequence
"system", -- group
function(object, prescript, before, after) -- action
-- Move the transparencies and gradients to a temporary table
local saved <const> = {}
for pos, name, value in unpacked_next,prescript do
if name:match("^tr_") then -- Transparencies
saved[name] = value
prescript[pos] = nil
elseif name:match("^sh_") then -- Gradients
saved[name] = value
prescript[pos] = nil
end
end
if tonumber(saved.tr_transparency) and
tonumber(saved.tr_transparency) < 0.30
then -- "delete" objects with opacity < 30%
before[1] = "\n%"
after[2] = "\n"
elseif saved.sh_type then -- replace gradients with their average color
-- Individual colour components
local rs <const> = {}
local gs <const> = {}
local bs <const> = {}
for name, value in pairs(saved) do
if name:match("^sh_color_[ab]_") then
-- Append each gradient stop
local r, g, b = value:match("(.*):(.*):(.*)")
rs[#rs+1] = r or value
gs[#gs+1] = g or value
bs[#bs+1] = b or value
end
end
if #rs == 0 then -- fallback
for _, value in ipairs{saved.sh_color_a, saved.sh_color_b} do
local r, g, b = value:match("(.*):(.*):(.*)")
rs[#rs+1] = r or value
gs[#gs+1] = g or value
bs[#bs+1] = b or value
end
end
-- If all the colours are identical, then there's probably a
-- `stop-opacity` somewhere, so we'll just discard it.
local all_equal = true
for i = 1, #rs do
if rs[i] ~= rs[1] or gs[i] ~= gs[1] or bs[i] ~= bs[1] then
all_equal = false
break
end
end
if all_equal then
before[1] = "\n%"
after[2] = "\n"
else
-- Add the average value as a solid fill/outline
local values <const> = colour_tuple(
average(rs), average(gs), average(bs)
)
before[2] = fill_draw_colour(values, values)
end
end
end
) end
-- Add "named destinations" for each page with the glyph name/codepoint.
-- Automatically memoized since the `__index` metamethod only calls the function
-- once per key.
local dests <const> = table.setmetatableindex(function(t, k)
local v = lpdf.delayedobject(dest_formatter(lpdf.pagereference(k)))
t[k] = v
return v
end)
local used_names <const> = {}
--- Makes a named destination to the current page.
---
--- @param name string
local function make_name(name)
if not used_names[name] then
lpdf.registerdestination(
"emoji" .. tostring(name):lower():gsub("\u{fe0f}", ""),
dests[tex.count.realpageno]
)
used_names[name] = true
else
print(
"Duplicate name",
name,
document.getargument("font"),
tex.count.realpageno
)
end
end
-- Save the list of characters that we've processed so far so that we don't make
-- duplicate named destinations.
local chars <const> = {}
-----------------------------
--- Process each SVG file ---
-----------------------------
local function process(name)
-- Get the character components of the current glyph
local components <const> = font.get_components(name)
if #components == 0 then
return
end
-- The character as a Unicode string
local codepoint_dec <const> = {}
for _, component in ipairs(components) do
codepoint_dec[#codepoint_dec+1] = tonumber(component, 16)
end
local char <const> = utf8.char(table.unpack(codepoint_dec))
local codepoint = utf8.codepoint(char)
-- Skip PUA characters
if characters.data[codepoint].description:match("PRIVATE") then
return
end
-- Each codepoint in a font can only have a single definition, so we use a
-- codepoint from the PUA if we have duplicates.
if not chars[char] and utf8.len(char) == 1 then
chars[char] = true
else
codepoint = fonts.helpers.sharedprivates[table.concat(components, "-")]
end
-- SVG file contents
local contents <const> = select(2, resolvers.loadbinfile(name))
-- ConTeXt can't process SVGs with embedded stylesheets
if contents:match("text/css") then
return
end
-- Convert the SVG to MetaPost
local mp_code = metapost.svgtomp { data = contents }
-- Scale and shift the MetaPost image
if mp_code:match("^draw") then
mp_code = mp_code
:gsub("clip currentpicture.-;", "\ninterim truecorners := 1;")
:gsub("^draw ", "picture p; p :=")
else
mp_code = mp_code
:gsub("^", "picture p; p := image(")
:gsub("$", "; interim truecorners := 1 ;);")
end
mp_code = mp_code
:gsub(
";%s*$",
"; p := p shifted -llcorner p; draw p scaled " ..
font.mp_scale .. " ;"
)
:gsub(
'withcolor "url%([^)]-%)"',
"withopacity 0"
)
do -- We need to sort the gradient orders to produce a valid PDF
mp_code = mp_code
:gsub("[ \n]*(withshadestep *%b())[\n ]*", "\n%1\n")
:gsub("%s*\n+%s", "\n")
:splitlines()
local seen = {}
local count = 0
for i, line in ipairs(mp_code) do
local frac = tonumber(line:match("withshadefraction%s*([%d.]+)"))
if frac then
seen[frac] = i
count = count + 1
elseif count > 0 then
local first = i - count
local i = 0
for frac, pos in table.sortedpairs(seen) do
mp_code[first + i] = mp_code[pos]
i = i + 1
end
seen = {}
count = 0
end
end
mp_code = table.concat(mp_code, "\n")
end
-- Render the Metafun image to PDF
local pdf_code, width, height = metapost.simple(
"metafun", -- "Instance"
mp_code, -- MP code
true, -- Use Metafun extensions?
false, -- Wrap in begfig/endfig?
"svg" -- Name
)
-- 3 digits of precision is plenty
pdf_code = pdf_code
:gsub("(%D%d%.%d%d)%d*", "%1")
:gsub("(%D%d%d%.%d)%d*", "%1")
:gsub("(%d%d%d)%.%d*", "%1")
-- Inject the licence data into each glyph
pdf_code = glyph_licence(font.licence, pdf_code)
-- Add the glyph to the font
make_glyph(codepoint, char, width, height, pdf_code)
-- Add the named destinations
make_name(char)
local names <const> = components_to_names(components)
for _, component in ipairs(names) do
make_name(component)
end
-- Ensure that we always have a unique name for each glyph
if not table.contains(names, tostring(codepoint)) then
make_name(tostring(codepoint))
end
-- Add any missing names
if #names == 0 and #components > 1 then
local char_names <const> = {}
local char_hexes <const> = {}
for _, code in ipairs(components) do
code = tonumber(code, 16)
if code ~= EMOJI_VAR then
if code ~= ZWJ then
table.insert(char_names, characters.data[code].description)
end
table.insert(char_hexes, hex_component(code))
end
end
make_name(
table.concat(char_names, " ")
:gsub(" ?TAG LATIN SMALL LETTER ?", "")
:gsub(" ?CANCEL TAG", "")
:gsub("WAVING BLACK FLAG ?", "flag: ")
)
make_name(table.concat(char_hexes, "-"))
end
-- Set the page number
context.pushcatcodes("text")
context.setupuserpagenumber {
viewerprefix = char,
state = "stop",
}
context.popcatcodes()
-- Add the glyph to the document
context.startTEXpage()
context.verbatim(utf8.char(codepoint))
context.stopTEXpage()
context.step()
-- Page number, again
for _, page in ipairs(structures.pages.tobesaved) do
page.status = "stop" -- "status" should maybe be "state"?
end
end
context.starttext()
-- `context.stepwise` + `context.step` ensures that the Lua and TeX code run in
-- sync.
context.stepwise(function() print(xpcall(function()
-- Process each SVG file in the input directory
local paths = memoized_table()
dir.globpattern(document.getargument("in"), "/*.svg$", true, paths)
setmetatable(paths, nil)
table.sort(paths)
for _, path in ipairs(paths) do
process(path)
end
end, debug.traceback)) end)
context.stoptext()