// Package pdf writes PDF files with embedded, subsetted TrueType // fonts. // // It knows nothing about documents, paragraphs or CRDTs — it takes // pages, and text placed on them at a position in a face at a size, // and produces bytes. internal/export is what turns a document into // those calls. The split is deliberate: laying text out and writing a // PDF are two jobs, and the one that has to be right about byte // offsets and object numbers should not also be the one deciding where // a line breaks. // // # What it emits, and why that shape // // Text is drawn through a Type0 (composite) font with Identity-H // encoding and a CIDFontType2 descendant. That is more machinery than // a simple /TrueType font with WinAnsiEncoding, and it buys two things // worth the machinery: // // - Any character the face has a glyph for, not the 224 a single-byte // encoding can name. A document with a — or a “ or a café in it is // ordinary, and an exporter that mangles them is not usable. // - Glyph ids addressed directly, which is what internal/fonts' // subsetter produces (it keeps ids where they are), so CIDToGIDMap // is /Identity and there is no mapping table to get wrong. // // The cost is that the text in the content stream is glyph ids, which // no longer resemble the words. A /ToUnicode CMap is what gives them // back, and it is not optional garnish: without one, selecting and // copying text out of the exported PDF yields nonsense, and so does // searching it. §11.3 asks for a PDF, and a PDF whose text cannot be // copied is a picture. package pdf import ( "bytes" "compress/zlib" "fmt" "sort" "strings" "github.com/carlosframework/docs/internal/fonts" ) // Builder accumulates pages and the fonts they use. type Builder struct { Width, Height float64 // points Title string pages []*Page // used records, per face, which glyphs the document draws and what // each one means in Unicode. Both halves are needed at the end: // the ids to subset with, the runes to write the ToUnicode CMap // from. used map[string]*faceUse } type faceUse struct { face fonts.Face font *fonts.Font glyphs map[uint16]bool unicode map[uint16]rune // resource is the name this face is known by inside a page's // resource dictionary ("F0", "F1", ...). resource string } // Page is one page's content stream, built up as PDF operators. type Page struct { b *Builder ops bytes.Buffer } // New starts a document of the given page size, in points (72 to the // inch). func New(width, height float64) *Builder { return &Builder{Width: width, Height: height, used: map[string]*faceUse{}} } // AddPage appends a page and returns it. func (b *Builder) AddPage() *Page { p := &Page{b: b} b.pages = append(b.pages, p) return p } // PageCount is how many pages have been added. func (b *Builder) PageCount() int { return len(b.pages) } func (b *Builder) use(face fonts.Face) (*faceUse, error) { if u, ok := b.used[face.File]; ok { return u, nil } font, err := face.Parsed() if err != nil { return nil, err } u := &faceUse{ face: face, font: font, glyphs: map[uint16]bool{}, unicode: map[uint16]rune{}, resource: fmt.Sprintf("F%d", len(b.used)), } b.used[face.File] = u return u, nil } // Text draws s with its baseline at (x, y), measured in points from // the bottom-left of the page — PDF's own origin, not the top-left the // layout above it thinks in. Converting between the two is // internal/export's job, done once, rather than a subtraction repeated // at every call site here. // // Returns the glyphs it could not find in the face, so a caller can // report a substitution rather than let a document export with silent // gaps in it (§11.2's degrade-loudly rule). // gray is 0 for ordinary black text; a blockquote sets it lighter. func (p *Page) Text(x, y float64, face fonts.Face, size, gray float64, s string) (missing []rune, err error) { if s == "" { return nil, nil } u, err := p.b.use(face) if err != nil { return nil, err } var hex strings.Builder for _, r := range s { gid := u.font.GlyphIndex(r) if gid == 0 { missing = append(missing, r) // Still drawn: .notdef is a visible box in most faces, // which is the loud degradation rather than a silent gap. } u.glyphs[gid] = true // First rune wins for a gid several runes map to (a face may // draw U+00A0 with the space glyph). Copying out the first is // better than copying out whichever happened to be recorded // last. if _, seen := u.unicode[gid]; !seen { u.unicode[gid] = r } fmt.Fprintf(&hex, "%04X", gid) } // q/Q around every run so a grey blockquote cannot leak its fill // colour into whatever is drawn next — the operators are a stack, // and the cheapest correct discipline is to leave it as it was // found. fmt.Fprintf(&p.ops, "q %s g BT /%s %s Tf 1 0 0 1 %s %s Tm <%s> Tj ET Q\n", num(gray), u.resource, num(size), num(x), num(y), hex.String()) return missing, nil } // Rule draws a filled rectangle — the one piece of non-text drawing a // document needs today, for a blockquote's edge. gray is 0 (black) to // 1 (white). func (p *Page) Rule(x, y, w, h, gray float64) { 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)) } // num formats a coordinate. PDF has no exponent notation, so %g would // eventually emit something no reader accepts; and trailing zeros are // trimmed because a content stream is most of the file's bytes. func num(f float64) string { s := fmt.Sprintf("%.3f", f) s = strings.TrimRight(s, "0") s = strings.TrimSuffix(s, ".") if s == "" || s == "-" { return "0" } return s } // --- serialisation --- // object is one indirect object, already serialised except for its // number. type object struct { body []byte } type writer struct { objects []object } // add appends an object and returns its number (1-based, as PDF // numbers them). func (w *writer) add(body string) int { w.objects = append(w.objects, object{body: []byte(body)}) return len(w.objects) } func (w *writer) addStream(dict string, data []byte) int { // Every stream is Flate-compressed. A page of text compresses to // about a third, and an embedded font subset to about half; the // alternative is a file several times larger for no benefit any // reader notices. var buf bytes.Buffer zw := zlib.NewWriter(&buf) // zlib.Writer.Write on a bytes.Buffer cannot fail, and neither can // Close; the errors are checked anyway rather than ignored with a // comment claiming that, because "cannot fail" is a property of // today's implementation and not of the interface. if _, err := zw.Write(data); err != nil { panic("pdf: compressing a stream: " + err.Error()) } if err := zw.Close(); err != nil { panic("pdf: closing a compressed stream: " + err.Error()) } body := fmt.Sprintf("<< %s /Filter /FlateDecode /Length %d >>\nstream\n", dict, buf.Len()) full := append([]byte(body), buf.Bytes()...) full = append(full, []byte("\nendstream")...) w.objects = append(w.objects, object{body: full}) return len(w.objects) } // Bytes serialises the document. func (b *Builder) Bytes() ([]byte, error) { w := &writer{} // Fonts first: a page's resource dictionary has to name them, and // naming an object means knowing its number. fontRefs := map[string]int{} for _, u := range b.sortedUse() { ref, err := b.writeFont(w, u) if err != nil { return nil, err } fontRefs[u.resource] = ref } var resources strings.Builder resources.WriteString("<< /Font << ") for _, u := range b.sortedUse() { fmt.Fprintf(&resources, "/%s %d 0 R ", u.resource, fontRefs[u.resource]) } resources.WriteString(">> >>") // The Pages node has to be numbered before the pages that name it // as /Parent, and it has to list them as /Kids — a chicken and egg // resolved the way PDF itself expects: reserve the number by // adding a placeholder and rewriting its body once the kids exist. pagesRef := w.add("") kids := make([]string, 0, len(b.pages)) for _, p := range b.pages { contentRef := w.addStream("", p.ops.Bytes()) pageRef := w.add(fmt.Sprintf( "<< /Type /Page /Parent %d 0 R /MediaBox [0 0 %s %s] /Resources %s /Contents %d 0 R >>", pagesRef, num(b.Width), num(b.Height), resources.String(), contentRef)) kids = append(kids, fmt.Sprintf("%d 0 R", pageRef)) } w.objects[pagesRef-1].body = []byte(fmt.Sprintf( "<< /Type /Pages /Kids [%s] /Count %d >>", strings.Join(kids, " "), len(b.pages))) catalogRef := w.add(fmt.Sprintf("<< /Type /Catalog /Pages %d 0 R >>", pagesRef)) infoRef := w.add(fmt.Sprintf("<< /Title %s /Producer (CARLOS Docs) >>", utf16String(b.Title))) return w.serialise(catalogRef, infoRef), nil } // sortedUse returns the faces in resource-name order, so a document // built twice from the same content produces the same bytes. Go's map // iteration order is deliberately random, and an export that differs // run to run cannot be tested by comparison. func (b *Builder) sortedUse() []*faceUse { out := make([]*faceUse, 0, len(b.used)) for _, u := range b.used { out = append(out, u) } sort.Slice(out, func(i, j int) bool { return out[i].resource < out[j].resource }) return out } // writeFont emits the four objects one embedded face needs and returns // the number of the Type0 font a page's resources point at. func (b *Builder) writeFont(w *writer, u *faceUse) (int, error) { font := u.font upm := float64(font.UnitsPerEm) // PDF glyph space is 1000 units to the em regardless of what the // font uses. scale := func(v float64) float64 { return v * 1000 / upm } subset, err := font.Subset(u.glyphs) if err != nil { return 0, fmt.Errorf("pdf: subset %s: %w", u.face.File, err) } fileRef := w.addStream(fmt.Sprintf("/Length1 %d", len(subset)), subset) flags := 32 // Nonsymbolic: the characters are in the standard Latin set if u.face.Family == fonts.Serif { flags |= 2 } if u.face.Family == fonts.Mono { flags |= 1 } if u.face.Italic { flags |= 64 } // StemV has no source in a TrueType file and every writer // approximates it. A reader uses it only to synthesise a // substitute face, which cannot happen here because the face is // embedded. stemV := 80 if u.face.Bold { stemV = 140 } descRef := w.add(fmt.Sprintf( "<< /Type /FontDescriptor /FontName /%s /Flags %d /FontBBox [%s %s %s %s] "+ "/ItalicAngle %s /Ascent %s /Descent %s /CapHeight %s /StemV %d /FontFile2 %d 0 R >>", subsetName(u), flags, num(scale(float64(font.BBox[0]))), num(scale(float64(font.BBox[1]))), num(scale(float64(font.BBox[2]))), num(scale(float64(font.BBox[3]))), num(font.ItalicAngle), num(scale(float64(font.Ascender))), num(scale(float64(font.Descender))), num(scale(float64(font.CapHeight))), stemV, fileRef)) // /W: the advance of every glyph actually drawn. Glyphs not listed // take /DW, which is why /DW is set to something sane rather than // left at PDF's own 1000 default. gids := make([]int, 0, len(u.glyphs)) for g := range u.glyphs { gids = append(gids, int(g)) } sort.Ints(gids) var widths strings.Builder for _, g := range gids { fmt.Fprintf(&widths, "%d [%s] ", g, num(scale(float64(font.Advance(uint16(g)))))) } cidRef := w.add(fmt.Sprintf( "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /%s "+ "/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> "+ "/FontDescriptor %d 0 R /DW %s /W [%s] /CIDToGIDMap /Identity >>", subsetName(u), descRef, num(scale(float64(font.Advance(0)))), widths.String())) toUnicodeRef := w.addStream("", []byte(toUnicodeCMap(u))) return w.add(fmt.Sprintf( "<< /Type /Font /Subtype /Type0 /BaseFont /%s /Encoding /Identity-H "+ "/DescendantFonts [%d 0 R] /ToUnicode %d 0 R >>", subsetName(u), cidRef, toUnicodeRef)), nil } // subsetName is the PDF convention for a subsetted font: six uppercase // letters, a plus sign, then the face's own name. The tag is supposed // to distinguish two different subsets of one face inside one file, // which cannot happen here — one Builder subsets each face once — so // it is derived from the resource name rather than randomly generated, // keeping the output byte-identical between runs. func subsetName(u *faceUse) string { tag := []byte("AAAAAA") n := 0 for _, c := range u.resource { n = n*31 + int(c) } for i := 5; i >= 0; i-- { tag[i] = byte('A' + n%26) n /= 26 } return string(tag) + "+" + u.face.PostScriptName() } // toUnicodeCMap writes the CMap that lets a reader turn the glyph ids // in the content stream back into text for selection and search. func toUnicodeCMap(u *faceUse) string { gids := make([]int, 0, len(u.unicode)) for g := range u.unicode { gids = append(gids, int(g)) } sort.Ints(gids) var b strings.Builder b.WriteString("/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n") b.WriteString("/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n") b.WriteString("/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n") b.WriteString("1 begincodespacerange\n<0000> \nendcodespacerange\n") // bfchar sections are capped at 100 entries by the specification. for start := 0; start < len(gids); start += 100 { end := start + 100 if end > len(gids) { end = len(gids) } fmt.Fprintf(&b, "%d beginbfchar\n", end-start) for _, g := range gids[start:end] { fmt.Fprintf(&b, "<%04X> <%s>\n", g, utf16BEHex(u.unicode[uint16(g)])) } b.WriteString("endbfchar\n") } b.WriteString("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n") return b.String() } // utf16BEHex encodes one rune as big-endian UTF-16 hex, surrogate pair // included — an emoji in a document is a perfectly ordinary thing to // want to copy back out. func utf16BEHex(r rune) string { if r > 0xFFFF { r -= 0x10000 hi := 0xD800 + (r >> 10) lo := 0xDC00 + (r & 0x3FF) return fmt.Sprintf("%04X%04X", hi, lo) } return fmt.Sprintf("%04X", r) } // utf16String encodes a string as a PDF text string. The UTF-16BE form // with a byte-order mark is the only one that can carry a title with // anything outside PDFDocEncoding in it, and a document title is // arbitrary text somebody typed. func utf16String(s string) string { var b strings.Builder b.WriteString("") return b.String() } // serialise writes the header, the objects, the cross-reference table // and the trailer. func (w *writer) serialise(catalogRef, infoRef int) []byte { var out bytes.Buffer out.WriteString("%PDF-1.7\n") // A comment line of high bytes, which is how a PDF tells the tools // that transfer it that it is binary and must not be newline- // translated. out.Write([]byte{'%', 0xE2, 0xE3, 0xCF, 0xD3, '\n'}) offsets := make([]int, len(w.objects)+1) for i, o := range w.objects { offsets[i+1] = out.Len() fmt.Fprintf(&out, "%d 0 obj\n", i+1) out.Write(o.body) out.WriteString("\nendobj\n") } xref := out.Len() fmt.Fprintf(&out, "xref\n0 %d\n", len(w.objects)+1) // Object 0 is always the head of the free list. out.WriteString("0000000000 65535 f \n") for i := 1; i <= len(w.objects); i++ { fmt.Fprintf(&out, "%010d 00000 n \n", offsets[i]) } fmt.Fprintf(&out, "trailer\n<< /Size %d /Root %d 0 R /Info %d 0 R >>\nstartxref\n%d\n%%%%EOF\n", len(w.objects)+1, catalogRef, infoRef, xref) return out.Bytes() }