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

sile-typesetter / sile / 12313034533

13 Dec 2024 09:28AM UTC coverage: 60.234% (-0.7%) from 60.941%
12313034533

push

github

web-flow
Merge 5a7694dff into d737b2656

9 of 25 new or added lines in 5 files covered. (36.0%)

145 existing lines in 16 files now uncovered.

12801 of 21252 relevant lines covered (60.23%)

2545.46 hits per line

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

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

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

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

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

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

16
local _dl = 0.5
50✔
17

18
local _debugfont
19
local _font
20

21
local outputter = pl.class(base)
50✔
22
outputter._name = "libtexpdf"
50✔
23
outputter.extension = "pdf"
50✔
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
4,391✔
31
      local sheetSize = SILE.documentState.sheetSize or SILE.documentState.paperSize
50✔
32
      deltaX = (sheetSize[1] - SILE.documentState.paperSize[1]) / 2
50✔
33
   end
34
   return x + deltaX
4,391✔
35
end
36
local function trueYCoord (y)
37
   if not deltaY then
4,389✔
38
      local sheetSize = SILE.documentState.sheetSize or SILE.documentState.paperSize
49✔
39
      deltaY = (sheetSize[2] - SILE.documentState.paperSize[2]) / 2
49✔
40
   end
41
   return y + deltaY
4,389✔
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 ()
50✔
49
   if not started then
7,020✔
50
      local sheetSize = SILE.documentState.sheetSize or SILE.documentState.paperSize
50✔
51
      local w, h = sheetSize[1], sheetSize[2]
50✔
52
      local fname = self:getOutputFilename()
50✔
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)
50✔
56
      pdf.beginpage()
50✔
57
      started = true
50✔
58
   end
59
end
60

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

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

70
function outputter.abort ()
50✔
UNCOV
71
   if started then
×
UNCOV
72
      pdf.endpage()
×
UNCOV
73
      pdf.finish()
×
UNCOV
74
      started = false
×
NEW
75
      lastkey = false
×
76
   end
77
end
78

79
function outputter:finish ()
50✔
80
   -- allows generation of empty PDFs
81
   self:_ensureInit()
50✔
82
   pdf.endpage()
50✔
83
   self:runHooks("prefinish")
50✔
84
   pdf.finish()
50✔
85
   started = false
50✔
86
   lastkey = false
50✔
87
end
88

89
function outputter.getCursor (_)
50✔
90
   return cursorX, cursorY
4,155✔
91
end
92

93
function outputter.setCursor (_, x, y, relative)
50✔
94
   x = SU.cast("number", x)
8,806✔
95
   y = SU.cast("number", y)
8,806✔
96
   local offset = relative and { x = cursorX, y = cursorY } or { x = 0, y = 0 }
4,403✔
97
   cursorX = offset.x + x
4,403✔
98
   cursorY = offset.y + (relative and 0 or SILE.documentState.paperSize[2]) - y
4,403✔
99
end
100

101
function outputter:setColor (color)
50✔
102
   self:_ensureInit()
×
103
   if color.r then
×
104
      pdf.setcolor_rgb(color.r, color.g, color.b)
×
105
   end
106
   if color.c then
×
107
      pdf.setcolor_cmyk(color.c, color.m, color.y, color.k)
×
108
   end
109
   if color.l then
×
110
      pdf.setcolor_gray(color.l)
×
111
   end
112
end
113

114
function outputter:pushColor (color)
50✔
115
   self:_ensureInit()
25✔
116
   if color.r then
25✔
117
      pdf.colorpush_rgb(color.r, color.g, color.b)
25✔
118
   end
119
   if color.c then
25✔
120
      pdf.colorpush_cmyk(color.c, color.m, color.y, color.k)
×
121
   end
122
   if color.l then
25✔
123
      pdf.colorpush_gray(color.l)
×
124
   end
125
end
126

127
function outputter:popColor ()
50✔
128
   self:_ensureInit()
25✔
129
   pdf.colorpop()
25✔
130
end
131

132
function outputter:_drawString (str, width, x_offset, y_offset)
50✔
133
   local x, y = self:getCursor()
4,154✔
134
   pdf.colorpush_rgb(0, 0, 0)
4,154✔
135
   pdf.colorpop()
4,154✔
136
   pdf.setstring(trueXCoord(x + x_offset), trueYCoord(y + y_offset), str, string.len(str), _font, width)
16,616✔
137
end
138

139
function outputter:drawHbox (value, width)
50✔
140
   width = SU.cast("number", width)
6,624✔
141
   self:_ensureInit()
3,312✔
142
   if not value.glyphString then
3,312✔
143
      return
×
144
   end
145
   -- Nodes which require kerning or have offsets to the glyph
146
   -- position should be output a glyph at a time. We pass the
147
   -- glyph advance from the htmx table, so that libtexpdf knows
148
   -- how wide each glyph is. It uses this to then compute the
149
   -- relative position between the pen after the glyph has been
150
   -- painted (cursorX + glyphAdvance) and the next painting
151
   -- position (cursorX + width - remember that the box's "width"
152
   -- is actually the shaped x_advance).
153
   if value.complex then
3,312✔
154
      for i = 1, #value.items do
1,285✔
155
         local item = value.items[i]
1,051✔
156
         local buf = glyph2string(item.gid)
1,051✔
157
         self:_drawString(buf, item.glyphAdvance, item.x_offset or 0, item.y_offset or 0)
1,051✔
158
         self:setCursor(item.width, 0, true)
1,051✔
159
      end
160
   else
161
      local buf = {}
3,078✔
162
      for i = 1, #value.glyphString do
11,882✔
163
         buf[i] = glyph2string(value.glyphString[i])
17,608✔
164
      end
165
      buf = table.concat(buf, "")
3,078✔
166
      self:_drawString(buf, width, 0, 0)
3,078✔
167
   end
168
end
169

170
function outputter:_withDebugFont (callback)
50✔
171
   if not _debugfont then
25✔
172
      _debugfont = self:setFont(debugfont)
6✔
173
   end
174
   local oldfont = _font
25✔
175
   _font = _debugfont
25✔
176
   callback()
25✔
177
   _font = oldfont
25✔
178
end
179

180
function outputter:setFont (options)
50✔
181
   self:_ensureInit()
3,315✔
182
   local key = SILE.font._key(options)
3,315✔
183
   if lastkey and key == lastkey then
3,315✔
184
      return _font
2,707✔
185
   end
186
   local font = SILE.font.cache(options, SILE.shaper.getFace)
608✔
187
   if options.direction == "TTB" then
608✔
188
      font.layout_dir = 1
1✔
189
   end
190
   if SILE.typesetter.frame and SILE.typesetter.frame:writingDirection() == "TTB" then
1,216✔
191
      pdf.setdirmode(1)
1✔
192
   else
193
      pdf.setdirmode(0)
607✔
194
   end
195
   _font = pdf.loadfont(font)
608✔
196
   if _font < 0 then
608✔
197
      SU.error("Font loading error for " .. pl.pretty.write(options, ""))
×
198
   end
199
   lastkey = key
608✔
200
   return _font
608✔
201
end
202

203
function outputter:drawImage (src, x, y, width, height, pageno)
50✔
204
   x = SU.cast("number", x)
×
205
   y = SU.cast("number", y)
×
206
   width = SU.cast("number", width)
×
207
   height = SU.cast("number", height)
×
208
   self:_ensureInit()
×
209
   pdf.drawimage(src, trueXCoord(x), trueYCoord(y), width, height, pageno or 1)
×
210
end
211

212
function outputter:getImageSize (src, pageno)
50✔
213
   self:_ensureInit() -- in case it's a PDF file
×
214
   local llx, lly, urx, ury, xresol, yresol = pdf.imagebbox(src, pageno or 1)
×
215
   return (urx - llx), (ury - lly), xresol, yresol
×
216
end
217

218
function outputter:drawSVG (figure, x, y, _, height, scalefactor)
50✔
219
   self:_ensureInit()
1✔
220
   x = SU.cast("number", x)
2✔
221
   y = SU.cast("number", y)
2✔
222
   height = SU.cast("number", height)
2✔
223
   pdf.add_content("q")
1✔
224
   self:setCursor(x, y)
1✔
225
   x, y = self:getCursor()
2✔
226
   local sheetSize = SILE.documentState.sheetSize or SILE.documentState.paperSize
1✔
227
   local newy = y - SILE.documentState.paperSize[2] / 2 + height - sheetSize[2] / 2
1✔
228
   pdf.add_content(table.concat({ scalefactor, 0, 0, -scalefactor, trueXCoord(x), newy, "cm" }, " "))
2✔
229
   pdf.add_content(figure)
1✔
230
   pdf.add_content("Q")
1✔
231
end
232

233
function outputter:drawRule (x, y, width, height)
50✔
234
   x = SU.cast("number", x)
336✔
235
   y = SU.cast("number", y)
336✔
236
   width = SU.cast("number", width)
336✔
237
   height = SU.cast("number", height)
336✔
238
   self:_ensureInit()
168✔
239
   local paperY = SILE.documentState.paperSize[2]
168✔
240
   pdf.setrule(trueXCoord(x), trueYCoord(paperY - y - height), width, height)
504✔
241
end
242

243
function outputter:debugFrame (frame)
50✔
244
   self:_ensureInit()
25✔
245
   self:pushColor(SILE.types.color({ r = 0.8, g = 0, b = 0 }))
53✔
246
   self:drawRule(frame:left() - _dl / 2, frame:top() - _dl / 2, frame:width() + _dl, _dl)
175✔
247
   self:drawRule(frame:left() - _dl / 2, frame:top() - _dl / 2, _dl, frame:height() + _dl)
175✔
248
   self:drawRule(frame:right() - _dl / 2, frame:top() - _dl / 2, _dl, frame:height() + _dl)
175✔
249
   self:drawRule(frame:left() - _dl / 2, frame:bottom() - _dl / 2, frame:width() + _dl, _dl)
175✔
250
   -- self:drawRule(frame:left() + frame:width()/2 - 5, (frame:top() + frame:bottom())/2+5, 10, 10)
251
   local stuff = SILE.shaper:createNnodes(frame.id, debugfont)
25✔
252
   stuff = stuff[1].nodes[1].value.glyphString -- Horrible hack
25✔
253
   local buf = {}
25✔
254
   for i = 1, #stuff do
176✔
255
      buf[i] = glyph2string(stuff[i])
302✔
256
   end
257
   buf = table.concat(buf, "")
25✔
258
   self:_withDebugFont(function ()
50✔
259
      self:setCursor(frame:left():tonumber() - _dl / 2, frame:top():tonumber() + _dl / 2)
125✔
260
      self:_drawString(buf, 0, 0, 0)
25✔
261
   end)
262
   self:popColor()
25✔
263
end
264

265
function outputter:debugHbox (hbox, scaledWidth)
50✔
266
   self:_ensureInit()
×
267
   self:pushColor(SILE.types.color({ r = 0.8, g = 0.3, b = 0.3 }))
×
268
   local paperY = SILE.documentState.paperSize[2]
×
269
   local x, y = self:getCursor()
×
270
   y = paperY - y
×
271
   self:drawRule(x - _dl / 2, y - _dl / 2 - hbox.height, scaledWidth + _dl, _dl)
×
272
   self:drawRule(x - _dl / 2, y - hbox.height - _dl / 2, _dl, hbox.height + hbox.depth + _dl)
×
273
   self:drawRule(x - _dl / 2, y - _dl / 2, scaledWidth + _dl, _dl)
×
274
   self:drawRule(x + scaledWidth - _dl / 2, y - hbox.height - _dl / 2, _dl, hbox.height + hbox.depth + _dl)
×
275
   if hbox.depth > SILE.types.length(0) then
×
276
      self:drawRule(x - _dl / 2, y + hbox.depth - _dl / 2, scaledWidth + _dl, _dl)
×
277
   end
278
   self:popColor()
×
279
end
280

281
-- The methods below are only implemented on outputters supporting these features.
282
-- In PDF, it relies on transformation matrices, but other backends may call
283
-- for a different strategy.
284
-- ! The API is unstable and subject to change. !
285

286
function outputter:scaleFn (xorigin, yorigin, xratio, yratio, callback)
50✔
287
   xorigin = SU.cast("number", xorigin)
116✔
288
   yorigin = SU.cast("number", yorigin)
116✔
289
   local x0 = trueXCoord(xorigin)
58✔
290
   local y0 = -trueYCoord(yorigin)
116✔
291
   self:_ensureInit()
58✔
292
   pdf:gsave()
58✔
293
   pdf.setmatrix(1, 0, 0, 1, x0, y0)
58✔
294
   pdf.setmatrix(xratio, 0, 0, yratio, 0, 0)
58✔
295
   pdf.setmatrix(1, 0, 0, 1, -x0, -y0)
58✔
296
   callback()
58✔
297
   pdf:grestore()
58✔
298
end
299

300
function outputter:rotateFn (xorigin, yorigin, theta, callback)
50✔
301
   xorigin = SU.cast("number", xorigin)
6✔
302
   yorigin = SU.cast("number", yorigin)
6✔
303
   local x0 = trueXCoord(xorigin)
3✔
304
   local y0 = -trueYCoord(yorigin)
6✔
305
   self:_ensureInit()
3✔
306
   pdf:gsave()
3✔
307
   pdf.setmatrix(1, 0, 0, 1, x0, y0)
3✔
308
   pdf.setmatrix(math.cos(theta), math.sin(theta), -math.sin(theta), math.cos(theta), 0, 0)
3✔
309
   pdf.setmatrix(1, 0, 0, 1, -x0, -y0)
3✔
310
   callback()
3✔
311
   pdf:grestore()
3✔
312
end
313

314
-- Other rotation unstable APIs
315

316
function outputter:enterFrameRotate (xa, xb, y, theta) -- Unstable API see rotate package
50✔
317
   xa = SU.cast("number", xa)
2✔
318
   xb = SU.cast("number", xb)
2✔
319
   y = SU.cast("number", y)
2✔
320
   -- Keep center point the same?
321
   local cx0 = trueXCoord(xa)
1✔
322
   local cx1 = trueXCoord(xb)
1✔
323
   local cy = -trueYCoord(y)
2✔
324
   self:_ensureInit()
1✔
325
   pdf:gsave()
1✔
326
   pdf.setmatrix(1, 0, 0, 1, cx1, cy)
1✔
327
   pdf.setmatrix(math.cos(theta), math.sin(theta), -math.sin(theta), math.cos(theta), 0, 0)
1✔
328
   pdf.setmatrix(1, 0, 0, 1, -cx0, -cy)
1✔
329
end
330

331
function outputter.leaveFrameRotate (_)
50✔
332
   pdf:grestore()
4✔
333
end
334

335
-- Unstable link APIs
336

337
function outputter:setLinkAnchor (name, x, y)
50✔
338
   x = SU.cast("number", x)
6✔
339
   y = SU.cast("number", y)
6✔
340
   self:_ensureInit()
3✔
341
   pdf.destination(name, trueXCoord(x), trueYCoord(y))
9✔
342
end
343

344
local function borderColor (color)
345
   if color then
1✔
346
      if color.r then
1✔
347
         return "/C [" .. color.r .. " " .. color.g .. " " .. color.b .. "]"
1✔
348
      end
349
      if color.c then
×
350
         return "/C [" .. color.c .. " " .. color.m .. " " .. color.y .. " " .. color.k .. "]"
×
351
      end
352
      if color.l then
×
353
         return "/C [" .. color.l .. "]"
×
354
      end
355
   end
356
   return ""
×
357
end
358
local function borderStyle (style, width)
359
   if style == "underline" then
1✔
360
      return "/BS<</Type/Border/S/U/W " .. width .. ">>"
×
361
   end
362
   if style == "dashed" then
1✔
363
      return "/BS<</Type/Border/S/D/D[3 2]/W " .. width .. ">>"
×
364
   end
365
   return "/Border[0 0 " .. width .. "]"
1✔
366
end
367

368
function outputter:startLink (_, _) -- destination, options as argument
50✔
369
   self:_ensureInit()
×
370
   -- HACK:
371
   -- Looking at the code, pdf.begin_annotation does nothing, and Simon wrote a comment
372
   -- about tracking boxes. Unsure what he implied with this obscure statement.
373
   -- Sure thing is that some backends may need the destination here, e.g. an HTML backend
374
   -- would generate a <a href="#destination">, as well as the options possibly for styling
375
   -- on the link opening?
376
   pdf.begin_annotation()
×
377
end
378

379
function outputter.endLink (_, dest, opts, x0, y0, x1, y1)
50✔
380
   local bordercolor = borderColor(opts.bordercolor)
1✔
381
   local borderwidth = SU.cast("integer", opts.borderwidth)
1✔
382
   local borderstyle = borderStyle(opts.borderstyle, borderwidth)
1✔
383
   local target = opts.external and "/Type/Action/S/URI/URI" or "/S/GoTo/D"
1✔
384
   local d = "<</Type/Annot/Subtype/Link" .. borderstyle .. bordercolor .. "/A<<" .. target .. "(" .. dest .. ")>>>>"
1✔
385
   pdf.end_annotation(
2✔
386
      d,
1✔
387
      trueXCoord(x0),
1✔
388
      trueYCoord(y0 - opts.borderoffset),
1✔
389
      trueXCoord(x1),
1✔
390
      trueYCoord(y1 + opts.borderoffset)
1✔
391
   )
392
end
393

394
-- Bookmarks and metadata
395

396
local function validate_date (date)
397
   return string.match(date, [[^D:%d+%s*-%s*%d%d%s*'%s*%d%d%s*'?$]]) ~= nil
×
398
end
399

400
function outputter:setMetadata (key, value)
50✔
401
   if key == "Trapped" then
×
402
      SU.warn("Skipping special metadata key \\Trapped")
×
403
      return
×
404
   end
405

406
   if key == "ModDate" or key == "CreationDate" then
×
407
      if not validate_date(value) then
×
408
         SU.warn("Invalid date: " .. value)
×
409
         return
×
410
      end
411
   else
412
      -- see comment in on bookmark
413
      value = SU.utf8_to_utf16be(value)
×
414
   end
415
   self:_ensureInit()
×
416
   pdf.metadata(key, value)
×
417
end
418

419
function outputter:setBookmark (dest, title, level)
50✔
420
   -- For annotations and bookmarks, text strings must be encoded using
421
   -- either PDFDocEncoding or UTF16-BE with a leading byte-order marker.
422
   -- As PDFDocEncoding supports only limited character repertoire for
423
   -- European languages, we use UTF-16BE for internationalization.
424
   local ustr = SU.utf8_to_utf16be_hexencoded(title)
3✔
425
   local d = "<</Title<" .. ustr .. ">/A<</S/GoTo/D(" .. dest .. ")>>>>"
3✔
426
   self:_ensureInit()
3✔
427
   pdf.bookmark(d, level)
3✔
428
end
429

430
-- Assumes the caller known what they want to stuff in raw PDF format
431
function outputter:drawRaw (literal)
50✔
432
   self:_ensureInit()
6✔
433
   pdf.add_content(literal)
6✔
434
end
435

436
return outputter
50✔
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