581 lines
19 KiB
Go
581 lines
19 KiB
Go
package webui
|
||
|
||
// A minimal PDF writer — exactly enough of the spec to paginate a table, and no
|
||
// more. Stdlib only (the gowasm engine takes no third-party dependency), which is
|
||
// also why the TSX's pdf-lib could not simply be swapped for a Go port of it.
|
||
//
|
||
// What is implemented:
|
||
//
|
||
// - Document structure: header, indirect objects, a classic cross-reference
|
||
// TABLE (not an xref stream), trailer, startxref, %%EOF.
|
||
// - Pages in portrait or landscape, US Letter (612x792 pt) — the same page size
|
||
// the TSX used (pdf-lib's default), so exports look identical.
|
||
// - Text in the two standard Type1 fonts that need no embedding: Helvetica and
|
||
// Helvetica-Bold, in WinAnsiEncoding.
|
||
// - Text measurement from the Adobe AFM glyph-width tables (below). Without
|
||
// widths you cannot size columns, truncate to fit, or right-align a number.
|
||
// - Filled rectangles and stroked lines (the grid, the zebra band, the rules).
|
||
//
|
||
// What is deliberately NOT implemented: images (so no logo — see
|
||
// autotable_export.go), compression (streams are plain, which makes the output
|
||
// greppable and the tests meaningful), transparency, annotations, outlines,
|
||
// metadata, encryption, and any font that needs embedding.
|
||
//
|
||
// Coordinates are PDF user space: origin bottom-left, y grows UP, units are
|
||
// points (1/72"). A caller lays a table out top-down by starting at
|
||
// height-margin and subtracting.
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// PDFOrientation mirrors the TSX's PDFOrientation union (landscape is 0, and so
|
||
// is Go's zero value — landscape is the default for a table, as it was there).
|
||
type PDFOrientation int
|
||
|
||
const (
|
||
PDF_ORIENTATION_LANDSCAPE PDFOrientation = 0
|
||
PDF_ORIENTATION_PORTRAIT PDFOrientation = 1
|
||
)
|
||
|
||
// US Letter, in points. Portrait is 612x792; landscape swaps them.
|
||
const (
|
||
pdfLetterShort = 612.0
|
||
pdfLetterLong = 792.0
|
||
)
|
||
|
||
// PDFColor is an RGB fill/stroke color, each component in [0,1].
|
||
type PDFColor struct{ R, G, B float64 }
|
||
|
||
// PDFGray is the shade of gray at v (0 = black, 1 = white).
|
||
func PDFGray(v float64) PDFColor { return PDFColor{v, v, v} }
|
||
|
||
// PDFTextStyle is how a run of text is drawn.
|
||
type PDFTextStyle struct {
|
||
Size float64 // in points; 0 means 10
|
||
Bold bool // Helvetica-Bold instead of Helvetica
|
||
Color PDFColor
|
||
}
|
||
|
||
// PDFOptions configures a new document.
|
||
type PDFOptions struct {
|
||
Orientation PDFOrientation
|
||
// Width/Height override the page size in points. Zero means US Letter in the
|
||
// chosen orientation.
|
||
Width, Height float64
|
||
}
|
||
|
||
type pdfPage struct {
|
||
content bytes.Buffer
|
||
}
|
||
|
||
// PDF is a document being written. Draw onto the current page (the one AddPage
|
||
// last created); SetPage rewinds to an earlier one, which is how a footer like
|
||
// "Page 2 of 7" gets stamped onto pages that were finished before the total was
|
||
// known.
|
||
type PDF struct {
|
||
w, h float64
|
||
pages []*pdfPage
|
||
cur int
|
||
}
|
||
|
||
// NewPDF creates an empty document. It has no pages until AddPage is called;
|
||
// Bytes on a page-less document emits one blank page, because a PDF with zero
|
||
// pages is not a valid PDF.
|
||
func NewPDF(o PDFOptions) *PDF {
|
||
w, h := pdfLetterLong, pdfLetterShort // landscape
|
||
if o.Orientation == PDF_ORIENTATION_PORTRAIT {
|
||
w, h = pdfLetterShort, pdfLetterLong
|
||
}
|
||
if o.Width > 0 {
|
||
w = o.Width
|
||
}
|
||
if o.Height > 0 {
|
||
h = o.Height
|
||
}
|
||
return &PDF{w: w, h: h, cur: -1}
|
||
}
|
||
|
||
// Width / Height are the page size in points.
|
||
func (p *PDF) Width() float64 { return p.w }
|
||
func (p *PDF) Height() float64 { return p.h }
|
||
|
||
// PageCount is how many pages exist so far.
|
||
func (p *PDF) PageCount() int { return len(p.pages) }
|
||
|
||
// AddPage appends a blank page and makes it current.
|
||
func (p *PDF) AddPage() {
|
||
p.pages = append(p.pages, &pdfPage{})
|
||
p.cur = len(p.pages) - 1
|
||
}
|
||
|
||
// SetPage makes page i (0-based) current, so a later pass can draw on it. Out of
|
||
// range is a no-op — a footer loop must never take the app down.
|
||
func (p *PDF) SetPage(i int) {
|
||
if i >= 0 && i < len(p.pages) {
|
||
p.cur = i
|
||
}
|
||
}
|
||
|
||
// page returns the current page, creating one if the caller drew before adding.
|
||
func (p *PDF) page() *pdfPage {
|
||
if p.cur < 0 || p.cur >= len(p.pages) {
|
||
p.AddPage()
|
||
}
|
||
return p.pages[p.cur]
|
||
}
|
||
|
||
// Text draws s with its left edge at x and its BASELINE at y.
|
||
func (p *PDF) Text(x, y float64, s string, st PDFTextStyle) {
|
||
if s == "" {
|
||
return
|
||
}
|
||
size := st.Size
|
||
if size <= 0 {
|
||
size = 10
|
||
}
|
||
font := "/F1"
|
||
if st.Bold {
|
||
font = "/F2"
|
||
}
|
||
c := &p.page().content
|
||
fmt.Fprintf(c, "BT\n%s %s Tf\n%s %s %s rg\n1 0 0 1 %s %s Tm\n%s Tj\nET\n",
|
||
font, pdfNum(size),
|
||
pdfNum(st.Color.R), pdfNum(st.Color.G), pdfNum(st.Color.B),
|
||
pdfNum(x), pdfNum(y),
|
||
pdfString(s),
|
||
)
|
||
}
|
||
|
||
// TextRight draws s with its RIGHT edge at x — how a number lands under the right
|
||
// edge of its column, and the only reason the width table has to be correct.
|
||
func (p *PDF) TextRight(x, y float64, s string, st PDFTextStyle) {
|
||
p.Text(x-PDFTextWidth(s, styleSize(st), st.Bold), y, s, st)
|
||
}
|
||
|
||
// TextCenter draws s centered on x.
|
||
func (p *PDF) TextCenter(x, y float64, s string, st PDFTextStyle) {
|
||
p.Text(x-PDFTextWidth(s, styleSize(st), st.Bold)/2, y, s, st)
|
||
}
|
||
|
||
func styleSize(st PDFTextStyle) float64 {
|
||
if st.Size <= 0 {
|
||
return 10
|
||
}
|
||
return st.Size
|
||
}
|
||
|
||
// Line strokes a straight line.
|
||
func (p *PDF) Line(x1, y1, x2, y2, thickness float64, color PDFColor) {
|
||
if thickness <= 0 {
|
||
thickness = 1
|
||
}
|
||
fmt.Fprintf(&p.page().content, "%s %s %s RG\n%s w\n%s %s m\n%s %s l\nS\n",
|
||
pdfNum(color.R), pdfNum(color.G), pdfNum(color.B),
|
||
pdfNum(thickness),
|
||
pdfNum(x1), pdfNum(y1), pdfNum(x2), pdfNum(y2),
|
||
)
|
||
}
|
||
|
||
// Rect fills a rectangle whose lower-left corner is (x,y). There is no stroked
|
||
// variant: a table's borders are drawn as lines, so nothing needs one.
|
||
func (p *PDF) Rect(x, y, w, h float64, color PDFColor) {
|
||
if w <= 0 || h <= 0 {
|
||
return
|
||
}
|
||
fmt.Fprintf(&p.page().content, "%s %s %s rg\n%s %s %s %s re\nf\n",
|
||
pdfNum(color.R), pdfNum(color.G), pdfNum(color.B),
|
||
pdfNum(x), pdfNum(y), pdfNum(w), pdfNum(h),
|
||
)
|
||
}
|
||
|
||
// ---- serialization ----
|
||
|
||
// Object layout is fixed, which is what keeps the xref arithmetic honest:
|
||
//
|
||
// 1 Catalog
|
||
// 2 Pages
|
||
// 3 Helvetica (/F1)
|
||
// 4 Helvetica-Bold (/F2)
|
||
// 5, 6 page 0: the page object, then its content stream
|
||
// 7, 8 page 1: …
|
||
const (
|
||
pdfObjCatalog = 1
|
||
pdfObjPages = 2
|
||
pdfObjFont = 3
|
||
pdfObjFontBold = 4
|
||
)
|
||
|
||
// pageObjNum / contentObjNum are the object numbers for page i.
|
||
func pageObjNum(i int) int { return 5 + 2*i }
|
||
func contentObjNum(i int) int { return 6 + 2*i }
|
||
|
||
// Bytes serializes the document. The result is a complete, standalone PDF file.
|
||
func (p *PDF) Bytes() []byte {
|
||
pages := p.pages
|
||
if len(pages) == 0 {
|
||
pages = []*pdfPage{{}} // a zero-page PDF is invalid; ship one blank page
|
||
}
|
||
nObjs := 4 + 2*len(pages)
|
||
|
||
var buf bytes.Buffer
|
||
buf.WriteString("%PDF-1.4\n")
|
||
// The conventional binary marker: four bytes >127 on a comment line, so a tool
|
||
// that sniffs the first lines classifies the file as binary and does not
|
||
// helpfully mangle its line endings.
|
||
buf.Write([]byte{'%', 0xE2, 0xE3, 0xCF, 0xD3, '\n'})
|
||
|
||
// offsets[n] is the byte offset of object n (index 0 unused).
|
||
offsets := make([]int, nObjs+1)
|
||
|
||
obj := func(n int, body string) {
|
||
offsets[n] = buf.Len()
|
||
fmt.Fprintf(&buf, "%d 0 obj\n%s\nendobj\n", n, body)
|
||
}
|
||
stream := func(n int, data []byte) {
|
||
offsets[n] = buf.Len()
|
||
fmt.Fprintf(&buf, "%d 0 obj\n<< /Length %d >>\nstream\n", n, len(data))
|
||
buf.Write(data)
|
||
buf.WriteString("\nendstream\nendobj\n")
|
||
}
|
||
|
||
obj(pdfObjCatalog, fmt.Sprintf("<< /Type /Catalog /Pages %d 0 R >>", pdfObjPages))
|
||
|
||
var kids strings.Builder
|
||
for i := range pages {
|
||
if i > 0 {
|
||
kids.WriteByte(' ')
|
||
}
|
||
fmt.Fprintf(&kids, "%d 0 R", pageObjNum(i))
|
||
}
|
||
obj(pdfObjPages, fmt.Sprintf("<< /Type /Pages /Kids [%s] /Count %d >>", kids.String(), len(pages)))
|
||
|
||
obj(pdfObjFont, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>")
|
||
obj(pdfObjFontBold, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>")
|
||
|
||
for i, pg := range pages {
|
||
obj(pageObjNum(i), fmt.Sprintf(
|
||
"<< /Type /Page /Parent %d 0 R /MediaBox [0 0 %s %s] "+
|
||
"/Resources << /Font << /F1 %d 0 R /F2 %d 0 R >> >> /Contents %d 0 R >>",
|
||
pdfObjPages, pdfNum(p.w), pdfNum(p.h),
|
||
pdfObjFont, pdfObjFontBold, contentObjNum(i),
|
||
))
|
||
stream(contentObjNum(i), pg.content.Bytes())
|
||
}
|
||
|
||
// The cross-reference table. Every entry is exactly 20 bytes — 10-digit
|
||
// offset, space, 5-digit generation, space, type, and a two-byte EOL — because
|
||
// readers index into it arithmetically rather than parsing it.
|
||
xref := buf.Len()
|
||
fmt.Fprintf(&buf, "xref\n0 %d\n", nObjs+1)
|
||
buf.WriteString("0000000000 65535 f \n")
|
||
for n := 1; n <= nObjs; n++ {
|
||
fmt.Fprintf(&buf, "%010d 00000 n \n", offsets[n])
|
||
}
|
||
fmt.Fprintf(&buf, "trailer\n<< /Size %d /Root %d 0 R >>\nstartxref\n%d\n%%%%EOF\n",
|
||
nObjs+1, pdfObjCatalog, xref)
|
||
|
||
return buf.Bytes()
|
||
}
|
||
|
||
// pdfNum formats a coordinate. PDF has no exponent notation, so strconv's 'g'/-1
|
||
// shortest form is unusable ("1e-07" is a syntax error in a content stream); and
|
||
// three decimals is well past what a 72dpi page can resolve.
|
||
func pdfNum(v float64) string {
|
||
if v == 0 { // also collapses -0
|
||
return "0"
|
||
}
|
||
s := strconv.FormatFloat(v, 'f', 3, 64)
|
||
s = strings.TrimRight(s, "0")
|
||
s = strings.TrimSuffix(s, ".")
|
||
if s == "" || s == "-" {
|
||
return "0"
|
||
}
|
||
return s
|
||
}
|
||
|
||
// pdfString renders s as a PDF literal string, transcoded to WinAnsi. Anything a
|
||
// parser could choke on — the delimiters, the escape character, every byte
|
||
// outside printable ASCII — is escaped, the last as three-digit octal.
|
||
func pdfString(s string) string {
|
||
var b strings.Builder
|
||
b.WriteByte('(')
|
||
for _, r := range s {
|
||
c := winAnsiByte(r)
|
||
switch {
|
||
case c == '(' || c == ')' || c == '\\':
|
||
b.WriteByte('\\')
|
||
b.WriteByte(c)
|
||
case c < 32 || c > 126:
|
||
fmt.Fprintf(&b, "\\%03o", c)
|
||
default:
|
||
b.WriteByte(c)
|
||
}
|
||
}
|
||
b.WriteByte(')')
|
||
return b.String()
|
||
}
|
||
|
||
// ---- encoding ----
|
||
|
||
// winAnsiByte maps a rune to its WinAnsiEncoding code. Latin-1 passes straight
|
||
// through; the printer's punctuation Windows squats in 0x80-0x9F (curly quotes,
|
||
// dashes, the ellipsis the truncator appends) is mapped explicitly. Anything else
|
||
// — CJK, emoji, a tab in the middle of a cell — becomes '?', because the standard
|
||
// 14 fonts have no glyph for it and silently dropping it would misalign the
|
||
// column instead.
|
||
func winAnsiByte(r rune) byte {
|
||
switch {
|
||
case r == '\t' || r == '\n' || r == '\r':
|
||
return ' '
|
||
case r >= 32 && r <= 126:
|
||
return byte(r)
|
||
case r >= 0xA0 && r <= 0xFF:
|
||
return byte(r)
|
||
}
|
||
if c, ok := winAnsiSpecial[r]; ok {
|
||
return c
|
||
}
|
||
return '?'
|
||
}
|
||
|
||
var winAnsiSpecial = map[rune]byte{
|
||
'€': 0x80, // euro
|
||
'‚': 0x82, // single low quote
|
||
'ƒ': 0x83, // florin
|
||
'„': 0x84, // double low quote
|
||
'…': 0x85, // ellipsis <- the truncation marker
|
||
'†': 0x86, // dagger
|
||
'‡': 0x87, // double dagger
|
||
'ˆ': 0x88, // circumflex
|
||
'‰': 0x89, // per mille
|
||
'Š': 0x8A, // S caron
|
||
'‹': 0x8B, // single left guillemet
|
||
'Œ': 0x8C, // OE
|
||
'Ž': 0x8E, // Z caron
|
||
'‘': 0x91, // left single quote
|
||
'’': 0x92, // right single quote (apostrophe)
|
||
'“': 0x93, // left double quote
|
||
'”': 0x94, // right double quote
|
||
'•': 0x95, // bullet
|
||
'–': 0x96, // en dash
|
||
'—': 0x97, // em dash
|
||
'˜': 0x98, // small tilde
|
||
'™': 0x99, // trademark
|
||
'š': 0x9A, // s caron
|
||
'›': 0x9B, // single right guillemet
|
||
'œ': 0x9C, // oe
|
||
'ž': 0x9E, // z caron
|
||
'Ÿ': 0x9F, // Y dieresis
|
||
}
|
||
|
||
// ---- measurement ----
|
||
|
||
// PDFTextWidth is the width of s in points, at the given size, in Helvetica (or
|
||
// Helvetica-Bold). Widths come from the Adobe AFM tables, in 1/1000 em.
|
||
func PDFTextWidth(s string, size float64, bold bool) float64 {
|
||
table := &helveticaWidths
|
||
if bold {
|
||
table = &helveticaBoldWidths
|
||
}
|
||
total := 0.0
|
||
for _, r := range s {
|
||
total += float64(table[winAnsiByte(r)])
|
||
}
|
||
return total * size / 1000
|
||
}
|
||
|
||
// PDFTruncate shortens s until it fits maxWidth, appending an ellipsis — the
|
||
// ellipsis being measured too, so the result really does fit. Returns "" when not
|
||
// even the ellipsis fits, which is the honest answer for a column that narrow.
|
||
func PDFTruncate(s string, maxWidth, size float64, bold bool) string {
|
||
if s == "" || PDFTextWidth(s, size, bold) <= maxWidth {
|
||
return s
|
||
}
|
||
runes := []rune(s)
|
||
for n := len(runes) - 1; n > 0; n-- {
|
||
if PDFTextWidth(string(runes[:n])+"…", size, bold) <= maxWidth {
|
||
return string(runes[:n]) + "…"
|
||
}
|
||
}
|
||
if PDFTextWidth("…", size, bold) <= maxWidth {
|
||
return "…"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// The Helvetica / Helvetica-Bold advance widths, indexed by WinAnsi code, in
|
||
// 1/1000 em — the Adobe Core-14 AFM data, which is what "no font embedding
|
||
// required" actually costs you: the widths have to live somewhere, and this is
|
||
// where.
|
||
//
|
||
// Codes 32-126 are the exact AFM values (note 39 is quotesingle and 96 is grave
|
||
// under WinAnsi, NOT quoteright/quoteleft as under StandardEncoding — a classic
|
||
// off-by-one-glyph). 0x80-0xFF are the AFM values for the Latin-1 and printer's
|
||
// punctuation glyphs; the accented letters carry the advance width of their base
|
||
// letter, which is exactly how the composite glyphs are built. Unmapped codes
|
||
// (0x81, 0x8D, 0x8F, 0x90, 0x9D) fall back to the width of 'n', so a stray byte
|
||
// costs a plausible amount of space rather than zero.
|
||
var (
|
||
helveticaWidths [256]uint16
|
||
helveticaBoldWidths [256]uint16
|
||
)
|
||
|
||
// helveticaAscii/helveticaBoldAscii are codes 32..126, in order.
|
||
var helveticaAscii = [95]uint16{
|
||
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, // 32-41 space ! " # $ % & ' ( )
|
||
389, 584, 278, 333, 278, 278, 556, 556, 556, 556, // 42-51 * + , - . / 0 1 2 3
|
||
556, 556, 556, 556, 556, 556, 278, 278, 584, 584, // 52-61 4 5 6 7 8 9 : ; < =
|
||
584, 556, 1015, 667, 667, 722, 722, 667, 611, 778, // 62-71 > ? @ A B C D E F G
|
||
722, 278, 500, 667, 556, 833, 722, 778, 667, 778, // 72-81 H I J K L M N O P Q
|
||
722, 667, 611, 722, 667, 944, 667, 667, 611, 278, // 82-91 R S T U V W X Y Z [
|
||
278, 278, 469, 556, 333, 556, 556, 500, 556, 556, // 92-101 \ ] ^ _ ` a b c d e
|
||
278, 556, 556, 222, 222, 500, 222, 833, 556, 556, // 102-111 f g h i j k l m n o
|
||
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, // 112-121 p q r s t u v w x y
|
||
500, 334, 260, 334, 584, // 122-126 z { | } ~
|
||
}
|
||
|
||
var helveticaBoldAscii = [95]uint16{
|
||
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, // 32-41
|
||
389, 584, 278, 333, 278, 278, 556, 556, 556, 556, // 42-51
|
||
556, 556, 556, 556, 556, 556, 333, 333, 584, 584, // 52-61
|
||
584, 611, 975, 722, 722, 722, 722, 667, 611, 778, // 62-71
|
||
722, 278, 556, 722, 611, 833, 722, 778, 667, 778, // 72-81
|
||
722, 667, 611, 722, 667, 944, 667, 667, 611, 333, // 82-91
|
||
278, 333, 584, 556, 333, 556, 611, 556, 611, 556, // 92-101
|
||
333, 611, 611, 278, 278, 556, 278, 889, 611, 611, // 102-111
|
||
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, // 112-121
|
||
500, 389, 280, 389, 584, // 122-126
|
||
}
|
||
|
||
// The high half: {code: {regular, bold}}.
|
||
var helveticaHigh = map[byte][2]uint16{
|
||
0x80: {556, 556}, // euro
|
||
0x82: {222, 278}, // quotesinglbase
|
||
0x83: {556, 556}, // florin
|
||
0x84: {333, 500}, // quotedblbase
|
||
0x85: {1000, 1000}, // ellipsis
|
||
0x86: {556, 556}, // dagger
|
||
0x87: {556, 556}, // daggerdbl
|
||
0x88: {333, 333}, // circumflex
|
||
0x89: {1000, 1000}, // perthousand
|
||
0x8A: {667, 667}, // Scaron
|
||
0x8B: {333, 333}, // guilsinglleft
|
||
0x8C: {1000, 1000}, // OE
|
||
0x8E: {611, 611}, // Zcaron
|
||
0x91: {222, 278}, // quoteleft
|
||
0x92: {222, 278}, // quoteright
|
||
0x93: {333, 500}, // quotedblleft
|
||
0x94: {333, 500}, // quotedblright
|
||
0x95: {350, 350}, // bullet
|
||
0x96: {556, 556}, // endash
|
||
0x97: {1000, 1000}, // emdash
|
||
0x98: {333, 333}, // tilde
|
||
0x99: {1000, 1000}, // trademark
|
||
0x9A: {500, 556}, // scaron
|
||
0x9B: {333, 333}, // guilsinglright
|
||
0x9C: {944, 944}, // oe
|
||
0x9E: {500, 500}, // zcaron
|
||
0x9F: {667, 667}, // Ydieresis
|
||
0xA0: {278, 278}, // nbsp
|
||
0xA1: {333, 333}, // exclamdown
|
||
0xA2: {556, 556}, // cent
|
||
0xA3: {556, 556}, // sterling
|
||
0xA4: {556, 556}, // currency
|
||
0xA5: {556, 556}, // yen
|
||
0xA6: {260, 280}, // brokenbar
|
||
0xA7: {556, 556}, // section
|
||
0xA8: {333, 333}, // dieresis
|
||
0xA9: {737, 737}, // copyright
|
||
0xAA: {370, 370}, // ordfeminine
|
||
0xAB: {556, 556}, // guillemotleft
|
||
0xAC: {584, 584}, // logicalnot
|
||
0xAD: {333, 333}, // soft hyphen
|
||
0xAE: {737, 737}, // registered
|
||
0xAF: {333, 333}, // macron
|
||
0xB0: {400, 400}, // degree
|
||
0xB1: {584, 584}, // plusminus
|
||
0xB2: {333, 333}, // twosuperior
|
||
0xB3: {333, 333}, // threesuperior
|
||
0xB4: {333, 333}, // acute
|
||
0xB5: {556, 611}, // mu
|
||
0xB6: {537, 556}, // paragraph
|
||
0xB7: {278, 278}, // periodcentered
|
||
0xB8: {333, 333}, // cedilla
|
||
0xB9: {333, 333}, // onesuperior
|
||
0xBA: {365, 365}, // ordmasculine
|
||
0xBB: {556, 556}, // guillemotright
|
||
0xBC: {834, 834}, // onequarter
|
||
0xBD: {834, 834}, // onehalf
|
||
0xBE: {834, 834}, // threequarters
|
||
0xBF: {611, 611}, // questiondown
|
||
0xC6: {1000, 1000}, // AE
|
||
0xD0: {722, 722}, // Eth
|
||
0xD7: {584, 584}, // multiply
|
||
0xD8: {778, 778}, // Oslash
|
||
0xDD: {667, 667}, // Yacute
|
||
0xDE: {667, 667}, // Thorn
|
||
0xDF: {611, 611}, // germandbls
|
||
0xE6: {889, 889}, // ae
|
||
0xF0: {556, 611}, // eth
|
||
0xF7: {584, 584}, // divide
|
||
0xF8: {611, 611}, // oslash
|
||
0xFD: {500, 556}, // yacute
|
||
0xFE: {556, 611}, // thorn
|
||
0xFF: {500, 556}, // ydieresis
|
||
}
|
||
|
||
// accentBase maps the composite Latin-1 letters to the ASCII letter whose advance
|
||
// width they share (Helvetica builds them by stacking an accent over the base
|
||
// glyph, which does not widen it). The accented i's are the exception: they are
|
||
// built over dotlessi, which is wider than i.
|
||
var accentBase = map[byte]byte{
|
||
0xC0: 'A', 0xC1: 'A', 0xC2: 'A', 0xC3: 'A', 0xC4: 'A', 0xC5: 'A',
|
||
0xC7: 'C',
|
||
0xC8: 'E', 0xC9: 'E', 0xCA: 'E', 0xCB: 'E',
|
||
0xD1: 'N',
|
||
0xD2: 'O', 0xD3: 'O', 0xD4: 'O', 0xD5: 'O', 0xD6: 'O',
|
||
0xD9: 'U', 0xDA: 'U', 0xDB: 'U', 0xDC: 'U',
|
||
0xE0: 'a', 0xE1: 'a', 0xE2: 'a', 0xE3: 'a', 0xE4: 'a', 0xE5: 'a',
|
||
0xE7: 'c',
|
||
0xE8: 'e', 0xE9: 'e', 0xEA: 'e', 0xEB: 'e',
|
||
0xF1: 'n',
|
||
0xF2: 'o', 0xF3: 'o', 0xF4: 'o', 0xF5: 'o', 0xF6: 'o',
|
||
0xF9: 'u', 0xFA: 'u', 0xFB: 'u', 0xFC: 'u',
|
||
}
|
||
|
||
func init() {
|
||
// dotlessi's advance — what the accented i's (0xCC-0xCF, 0xEC-0xEF) are built on.
|
||
const dotlessI, dotlessIBold = 278, 278
|
||
|
||
fill := func(dst *[256]uint16, ascii *[95]uint16, pick int, dotless uint16) {
|
||
fallback := ascii['n'-32]
|
||
for i := range dst {
|
||
dst[i] = fallback
|
||
}
|
||
for i, w := range ascii {
|
||
dst[32+i] = w
|
||
}
|
||
for c, w := range helveticaHigh {
|
||
dst[c] = w[pick]
|
||
}
|
||
for c, base := range accentBase {
|
||
dst[c] = ascii[base-32]
|
||
}
|
||
for _, c := range []byte{0xCC, 0xCD, 0xCE, 0xCF, 0xEC, 0xED, 0xEE, 0xEF} {
|
||
dst[c] = dotless
|
||
}
|
||
// Codes 0-31 are unprintable and never emitted (winAnsiByte folds
|
||
// whitespace to a space), but give them the space width anyway so a
|
||
// measurement can never be wildly off.
|
||
for i := 0; i < 32; i++ {
|
||
dst[i] = ascii[0]
|
||
}
|
||
}
|
||
fill(&helveticaWidths, &helveticaAscii, 0, dotlessI)
|
||
fill(&helveticaBoldWidths, &helveticaBoldAscii, 1, dotlessIBold)
|
||
}
|