oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1// Package pdf writes PDF files with embedded, subsetted TrueType
2// fonts.
3//
4// It knows nothing about documents, paragraphs or CRDTs — it takes
5// pages, and text placed on them at a position in a face at a size,
6// and produces bytes. internal/export is what turns a document into
7// those calls. The split is deliberate: laying text out and writing a
8// PDF are two jobs, and the one that has to be right about byte
9// offsets and object numbers should not also be the one deciding where
10// a line breaks.
11//
12// # What it emits, and why that shape
13//
14// Text is drawn through a Type0 (composite) font with Identity-H
15// encoding and a CIDFontType2 descendant. That is more machinery than
16// a simple /TrueType font with WinAnsiEncoding, and it buys two things
17// worth the machinery:
18//
19// - Any character the face has a glyph for, not the 224 a single-byte
20// encoding can name. A document with a — or a “ or a café in it is
21// ordinary, and an exporter that mangles them is not usable.
22// - Glyph ids addressed directly, which is what internal/fonts'
23// subsetter produces (it keeps ids where they are), so CIDToGIDMap
24// is /Identity and there is no mapping table to get wrong.
25//
26// The cost is that the text in the content stream is glyph ids, which
27// no longer resemble the words. A /ToUnicode CMap is what gives them
28// back, and it is not optional garnish: without one, selecting and
29// copying text out of the exported PDF yields nonsense, and so does
30// searching it. §11.3 asks for a PDF, and a PDF whose text cannot be
31// copied is a picture.
32package pdf
33
34import (
35 "bytes"
36 "compress/zlib"
37 "fmt"
38 "sort"
39 "strings"
40
41 "github.com/carlosframework/docs/internal/fonts"
42)
43
44// Builder accumulates pages and the fonts they use.
45type Builder struct {
46 Width, Height float64 // points
47 Title string
48
49 pages []*Page
50
51 // used records, per face, which glyphs the document draws and what
52 // each one means in Unicode. Both halves are needed at the end:
53 // the ids to subset with, the runes to write the ToUnicode CMap
54 // from.
55 used map[string]*faceUse
56}
57
58type faceUse struct {
59 face fonts.Face
60 font *fonts.Font
61 glyphs map[uint16]bool
62 unicode map[uint16]rune
63 // resource is the name this face is known by inside a page's
64 // resource dictionary ("F0", "F1", ...).
65 resource string
66}
67
68// Page is one page's content stream, built up as PDF operators.
69type Page struct {
70 b *Builder
71 ops bytes.Buffer
72}
73
74// New starts a document of the given page size, in points (72 to the
75// inch).
76func New(width, height float64) *Builder {
77 return &Builder{Width: width, Height: height, used: map[string]*faceUse{}}
78}
79
80// AddPage appends a page and returns it.
81func (b *Builder) AddPage() *Page {
82 p := &Page{b: b}
83 b.pages = append(b.pages, p)
84 return p
85}
86
87// PageCount is how many pages have been added.
88func (b *Builder) PageCount() int { return len(b.pages) }
89
90func (b *Builder) use(face fonts.Face) (*faceUse, error) {
91 if u, ok := b.used[face.File]; ok {
92 return u, nil
93 }
94 font, err := face.Parsed()
95 if err != nil {
96 return nil, err
97 }
98 u := &faceUse{
99 face: face,
100 font: font,
101 glyphs: map[uint16]bool{},
102 unicode: map[uint16]rune{},
103 resource: fmt.Sprintf("F%d", len(b.used)),
104 }
105 b.used[face.File] = u
106 return u, nil
107}
108
109// Text draws s with its baseline at (x, y), measured in points from
110// the bottom-left of the page — PDF's own origin, not the top-left the
111// layout above it thinks in. Converting between the two is
112// internal/export's job, done once, rather than a subtraction repeated
113// at every call site here.
114//
115// Returns the glyphs it could not find in the face, so a caller can
116// report a substitution rather than let a document export with silent
117// gaps in it (§11.2's degrade-loudly rule).
118// gray is 0 for ordinary black text; a blockquote sets it lighter.
119func (p *Page) Text(x, y float64, face fonts.Face, size, gray float64, s string) (missing []rune, err error) {
120 if s == "" {
121 return nil, nil
122 }
123 u, err := p.b.use(face)
124 if err != nil {
125 return nil, err
126 }
127
128 var hex strings.Builder
129 for _, r := range s {
130 gid := u.font.GlyphIndex(r)
131 if gid == 0 {
132 missing = append(missing, r)
133 // Still drawn: .notdef is a visible box in most faces,
134 // which is the loud degradation rather than a silent gap.
135 }
136 u.glyphs[gid] = true
137 // First rune wins for a gid several runes map to (a face may
138 // draw U+00A0 with the space glyph). Copying out the first is
139 // better than copying out whichever happened to be recorded
140 // last.
141 if _, seen := u.unicode[gid]; !seen {
142 u.unicode[gid] = r
143 }
144 fmt.Fprintf(&hex, "%04X", gid)
145 }
146
147 // q/Q around every run so a grey blockquote cannot leak its fill
148 // colour into whatever is drawn next — the operators are a stack,
149 // and the cheapest correct discipline is to leave it as it was
150 // found.
151 fmt.Fprintf(&p.ops, "q %s g BT /%s %s Tf 1 0 0 1 %s %s Tm <%s> Tj ET Q\n",
152 num(gray), u.resource, num(size), num(x), num(y), hex.String())
153 return missing, nil
154}
155
156// Rule draws a filled rectangle — the one piece of non-text drawing a
157// document needs today, for a blockquote's edge. gray is 0 (black) to
158// 1 (white).
159func (p *Page) Rule(x, y, w, h, gray float64) {
160 fmt.Fprintf(&p.ops, "q %s g %s %s %s %s re f Q\n", num(gray), num(x), num(y), num(w), num(h))
161}
162
163// num formats a coordinate. PDF has no exponent notation, so %g would
164// eventually emit something no reader accepts; and trailing zeros are
165// trimmed because a content stream is most of the file's bytes.
166func num(f float64) string {
167 s := fmt.Sprintf("%.3f", f)
168 s = strings.TrimRight(s, "0")
169 s = strings.TrimSuffix(s, ".")
170 if s == "" || s == "-" {
171 return "0"
172 }
173 return s
174}
175
176// --- serialisation ---
177
178// object is one indirect object, already serialised except for its
179// number.
180type object struct {
181 body []byte
182}
183
184type writer struct {
185 objects []object
186}
187
188// add appends an object and returns its number (1-based, as PDF
189// numbers them).
190func (w *writer) add(body string) int {
191 w.objects = append(w.objects, object{body: []byte(body)})
192 return len(w.objects)
193}
194
195func (w *writer) addStream(dict string, data []byte) int {
196 // Every stream is Flate-compressed. A page of text compresses to
197 // about a third, and an embedded font subset to about half; the
198 // alternative is a file several times larger for no benefit any
199 // reader notices.
200 var buf bytes.Buffer
201 zw := zlib.NewWriter(&buf)
202 // zlib.Writer.Write on a bytes.Buffer cannot fail, and neither can
203 // Close; the errors are checked anyway rather than ignored with a
204 // comment claiming that, because "cannot fail" is a property of
205 // today's implementation and not of the interface.
206 if _, err := zw.Write(data); err != nil {
207 panic("pdf: compressing a stream: " + err.Error())
208 }
209 if err := zw.Close(); err != nil {
210 panic("pdf: closing a compressed stream: " + err.Error())
211 }
212 body := fmt.Sprintf("<< %s /Filter /FlateDecode /Length %d >>\nstream\n", dict, buf.Len())
213 full := append([]byte(body), buf.Bytes()...)
214 full = append(full, []byte("\nendstream")...)
215 w.objects = append(w.objects, object{body: full})
216 return len(w.objects)
217}
218
219// Bytes serialises the document.
220func (b *Builder) Bytes() ([]byte, error) {
221 w := &writer{}
222
223 // Fonts first: a page's resource dictionary has to name them, and
224 // naming an object means knowing its number.
225 fontRefs := map[string]int{}
226 for _, u := range b.sortedUse() {
227 ref, err := b.writeFont(w, u)
228 if err != nil {
229 return nil, err
230 }
231 fontRefs[u.resource] = ref
232 }
233
234 var resources strings.Builder
235 resources.WriteString("<< /Font << ")
236 for _, u := range b.sortedUse() {
237 fmt.Fprintf(&resources, "/%s %d 0 R ", u.resource, fontRefs[u.resource])
238 }
239 resources.WriteString(">> >>")
240
241 // The Pages node has to be numbered before the pages that name it
242 // as /Parent, and it has to list them as /Kids — a chicken and egg
243 // resolved the way PDF itself expects: reserve the number by
244 // adding a placeholder and rewriting its body once the kids exist.
245 pagesRef := w.add("")
246
247 kids := make([]string, 0, len(b.pages))
248 for _, p := range b.pages {
249 contentRef := w.addStream("", p.ops.Bytes())
250 pageRef := w.add(fmt.Sprintf(
251 "<< /Type /Page /Parent %d 0 R /MediaBox [0 0 %s %s] /Resources %s /Contents %d 0 R >>",
252 pagesRef, num(b.Width), num(b.Height), resources.String(), contentRef))
253 kids = append(kids, fmt.Sprintf("%d 0 R", pageRef))
254 }
255 w.objects[pagesRef-1].body = []byte(fmt.Sprintf(
256 "<< /Type /Pages /Kids [%s] /Count %d >>", strings.Join(kids, " "), len(b.pages)))
257
258 catalogRef := w.add(fmt.Sprintf("<< /Type /Catalog /Pages %d 0 R >>", pagesRef))
259
260 infoRef := w.add(fmt.Sprintf("<< /Title %s /Producer (CARLOS Docs) >>", utf16String(b.Title)))
261
262 return w.serialise(catalogRef, infoRef), nil
263}
264
265// sortedUse returns the faces in resource-name order, so a document
266// built twice from the same content produces the same bytes. Go's map
267// iteration order is deliberately random, and an export that differs
268// run to run cannot be tested by comparison.
269func (b *Builder) sortedUse() []*faceUse {
270 out := make([]*faceUse, 0, len(b.used))
271 for _, u := range b.used {
272 out = append(out, u)
273 }
274 sort.Slice(out, func(i, j int) bool { return out[i].resource < out[j].resource })
275 return out
276}
277
278// writeFont emits the four objects one embedded face needs and returns
279// the number of the Type0 font a page's resources point at.
280func (b *Builder) writeFont(w *writer, u *faceUse) (int, error) {
281 font := u.font
282 upm := float64(font.UnitsPerEm)
283 // PDF glyph space is 1000 units to the em regardless of what the
284 // font uses.
285 scale := func(v float64) float64 { return v * 1000 / upm }
286
287 subset, err := font.Subset(u.glyphs)
288 if err != nil {
289 return 0, fmt.Errorf("pdf: subset %s: %w", u.face.File, err)
290 }
291
292 fileRef := w.addStream(fmt.Sprintf("/Length1 %d", len(subset)), subset)
293
294 flags := 32 // Nonsymbolic: the characters are in the standard Latin set
295 if u.face.Family == fonts.Serif {
296 flags |= 2
297 }
298 if u.face.Family == fonts.Mono {
299 flags |= 1
300 }
301 if u.face.Italic {
302 flags |= 64
303 }
304 // StemV has no source in a TrueType file and every writer
305 // approximates it. A reader uses it only to synthesise a
306 // substitute face, which cannot happen here because the face is
307 // embedded.
308 stemV := 80
309 if u.face.Bold {
310 stemV = 140
311 }
312
313 descRef := w.add(fmt.Sprintf(
314 "<< /Type /FontDescriptor /FontName /%s /Flags %d /FontBBox [%s %s %s %s] "+
315 "/ItalicAngle %s /Ascent %s /Descent %s /CapHeight %s /StemV %d /FontFile2 %d 0 R >>",
316 subsetName(u), flags,
317 num(scale(float64(font.BBox[0]))), num(scale(float64(font.BBox[1]))),
318 num(scale(float64(font.BBox[2]))), num(scale(float64(font.BBox[3]))),
319 num(font.ItalicAngle),
320 num(scale(float64(font.Ascender))), num(scale(float64(font.Descender))),
321 num(scale(float64(font.CapHeight))), stemV, fileRef))
322
323 // /W: the advance of every glyph actually drawn. Glyphs not listed
324 // take /DW, which is why /DW is set to something sane rather than
325 // left at PDF's own 1000 default.
326 gids := make([]int, 0, len(u.glyphs))
327 for g := range u.glyphs {
328 gids = append(gids, int(g))
329 }
330 sort.Ints(gids)
331 var widths strings.Builder
332 for _, g := range gids {
333 fmt.Fprintf(&widths, "%d [%s] ", g, num(scale(float64(font.Advance(uint16(g))))))
334 }
335
336 cidRef := w.add(fmt.Sprintf(
337 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /%s "+
338 "/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> "+
339 "/FontDescriptor %d 0 R /DW %s /W [%s] /CIDToGIDMap /Identity >>",
340 subsetName(u), descRef, num(scale(float64(font.Advance(0)))), widths.String()))
341
342 toUnicodeRef := w.addStream("", []byte(toUnicodeCMap(u)))
343
344 return w.add(fmt.Sprintf(
345 "<< /Type /Font /Subtype /Type0 /BaseFont /%s /Encoding /Identity-H "+
346 "/DescendantFonts [%d 0 R] /ToUnicode %d 0 R >>",
347 subsetName(u), cidRef, toUnicodeRef)), nil
348}
349
350// subsetName is the PDF convention for a subsetted font: six uppercase
351// letters, a plus sign, then the face's own name. The tag is supposed
352// to distinguish two different subsets of one face inside one file,
353// which cannot happen here — one Builder subsets each face once — so
354// it is derived from the resource name rather than randomly generated,
355// keeping the output byte-identical between runs.
356func subsetName(u *faceUse) string {
357 tag := []byte("AAAAAA")
358 n := 0
359 for _, c := range u.resource {
360 n = n*31 + int(c)
361 }
362 for i := 5; i >= 0; i-- {
363 tag[i] = byte('A' + n%26)
364 n /= 26
365 }
366 return string(tag) + "+" + u.face.PostScriptName()
367}
368
369// toUnicodeCMap writes the CMap that lets a reader turn the glyph ids
370// in the content stream back into text for selection and search.
371func toUnicodeCMap(u *faceUse) string {
372 gids := make([]int, 0, len(u.unicode))
373 for g := range u.unicode {
374 gids = append(gids, int(g))
375 }
376 sort.Ints(gids)
377
378 var b strings.Builder
379 b.WriteString("/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n")
380 b.WriteString("/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n")
381 b.WriteString("/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n")
382 b.WriteString("1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n")
383
384 // bfchar sections are capped at 100 entries by the specification.
385 for start := 0; start < len(gids); start += 100 {
386 end := start + 100
387 if end > len(gids) {
388 end = len(gids)
389 }
390 fmt.Fprintf(&b, "%d beginbfchar\n", end-start)
391 for _, g := range gids[start:end] {
392 fmt.Fprintf(&b, "<%04X> <%s>\n", g, utf16BEHex(u.unicode[uint16(g)]))
393 }
394 b.WriteString("endbfchar\n")
395 }
396 b.WriteString("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n")
397 return b.String()
398}
399
400// utf16BEHex encodes one rune as big-endian UTF-16 hex, surrogate pair
401// included — an emoji in a document is a perfectly ordinary thing to
402// want to copy back out.
403func utf16BEHex(r rune) string {
404 if r > 0xFFFF {
405 r -= 0x10000
406 hi := 0xD800 + (r >> 10)
407 lo := 0xDC00 + (r & 0x3FF)
408 return fmt.Sprintf("%04X%04X", hi, lo)
409 }
410 return fmt.Sprintf("%04X", r)
411}
412
413// utf16String encodes a string as a PDF text string. The UTF-16BE form
414// with a byte-order mark is the only one that can carry a title with
415// anything outside PDFDocEncoding in it, and a document title is
416// arbitrary text somebody typed.
417func utf16String(s string) string {
418 var b strings.Builder
419 b.WriteString("<FEFF")
420 for _, r := range s {
421 b.WriteString(utf16BEHex(r))
422 }
423 b.WriteString(">")
424 return b.String()
425}
426
427// serialise writes the header, the objects, the cross-reference table
428// and the trailer.
429func (w *writer) serialise(catalogRef, infoRef int) []byte {
430 var out bytes.Buffer
431 out.WriteString("%PDF-1.7\n")
432 // A comment line of high bytes, which is how a PDF tells the tools
433 // that transfer it that it is binary and must not be newline-
434 // translated.
435 out.Write([]byte{'%', 0xE2, 0xE3, 0xCF, 0xD3, '\n'})
436
437 offsets := make([]int, len(w.objects)+1)
438 for i, o := range w.objects {
439 offsets[i+1] = out.Len()
440 fmt.Fprintf(&out, "%d 0 obj\n", i+1)
441 out.Write(o.body)
442 out.WriteString("\nendobj\n")
443 }
444
445 xref := out.Len()
446 fmt.Fprintf(&out, "xref\n0 %d\n", len(w.objects)+1)
447 // Object 0 is always the head of the free list.
448 out.WriteString("0000000000 65535 f \n")
449 for i := 1; i <= len(w.objects); i++ {
450 fmt.Fprintf(&out, "%010d 00000 n \n", offsets[i])
451 }
452 fmt.Fprintf(&out, "trailer\n<< /Size %d /Root %d 0 R /Info %d 0 R >>\nstartxref\n%d\n%%%%EOF\n",
453 len(w.objects)+1, catalogRef, infoRef, xref)
454 return out.Bytes()
455}
456