• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sile-typesetter / sile / 9409557472

07 Jun 2024 12:09AM UTC coverage: 69.448% (-4.5%) from 73.988%
9409557472

push

github

alerque
fix(build): Distribute vendored compat-5.3.c source file

12025 of 17315 relevant lines covered (69.45%)

6023.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.62
/outputters/libtexpdf.lua
1
local base = require("outputters.base")
80✔
2
local pdf = require("justenoughlibtexpdf")
80✔
3

4
local cursorX = 0
80✔
5
local cursorY = 0
80✔
6

7
local started = false
80✔
8
local lastkey = false
80✔
9

10
local debugfont = SILE.font.loadDefaults({ family = "Gentium Plus", language = "en", size = 10 })
80✔
11

12
local glyph2string = function (glyph)
13
   return string.char(math.floor(glyph % 2 ^ 32 / 2 ^ 8)) .. string.char(glyph % 0x100)
63,363✔
14
end
15

16
local _dl = 0.5
80✔
17

18
local _debugfont
19
local _font
20

21
local outputter = pl.class(base)
80✔
22
outputter._name = "libtexpdf"
80✔
23
outputter.extension = "pdf"
80✔
24

25
-- N.B. Sometimes setCoord is called before the outputter has ensured initialization.
26
-- This ok for coordinates manipulation, at these points we know the page size.
27
local deltaX
28
local deltaY
29
local function trueXCoord (x)
30
   if not deltaX then
7,718✔
31
      local sheetSize = SILE.documentState.sheetSize or SILE.documentState.paperSize
80✔
32
      deltaX = (sheetSize[1] - SILE.documentState.paperSize[1]) / 2
80✔
33
   end
34
   return x + deltaX
7,718✔
35
end
36
local function trueYCoord (y)
37
   if not deltaY then
7,713✔
38
      local sheetSize = SILE.documentState.sheetSize or SILE.documentState.paperSize
79✔
39
      deltaY = (sheetSize[2] - SILE.documentState.paperSize[2]) / 2
79✔
40
   end
41
   return y + deltaY
7,713✔
42
end
43

44
-- The outputter init can't actually initialize output (as logical as it might
45
-- have seemed) because that requires a page size which we don't know yet.
46
-- function outputter:_init () end
47

48
function outputter:_ensureInit ()
80✔
49
   if not started then
12,561✔
50
      local sheetSize = SILE.documentState.sheetSize or SILE.documentState.paperSize
80✔
51
      local w, h = sheetSize[1], sheetSize[2]
80✔
52
      local fname = self:getOutputFilename()
80✔
53
      -- Ideally we could want to set the PDF CropBox, BleedBox, TrimBox...
54
      -- Our wrapper only manages the MediaBox at this point.
55
      pdf.init(fname == "-" and "/dev/stdout" or fname, w, h, SILE.full_version)
80✔
56
      pdf.beginpage()
80✔
57
      started = true
80✔
58
   end
59
end
60

61
function outputter:newPage ()
80✔
62
   self:_ensureInit()
26✔
63
   pdf.endpage()
26✔
64
   pdf.beginpage()
26✔
65
end
66

67
-- pdf structure package needs a tie in here
68
function outputter._endHook (_) end
80✔
69

70
function outputter:finish ()
80✔
71
   self:_ensureInit()
80✔
72
   pdf.endpage()
80✔
73
   self:runHooks("prefinish")
80✔
74
   pdf.finish()
80✔
75
   started = false
80✔
76
   lastkey = nil
80✔
77
end
78

79
function outputter.getCursor (_)
80✔
80
   return cursorX, cursorY
7,470✔
81
end
82

83
function outputter.setCursor (_, x, y, relative)
80✔
84
   x = SU.cast("number", x)
16,192✔
85
   y = SU.cast("number", y)
16,192✔
86
   local offset = relative and { x = cursorX, y = cursorY } or { x = 0, y = 0 }
8,096✔
87
   cursorX = offset.x + x
8,096✔
88
   cursorY = offset.y + (relative and 0 or SILE.documentState.paperSize[2]) - y
8,096✔
89
end
90

91
function outputter:setColor (color)
80✔
92
   self:_ensureInit()
×
93
   if color.r then
×
94
      pdf.setcolor_rgb(color.r, color.g, color.b)
×
95
   end
96
   if color.c then
×
97
      pdf.setcolor_cmyk(color.c, color.m, color.y, color.k)
×
98
   end
99
   if color.l then
×
100
      pdf.setcolor_gray(color.l)
×
101
   end
102
end
103

104
function outputter:pushColor (color)
80✔
105
   self:_ensureInit()
26✔
106
   if color.r then
26✔
107
      pdf.colorpush_rgb(color.r, color.g, color.b)
26✔
108
   end
109
   if color.c then
26✔
110
      pdf.colorpush_cmyk(color.c, color.m, color.y, color.k)
×
111
   end
112
   if color.l then
26✔
113
      pdf.colorpush_gray(color.l)
×
114
   end
115
end
116

117
function outputter:popColor ()
80✔
118
   self:_ensureInit()
26✔
119
   pdf.colorpop()
26✔
120
end
121

122
function outputter:_drawString (str, width, x_offset, y_offset)
80✔
123
   local x, y = self:getCursor()
7,466✔
124
   pdf.colorpush_rgb(0, 0, 0)
7,466✔
125
   pdf.colorpop()
7,466✔
126
   pdf.setstring(trueXCoord(x + x_offset), trueYCoord(y + y_offset), str, string.len(str), _font, width)
29,864✔
127
end
128

129
function outputter:drawHbox (value, width)
80✔
130
   width = SU.cast("number", width)
12,108✔
131
   self:_ensureInit()
6,054✔
132
   if not value.glyphString then
6,054✔
133
      return
×
134
   end
135
   -- Nodes which require kerning or have offsets to the glyph
136
   -- position should be output a glyph at a time. We pass the
137
   -- glyph advance from the htmx table, so that libtexpdf knows
138
   -- how wide each glyph is. It uses this to then compute the
139
   -- relative position between the pen after the glyph has been
140
   -- painted (cursorX + glyphAdvance) and the next painting
141
   -- position (cursorX + width - remember that the box's "width"
142
   -- is actually the shaped x_advance).
143
   if value.complex then
6,054✔
144
      for i = 1, #value.items do
2,144✔
145
         local item = value.items[i]
1,765✔
146
         local buf = glyph2string(item.gid)
1,765✔
147
         self:_drawString(buf, item.glyphAdvance, item.x_offset or 0, item.y_offset or 0)
1,765✔
148
         self:setCursor(item.width, 0, true)
1,765✔
149
      end
150
   else
151
      local buf = {}
5,675✔
152
      for i = 1, #value.glyphString do
24,873✔
153
         buf[i] = glyph2string(value.glyphString[i])
38,396✔
154
      end
155
      buf = table.concat(buf, "")
5,675✔
156
      self:_drawString(buf, width, 0, 0)
5,675✔
157
   end
158
end
159

160
function outputter:_withDebugFont (callback)
80✔
161
   if not _debugfont then
26✔
162
      _debugfont = self:setFont(debugfont)
8✔
163
   end
164
   local oldfont = _font
26✔
165
   _font = _debugfont
26✔
166
   callback()
26✔
167
   _font = oldfont
26✔
168
end
169

170
function outputter:setFont (options)
80✔
171
   self:_ensureInit()
6,058✔
172
   local key = SILE.font._key(options)
6,058✔
173
   if lastkey and key == lastkey then
6,058✔
174
      return _font
5,341✔
175
   end
176
   local font = SILE.font.cache(options, SILE.shaper.getFace)
717✔
177
   if options.direction == "TTB" then
717✔
178
      font.layout_dir = 1
1✔
179
   end
180
   if SILE.typesetter.frame and SILE.typesetter.frame:writingDirection() == "TTB" then
1,434✔
181
      pdf.setdirmode(1)
1✔
182
   else
183
      pdf.setdirmode(0)
716✔
184
   end
185
   _font = pdf.loadfont(font)
717✔
186
   if _font < 0 then
717✔
187
      SU.error("Font loading error for " .. pl.pretty.write(options, ""))
×
188
   end
189
   lastkey = key
717✔
190
   return _font
717✔
191
end
192

193
function outputter:drawImage (src, x, y, width, height, pageno)
80✔
194
   x = SU.cast("number", x)
4✔
195
   y = SU.cast("number", y)
4✔
196
   width = SU.cast("number", width)
4✔
197
   height = SU.cast("number", height)
4✔
198
   self:_ensureInit()
2✔
199
   pdf.drawimage(src, trueXCoord(x), trueYCoord(y), width, height, pageno or 1)
6✔
200
end
201

202
function outputter:getImageSize (src, pageno)
80✔
203
   self:_ensureInit() -- in case it's a PDF file
2✔
204
   local llx, lly, urx, ury, xresol, yresol = pdf.imagebbox(src, pageno or 1)
2✔
205
   return (urx - llx), (ury - lly), xresol, yresol
2✔
206
end
207

208
function outputter:drawSVG (figure, x, y, _, height, scalefactor)
80✔
209
   self:_ensureInit()
4✔
210
   x = SU.cast("number", x)
8✔
211
   y = SU.cast("number", y)
8✔
212
   height = SU.cast("number", height)
8✔
213
   pdf.add_content("q")
4✔
214
   self:setCursor(x, y)
4✔
215
   x, y = self:getCursor()
8✔
216
   local sheetSize = SILE.documentState.sheetSize or SILE.documentState.paperSize
4✔
217
   local newy = y - SILE.documentState.paperSize[2] / 2 + height - sheetSize[2] / 2
4✔
218
   pdf.add_content(table.concat({ scalefactor, 0, 0, -scalefactor, trueXCoord(x), newy, "cm" }, " "))
8✔
219
   pdf.add_content(figure)
4✔
220
   pdf.add_content("Q")
4✔
221
end
222

223
function outputter:drawRule (x, y, width, height)
80✔
224
   x = SU.cast("number", x)
472✔
225
   y = SU.cast("number", y)
472✔
226
   width = SU.cast("number", width)
472✔
227
   height = SU.cast("number", height)
472✔
228
   self:_ensureInit()
236✔
229
   local paperY = SILE.documentState.paperSize[2]
236✔
230
   pdf.setrule(trueXCoord(x), trueYCoord(paperY - y - height), width, height)
708✔
231
end
232

233
function outputter:debugFrame (frame)
80✔
234
   self:_ensureInit()
26✔
235
   self:pushColor(SILE.types.color({ r = 0.8, g = 0, b = 0 }))
56✔
236
   self:drawRule(frame:left() - _dl / 2, frame:top() - _dl / 2, frame:width() + _dl, _dl)
182✔
237
   self:drawRule(frame:left() - _dl / 2, frame:top() - _dl / 2, _dl, frame:height() + _dl)
182✔
238
   self:drawRule(frame:right() - _dl / 2, frame:top() - _dl / 2, _dl, frame:height() + _dl)
182✔
239
   self:drawRule(frame:left() - _dl / 2, frame:bottom() - _dl / 2, frame:width() + _dl, _dl)
182✔
240
   -- self:drawRule(frame:left() + frame:width()/2 - 5, (frame:top() + frame:bottom())/2+5, 10, 10)
241
   local stuff = SILE.shaper:createNnodes(frame.id, debugfont)
26✔
242
   stuff = stuff[1].nodes[1].value.glyphString -- Horrible hack
26✔
243
   local buf = {}
26✔
244
   for i = 1, #stuff do
184✔
245
      buf[i] = glyph2string(stuff[i])
316✔
246
   end
247
   buf = table.concat(buf, "")
26✔
248
   self:_withDebugFont(function ()
52✔
249
      self:setCursor(frame:left():tonumber() - _dl / 2, frame:top():tonumber() + _dl / 2)
130✔
250
      self:_drawString(buf, 0, 0, 0)
26✔
251
   end)
252
   self:popColor()
26✔
253
end
254

255
function outputter:debugHbox (hbox, scaledWidth)
80✔
256
   self:_ensureInit()
×
257
   self:pushColor(SILE.types.color({ r = 0.8, g = 0.3, b = 0.3 }))
×
258
   local paperY = SILE.documentState.paperSize[2]
×
259
   local x, y = self:getCursor()
×
260
   y = paperY - y
×
261
   self:drawRule(x - _dl / 2, y - _dl / 2 - hbox.height, scaledWidth + _dl, _dl)
×
262
   self:drawRule(x - _dl / 2, y - hbox.height - _dl / 2, _dl, hbox.height + hbox.depth + _dl)
×
263
   self:drawRule(x - _dl / 2, y - _dl / 2, scaledWidth + _dl, _dl)
×
264
   self:drawRule(x + scaledWidth - _dl / 2, y - hbox.height - _dl / 2, _dl, hbox.height + hbox.depth + _dl)
×
265
   if hbox.depth > SILE.types.length(0) then
×
266
      self:drawRule(x - _dl / 2, y + hbox.depth - _dl / 2, scaledWidth + _dl, _dl)
×
267
   end
268
   self:popColor()
×
269
end
270

271
-- The methods below are only implemented on outputters supporting these features.
272
-- In PDF, it relies on transformation matrices, but other backends may call
273
-- for a different strategy.
274
-- ! The API is unstable and subject to change. !
275

276
function outputter:scaleFn (xorigin, yorigin, xratio, yratio, callback)
80✔
277
   xorigin = SU.cast("number", xorigin)
×
278
   yorigin = SU.cast("number", yorigin)
×
279
   local x0 = trueXCoord(xorigin)
×
280
   local y0 = -trueYCoord(yorigin)
×
281
   self:_ensureInit()
×
282
   pdf:gsave()
×
283
   pdf.setmatrix(1, 0, 0, 1, x0, y0)
×
284
   pdf.setmatrix(xratio, 0, 0, yratio, 0, 0)
×
285
   pdf.setmatrix(1, 0, 0, 1, -x0, -y0)
×
286
   callback()
×
287
   pdf:grestore()
×
288
end
289

290
function outputter:rotateFn (xorigin, yorigin, theta, callback)
80✔
291
   xorigin = SU.cast("number", xorigin)
6✔
292
   yorigin = SU.cast("number", yorigin)
6✔
293
   local x0 = trueXCoord(xorigin)
3✔
294
   local y0 = -trueYCoord(yorigin)
6✔
295
   self:_ensureInit()
3✔
296
   pdf:gsave()
3✔
297
   pdf.setmatrix(1, 0, 0, 1, x0, y0)
3✔
298
   pdf.setmatrix(math.cos(theta), math.sin(theta), -math.sin(theta), math.cos(theta), 0, 0)
3✔
299
   pdf.setmatrix(1, 0, 0, 1, -x0, -y0)
3✔
300
   callback()
3✔
301
   pdf:grestore()
3✔
302
end
303

304
-- Other rotation unstable APIs
305

306
function outputter:enterFrameRotate (xa, xb, y, theta) -- Unstable API see rotate package
80✔
307
   xa = SU.cast("number", xa)
2✔
308
   xb = SU.cast("number", xb)
2✔
309
   y = SU.cast("number", y)
2✔
310
   -- Keep center point the same?
311
   local cx0 = trueXCoord(xa)
1✔
312
   local cx1 = trueXCoord(xb)
1✔
313
   local cy = -trueYCoord(y)
2✔
314
   self:_ensureInit()
1✔
315
   pdf:gsave()
1✔
316
   pdf.setmatrix(1, 0, 0, 1, cx1, cy)
1✔
317
   pdf.setmatrix(math.cos(theta), math.sin(theta), -math.sin(theta), math.cos(theta), 0, 0)
1✔
318
   pdf.setmatrix(1, 0, 0, 1, -cx0, -cy)
1✔
319
end
320

321
function outputter.leaveFrameRotate (_)
80✔
322
   pdf:grestore()
4✔
323
end
324

325
-- Unstable link APIs
326

327
function outputter:setLinkAnchor (name, x, y)
80✔
328
   x = SU.cast("number", x)
6✔
329
   y = SU.cast("number", y)
6✔
330
   self:_ensureInit()
3✔
331
   pdf.destination(name, trueXCoord(x), trueYCoord(y))
9✔
332
end
333

334
local function borderColor (color)
335
   if color then
1✔
336
      if color.r then
1✔
337
         return "/C [" .. color.r .. " " .. color.g .. " " .. color.b .. "]"
1✔
338
      end
339
      if color.c then
×
340
         return "/C [" .. color.c .. " " .. color.m .. " " .. color.y .. " " .. color.k .. "]"
×
341
      end
342
      if color.l then
×
343
         return "/C [" .. color.l .. "]"
×
344
      end
345
   end
346
   return ""
×
347
end
348
local function borderStyle (style, width)
349
   if style == "underline" then
1✔
350
      return "/BS<</Type/Border/S/U/W " .. width .. ">>"
×
351
   end
352
   if style == "dashed" then
1✔
353
      return "/BS<</Type/Border/S/D/D[3 2]/W " .. width .. ">>"
×
354
   end
355
   return "/Border[0 0 " .. width .. "]"
1✔
356
end
357

358
function outputter:startLink (_, _) -- destination, options as argument
80✔
359
   self:_ensureInit()
×
360
   -- HACK:
361
   -- Looking at the code, pdf.begin_annotation does nothing, and Simon wrote a comment
362
   -- about tracking boxes. Unsure what he implied with this obscure statement.
363
   -- Sure thing is that some backends may need the destination here, e.g. an HTML backend
364
   -- would generate a <a href="#destination">, as well as the options possibly for styling
365
   -- on the link opening?
366
   pdf.begin_annotation()
×
367
end
368

369
function outputter.endLink (_, dest, opts, x0, y0, x1, y1)
80✔
370
   local bordercolor = borderColor(opts.bordercolor)
1✔
371
   local borderwidth = SU.cast("integer", opts.borderwidth)
1✔
372
   local borderstyle = borderStyle(opts.borderstyle, borderwidth)
1✔
373
   local target = opts.external and "/Type/Action/S/URI/URI" or "/S/GoTo/D"
1✔
374
   local d = "<</Type/Annot/Subtype/Link" .. borderstyle .. bordercolor .. "/A<<" .. target .. "(" .. dest .. ")>>>>"
1✔
375
   pdf.end_annotation(
2✔
376
      d,
1✔
377
      trueXCoord(x0),
1✔
378
      trueYCoord(y0 - opts.borderoffset),
1✔
379
      trueXCoord(x1),
1✔
380
      trueYCoord(y1 + opts.borderoffset)
1✔
381
   )
382
end
383

384
-- Bookmarks and metadata
385

386
local function validate_date (date)
387
   return string.match(date, [[^D:%d+%s*-%s*%d%d%s*'%s*%d%d%s*'?$]]) ~= nil
×
388
end
389

390
function outputter:setMetadata (key, value)
80✔
391
   if key == "Trapped" then
×
392
      SU.warn("Skipping special metadata key \\Trapped")
×
393
      return
×
394
   end
395

396
   if key == "ModDate" or key == "CreationDate" then
×
397
      if not validate_date(value) then
×
398
         SU.warn("Invalid date: " .. value)
×
399
         return
×
400
      end
401
   else
402
      -- see comment in on bookmark
403
      value = SU.utf8_to_utf16be(value)
×
404
   end
405
   self:_ensureInit()
×
406
   pdf.metadata(key, value)
×
407
end
408

409
function outputter:setBookmark (dest, title, level)
80✔
410
   -- For annotations and bookmarks, text strings must be encoded using
411
   -- either PDFDocEncoding or UTF16-BE with a leading byte-order marker.
412
   -- As PDFDocEncoding supports only limited character repertoire for
413
   -- European languages, we use UTF-16BE for internationalization.
414
   local ustr = SU.utf8_to_utf16be_hexencoded(title)
3✔
415
   local d = "<</Title<" .. ustr .. ">/A<</S/GoTo/D(" .. dest .. ")>>>>"
3✔
416
   self:_ensureInit()
3✔
417
   pdf.bookmark(d, level)
3✔
418
end
419

420
-- Assumes the caller known what they want to stuff in raw PDF format
421
function outputter:drawRaw (literal)
80✔
422
   self:_ensureInit()
6✔
423
   pdf.add_content(literal)
6✔
424
end
425

426
return outputter
80✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc