P5a plan: PDF documents — seven Ornith tasks with acceptance tests first

docs/plans/p5a: 01 kind+fetch limits, 02 pdftext child + runner, 03 dcache PDF tables,
04 pdfpool, 05 pdfdoc render (map/pages/grep), 06 api, 07 digest citations, metrics, CLI,
skill, unit, README. Given tests compile against a panic-only skeleton and fail there for the
intended reasons; PDFs for tests are built by internal/pdftest (given), plus two qpdf-encrypted
fixtures.

Owner changes for the run: vendor go-pdfkit/extract v0.3.0 + reader v0.6.0 (pinned via
tools.go until task 02); AGENTS.md, tools/run-plan.sh (enforces a Co-Authored-By trailer for
the model), docs/implementer-log.md, `make gate`; gofmt (whitespace) on clean.go and guard;
TestUnsupportedType uses a zip now that PDFs are supported; requestDeadline 90→210 s and
writeTimeout 120→240 s for the PDF path's worst case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 20:47:38 -07:00
co-authored by Claude Opus 5.5
parent 3f2b78d4aa
commit cfde35397a
115 changed files with 26422 additions and 43 deletions
+2
View File
@@ -0,0 +1,2 @@
coverage.out
*.out
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2026 the go-opentype/opentype authors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
+204
View File
@@ -0,0 +1,204 @@
# go-opentype/opentype
[![CI](https://github.com/go-opentype/opentype/actions/workflows/ci.yml/badge.svg)](https://github.com/go-opentype/opentype/actions/workflows/ci.yml)
[![pkg.go.dev](https://img.shields.io/badge/pkg.go.dev-opentype-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/go-opentype/opentype)
![coverage](https://img.shields.io/badge/coverage-100%25-1a7f37)
![go](https://img.shields.io/badge/Go-1.26.4%2B-00ADD8?logo=go&logoColor=white)
[![license](https://img.shields.io/badge/license-BSD--3--Clause-blue)](./LICENSE)
Pure-Go, **CGO=0, standard-library-only** parser, shaper and anti-aliased
rasteriser for TrueType/OpenType fonts. No `golang.org/x/*`, no third-party
modules — it imports only `image`, `encoding/binary`, `math`, `errors` and
friends, so it builds for every Go target including `GOOS=js GOARCH=wasm`.
It exists to functionally replace the narrow slice of
`golang.org/x/image/font/opentype` that a glyph-blitting UI needs — parse a
font blob, build a face at a pixel size, pull per-rune advances and 8-bit
alpha coverage masks — and then goes further: CFF/CFF2 outlines, variable
fonts, GPOS/GSUB shaping, kerning and TrueType/CFF hinting are all in scope.
## Install
```sh
go get github.com/go-opentype/opentype
```
## Quick start
```go
package main
import (
"fmt"
"os"
"github.com/go-opentype/opentype"
)
func main() {
ttf, err := os.ReadFile("MyFont.ttf") // or .otf (CFF/CFF2)
if err != nil {
panic(err)
}
f, err := opentype.Parse(ttf)
if err != nil {
panic(err)
}
face := f.NewFace(16) // 16px per em
m := face.Metrics() // Metrics{Ascent, Descent, Height} in pixels
w := face.Measure("Hello") // total advance width in pixels
adv := face.Advance('H') // one rune's advance in pixels
fmt.Println(m, w, adv)
// Rasterise a glyph with (penX, baselineY) as the pen origin on the baseline.
bounds, mask, maskp, advance, ok := face.GlyphMask('H', 0, 16)
if ok && mask != nil {
// Composite the coverage mask onto your destination:
// pixel (maskp.X+i, maskp.Y+j) covers destination (bounds.Min.X+i, bounds.Min.Y+j).
_ = bounds
_ = maskp
_ = advance
}
}
```
The `GlyphMask` shape deliberately mirrors `x/image`'s `font.Face.Glyph`, so
swapping this in for the x/image face is mechanical.
## Features
- **sfnt container** parsing (`0x00010000` TrueType and `OTTO` CFF magics)
- **TrueType `glyf` outlines** — simple and composite glyphs, implied
on-curve midpoint synthesis, cycle/depth-guarded composites
- **CFF and CFF2 outlines** — Type 2 charstrings, subroutines, `seac`
accent composition, CFF2 variable-font blend operators
- **Variable fonts** — `fvar` axes and named instances, `avar` axis-value
mapping, `gvar`/CFF2 glyph-outline interpolation, and `HVAR`/`VVAR`/`MVAR`
metric variation, via `(*Face).SetVariation`
- **GSUB/GPOS shaping** — ligatures, contextual and positional-form
substitution (`isol`/`init`/`medi`/`fina`), pair/single/cursive
positioning, mark-to-base and mark-to-mark attachment
- **Kerning** — GPOS pair positioning with legacy `kern`-table fallback
through a single `Kerner`
- **Hinting** — TrueType instruction interpreter and a CFF/Type 2 stem
grid-fitter, toggled per `Face` with `SetHinting`; optional
`SetStemDarkening`
- **Vertical metrics** — `vhea`/`vmtx`/`VORG`-aware vertical advances and
origins for vertical writing modes
- **OpenType `MATH` table** — math-typesetting metrics (constants, per-glyph
italic correction / top-accent attachment / four-corner kerning, and
stretchy-glyph size variants and assemblies), exposed pixel-scaled through
a `Face`
- **Font descriptors** — the OS/2- and `head`/`post`-derived scalars a PDF
`/FontDescriptor` or CSS `@font-face` needs (`Ascent`, `Descent`,
`FontBBox`, `CapHeight`, `XHeight`, `ItalicAngle`, `WeightClass`,
`WidthClass`, `StemV`, `Flags`, `IsFixedPitch`/`IsItalic`/`IsSerif`)
- **Subsetting & instancing** — `SubsetTrueType`/`SubsetCFF` cut a font down
to a glyph set (closing composite/`seac` dependencies and remapping ids),
and `Instance`/`InstanceBytes` bake a variable font to a static instance —
the writer side needed to embed fonts in PDFs
- **Anti-aliased rasterisation** via 4×4 supersampling under the non-zero
winding rule
A `Font` is immutable after `Parse` and safe for concurrent use. A `Face`
caches rasterised glyphs and is **not** safe for concurrent use; build one
`Face` per goroutine if needed.
## API tour
| Symbol | Purpose |
| --- | --- |
| `Parse(b []byte) (*Font, error)` | Decode an sfnt (TrueType or CFF/OTTO) font blob. |
| `(*Font).NumGlyphs() int` | Glyph count (from `maxp`). |
| `(*Font).GlyphIndex(r rune) (GlyphIndex, bool)` | Map a rune via the cmap. |
| `(*Font).GlyphIndexVariation(r, vs rune) (GlyphIndex, bool)` | Map a rune + Unicode variation selector. |
| `(*Font).Axes() []Axis` | Variable-font design axes (`fvar`). |
| `(*Font).NamedInstances() []NamedInstance` | Named positions in the variation space. |
| `(*Font).GPOS() *GPOS` / `(*Font).GSUB() *GSUB` | Parsed layout tables, or `nil`. |
| `(*Font).HasVerticalMetrics() bool` | Whether `vhea`/`vmtx` are present. |
| `(*Font).NewFace(sizePx int) *Face` | Build a sized face (scale = `sizePx / unitsPerEm`). |
| `(*Face).Metrics() Metrics` | `Ascent`, `Descent`, `Height` in pixels. |
| `(*Face).Advance(r rune) int` / `(*Face).Measure(s string) int` | Rune / string advance in pixels. |
| `(*Face).Kern(prev, r rune) int` / `(*Face).MeasureKerned(s string) int` | GPOS-or-`kern` pair adjustment. |
| `(*Face).GlyphMask(r, x, y) (image.Rectangle, *image.Alpha, image.Point, int, bool)` | Rasterised glyph + advance. |
| `(*Face).Shape(text string, features ...string) []GlyphIndex` | GSUB-substituted glyph run. |
| `(*Face).ShapePositioned(text string, features ...string) []PositionedGlyph` | GSUB + GPOS glyph run with pen offsets. |
| `(*Face).SetVariation(coords map[string]float64)` | Instance a variable font at the given axis coordinates. |
| `(*Face).SetHinting(on bool)` / `(*Face).SetStemDarkening(on bool)` | Toggle the TrueType/CFF hinter. |
| `(*Face).VerticalAdvance(r rune) int` / `(*Face).VerticalOrigin(r rune) (int, bool)` | Vertical writing-mode metrics. |
| `(*Font).HasMath() bool` | Whether the font carries an OpenType `MATH` table. |
| `(*Face).MathConstant(w MathConstant) int` / `(*Face).ItalicCorrection(g GlyphIndex) int` / `(*Face).MathVariants(g, vertical) ([]MathVariant, *MathAssembly)` | Pixel-scaled `MATH`-table metrics for a math-layout engine. |
| `(*Font).Ascent/Descent/LineGap/FontBBox/CapHeight/XHeight/ItalicAngle/WeightClass/WidthClass/StemV/Flags/IsFixedPitch/IsItalic/IsSerif` | Font-descriptor scalars for PDF `/FontDescriptor` and CSS `@font-face`. |
| `(*Font).Instance(coords map[string]float64) (*Font, error)` / `(*Font).InstanceBytes(coords) ([]byte, error)` | Bake a variable font to a static instance (as a `*Font` or a new sfnt blob). |
| `(*Font).SubsetTrueType(gids []GlyphIndex) ([]byte, map[GlyphIndex]GlyphIndex, error)` | Cut a TrueType font to a glyph set, closing composites and returning the old→new id map. |
| `(*Font).SubsetCFF(gids []GlyphIndex) ([]byte, error)` | Cut a CFF/OTTO font to a glyph set (closing `seac` accents). |
See [`example_test.go`](./example_test.go) for runnable examples of each of
these, and `go doc github.com/go-opentype/opentype` for the full reference.
## Support matrix
- sfnt container (`0x00010000` and `true` TrueType magics, `OTTO` CFF magic)
- `head`, `maxp`, `hhea`, `hmtx` (including the trailing-run shared advance)
- `cmap` formats **0, 2, 4, 6, 8, 10, 12, 13** and **14** (Unicode variation
sequences), preferring the richest available (12/13 over 4/6)
- `loca` (short and long), `glyf` simple and composite TrueType glyphs
- CFF and CFF2 (`OTTO`) Type 2 charstring outlines, including `seac`
- `fvar`/`avar`/`gvar`/`HVAR`/`VVAR`/`MVAR` and CFF2 blends for variable
fonts, plus `Instance`/`InstanceBytes` to bake a static instance
- `GSUB`/`GPOS`/`GDEF`, the legacy `kern` table
- OS/2 + `head`/`post` **font-descriptor** scalars (`Ascent`, `Descent`,
`FontBBox`, `CapHeight`, `XHeight`, `ItalicAngle`, `WeightClass`,
`WidthClass`, `StemV`, `Flags`, …) for PDF/CSS embedding
- **subsetting** — `SubsetTrueType` (glyf, composite-closure aware, with an
old→new id map) and `SubsetCFF` (`seac`-closure aware)
- the OpenType **`MATH`** table — math-typesetting metrics (constants, per-glyph
italic correction / top-accent attachment / corner kerning, and stretchy-glyph
size variants and assemblies), exposed pixel-scaled through a `Face`
(`HasMath`, `MathConstant`, `ItalicCorrection`, `TopAccentAttachment`,
`MathKern`, `MathVariants`); math *layout* is left to a higher-level engine
- TrueType instruction hinting and a CFF stem grid-fitter/darkener
- `vhea`/`vmtx`/`VORG` vertical metrics
- anti-aliased rasterisation via 4×4 supersampling under the non-zero
winding rule
Rasterisation uses uniform supersampling (not the delta-hinted rasterisation
of a native TrueType/CFF hinter's drop-out control), so it favours
correctness and portability over the very last drop of small-size sharpness.
## Testing
Tests never depend on an external font to reach 100% coverage: they
synthesise minimal-but-valid TrueType and CFF fonts in memory to
deterministically exercise every parse, shape and raster branch, including
the error paths. A real-world font, Adobe's Source Serif 4 (SIL Open Font
License, bundled under [`testdata/`](./testdata)), is used in
[`example_test.go`](./example_test.go) and in a handful of sanity-check
tests, so the documented examples double as an end-to-end smoke test against
production CFF charstrings and Private DICTs. CI enforces **100.0% statement
coverage**, `go vet`, and a cross-compile smoke over
`linux/{amd64,arm64,riscv64,loong64,ppc64le,s390x}`, `js/wasm`,
`darwin/arm64` and `windows/amd64`.
## Part of the go-opentype pure-Go text stack
`go-opentype/opentype` is the parsing/shaping/rasterising engine at the base
of a dependency-free text stack:
- **[opentype](https://github.com/go-opentype/opentype)** (this repo) — font
parsing, GSUB/GPOS shaping, hinting and rasterisation.
- **[bidi](https://github.com/go-opentype/bidi)** — a full Unicode
Bidirectional Algorithm (UBA) implementation, for ordering mixed
left-to-right/right-to-left text before it is shaped.
- **[shape](https://github.com/go-opentype/shape)** — a HarfBuzz-lite
complex-script shaper (Arabic, Indic, Hangul, USE, Egyptian
hieroglyphs, ...) built on this package's GSUB/GPOS engine.
- **[fonts](https://github.com/go-opentype/fonts)** — 46 bundled OFL/BSD
font families (Latin, non-Latin scripts and CJK), per-family lazily
`go:embed`-ed, ready to feed to `Parse`.
## License
BSD-3-Clause. See [LICENSE](./LICENSE).
+43
View File
@@ -0,0 +1,43 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
// This file adds by-glyph-index advance accessors that honour the face's current
// variation instance. Face.Advance and Face.VerticalAdvance take a rune and go
// through the cmap; a text layer that has already resolved glyph indices (a
// shaper, or a PDF writer emitting a /W width array) needs the advance of a glyph
// it names directly, at the same instance the outlines are rendered for. Without
// these it would have to rasterise the glyph or reach past the cmap by hand.
// AdvanceIndex returns the horizontal advance of glyph gid in whole pixels at the
// face's size, honouring the face's current variation (the HVAR delta, or the
// gvar phantom-point fallback, is folded in exactly as Face.Advance does for a
// rune). With no variation set it is the base advance scaled to pixels. It is the
// by-glyph-index counterpart of Face.Advance, for a caller that already holds a
// glyph id rather than a rune. An out-of-range gid advances by zero.
func (fc *Face) AdvanceIndex(gid GlyphIndex) int {
return roundInt(fc.hAdvanceUnits(gid) * fc.scale)
}
// AdvanceIndexUnits returns the horizontal advance of glyph gid in font units at
// the face's current variation, as a float64 (the value AdvanceIndex rounds and
// scales to pixels). It is the exact width a PDF /W array wants — font-unit widths
// that track the instanced font — without the caller re-deriving the HVAR delta.
// An out-of-range gid advances by zero.
func (fc *Face) AdvanceIndexUnits(gid GlyphIndex) float64 {
return fc.hAdvanceUnits(gid)
}
// VerticalAdvanceIndex returns the vertical advance (advance height) of glyph gid
// in whole pixels at the face's size, honouring the face's current variation (the
// VVAR delta is folded in). When the font has no vmtx table the advance is one em.
// It is the by-glyph-index counterpart of Face.VerticalAdvance.
func (fc *Face) VerticalAdvanceIndex(gid GlyphIndex) int {
adv := float64(fc.font.verticalAdvance(gid))
if fc.varCoords != nil {
adv += fc.font.verticalAdvanceDelta(int(gid), fc.varNorm)
}
return roundInt(adv * fc.scale)
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import "fmt"
// This file implements the optional 'avar' (Axis Variations) table. It refines
// the linear normalization performed by NormalizeCoords: for each axis it maps
// the normalized coordinate through a piecewise-linear segment map, letting a
// font make, say, the visual midpoint of a weight axis land somewhere other
// than the arithmetic midpoint. A font without 'avar' normalizes linearly.
// avarMap is one (from, to) control point of an axis segment map, both in the
// normalized F2Dot14 domain decoded to float64.
type avarMap struct {
from, to float64
}
// avarTable holds one segment map per axis, in axis order.
type avarTable struct {
segments [][]avarMap
}
// parseAvar decodes the 'avar' table: a header followed by one SegmentMaps
// record per axis, each a count and that many (fromCoordinate, toCoordinate)
// F2Dot14 pairs.
func (f *Font) parseAvar(b []byte) error {
r := reader{b: b}
r.u16() // majorVersion
r.u16() // minorVersion
r.u16() // reserved
axisCount := int(r.u16())
segments := make([][]avarMap, axisCount)
for i := range segments {
count := int(r.u16())
maps := make([]avarMap, count)
for j := range maps {
maps[j].from = r.f2dot14()
maps[j].to = r.f2dot14()
}
segments[i] = maps
}
if r.err != nil {
return fmt.Errorf("opentype: avar table: %w", r.err)
}
f.avar = &avarTable{segments: segments}
return nil
}
// apply maps a normalized coordinate v for the given axis through its segment
// map. Control points are assumed sorted by ascending fromCoordinate (as the
// specification requires). An axis with fewer than two control points, or an
// index past the table, is treated as identity.
func (a *avarTable) apply(axis int, v float64) float64 {
if axis >= len(a.segments) {
return v
}
seg := a.segments[axis]
if len(seg) < 2 {
return v
}
if v <= seg[0].from {
return seg[0].to
}
for i := 1; i < len(seg); i++ {
if v < seg[i].from {
lo, hi := seg[i-1], seg[i]
return lo.to + (hi.to-lo.to)*(v-lo.from)/(hi.from-lo.from)
}
if v == seg[i].from {
return seg[i].to
}
}
return seg[len(seg)-1].to
}
+202
View File
@@ -0,0 +1,202 @@
// Copyright (c) 2026, the go-opentype authors
// All rights reserved.
//
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import (
"fmt"
"math"
)
// ParseCFF decodes a bare CFF font program — a Compact Font Format font that
// is not wrapped in an sfnt container, which is how a PDF carries one as a
// FontFile3 of subtype Type1C. Such a program has no head, no hmtx and no
// character map: the size of its units comes from its font matrix, its
// advances from the charstrings themselves, and the way from a name or a byte
// to a glyph from its own charset and encoding. [Font.HasCharacterMap] is
// false for the result, and [Font.GlyphIndexByName] and
// [Font.GlyphIndexByCode] are how it is addressed.
//
// The byte slice is retained, not copied, and must not be mutated afterwards.
func ParseCFF(b []byte) (*Font, error) {
cff, err := parseCFF(b)
if err != nil {
return nil, err
}
n := len(cff.charStrings)
if n == 0 {
return nil, fmt.Errorf("opentype: cff: the font has no glyphs")
}
f := &Font{
cff: cff,
numGlyphs: n,
unitsPerEm: unitsPerEmOf(cff.fontMatrix),
numberOfHMetrics: n,
}
f.advances, f.lsbs = cff.metrics(n)
// A bare program says nothing about how the line is spaced; a page of one
// is laid out by whatever carries it, which for a PDF is the text matrix.
f.ascender, f.descender = f.unitsPerEm, 0
f.buildNames()
return f, nil
}
// unitsPerEmOf turns a font matrix into the number of units to the em, which
// is what the rest of this package counts in. A matrix of a thousandth is the
// usual one and gives a thousand.
func unitsPerEmOf(m [6]float64) int {
upem := int(math.Round(1 / m[0]))
if upem < 16 || upem > 16384 {
return 1000
}
return upem
}
// metrics is the advance and left side bearing of every glyph, read from the
// charstrings. A charstring that does not say gets the font's default.
func (c *cffTable) metrics(n int) (advances, lsbs []int) {
advances = make([]int, n)
lsbs = make([]int, n)
def, nominal := 0.0, 0.0
if c.priv != nil {
def, nominal = c.priv.defaultWidthX, c.priv.nominalWidthX
}
for gid := 0; gid < n; gid++ {
w := def
if delta, ok := charstringWidth(c.charStrings[gid]); ok {
w = nominal + delta
}
advances[gid] = int(math.Round(w))
}
return advances, lsbs
}
// charstringWidth reads the optional width a Type 2 charstring may carry in
// front of its first stem, move or endchar operator. It reads operands only,
// and gives up at the first operator that could hide one behind a subroutine
// rather than guess.
func charstringWidth(cs []byte) (float64, bool) {
var nOperands int
var first float64
for i := 0; i < len(cs); {
b := cs[i]
switch {
case b >= 32 || b == 28:
v, next, ok := t2Operand(cs, i)
if !ok {
return 0, false
}
if nOperands == 0 {
first = v
}
nOperands++
i = next
case b == 1 || b == 3 || b == 18 || b == 23: // hstem, vstem, hstemhm, vstemhm
return first, nOperands%2 == 1
case b == 19 || b == 20: // hintmask, cntrmask
return first, nOperands%2 == 1
case b == 21: // rmoveto
return first, nOperands > 2
case b == 22 || b == 4: // hmoveto, vmoveto
return first, nOperands > 1
case b == 14: // endchar
return first, nOperands == 1 || nOperands == 5
default:
// A subroutine call, or an arithmetic operator: the width, if
// there is one, is no longer in front of us.
return 0, false
}
}
return 0, false
}
// t2Operand reads one Type 2 charstring operand, giving its value and where
// the next byte is.
func t2Operand(cs []byte, i int) (float64, int, bool) {
b := cs[i]
switch {
case b == 28:
if i+2 >= len(cs) {
return 0, 0, false
}
return float64(int16(uint16(cs[i+1])<<8 | uint16(cs[i+2]))), i + 3, true
case b < 247:
return float64(int(b) - 139), i + 1, true
case b < 251:
if i+1 >= len(cs) {
return 0, 0, false
}
return float64((int(b)-247)*256 + int(cs[i+1]) + 108), i + 2, true
case b < 255:
if i+1 >= len(cs) {
return 0, 0, false
}
return float64(-(int(b)-251)*256 - int(cs[i+1]) - 108), i + 2, true
default: // 255: a sixteen-sixteen fixed-point number
if i+4 >= len(cs) {
return 0, 0, false
}
v := int32(uint32(cs[i+1])<<24 | uint32(cs[i+2])<<16 | uint32(cs[i+3])<<8 | uint32(cs[i+4]))
return float64(v) / 65536, i + 5, true
}
}
// buildNames indexes the font by the names it gives its glyphs, so that a
// document addressing it by name has somewhere to look. A font addressed by
// identifier names nothing, and this leaves it alone.
func (f *Font) buildNames() {
if f.cff == nil || f.cff.isCID {
return
}
f.glyphNames = make(map[string]GlyphIndex, f.numGlyphs)
for gid := 0; gid < f.numGlyphs; gid++ {
name, ok := f.cff.glyphName(gid)
if !ok || name == "" {
continue
}
if _, seen := f.glyphNames[name]; !seen {
f.glyphNames[name] = GlyphIndex(gid)
}
}
}
// GlyphName is what the font calls a glyph. ok is false for a font that does
// not name its glyphs — a TrueType font usually does not, and a CFF font
// addressed by character identifier never does.
func (f *Font) GlyphName(gid GlyphIndex) (string, bool) {
if f.t1 != nil {
return f.t1.glyphName(int(gid))
}
if f.cff == nil || f.cff.isCID {
return "", false
}
return f.cff.glyphName(int(gid))
}
// GlyphIndexByName maps a glyph name to its index. ok is false when the font
// names no glyphs or has none of that name.
func (f *Font) GlyphIndexByName(name string) (GlyphIndex, bool) {
gid, ok := f.glyphNames[name]
return gid, ok
}
// GlyphIndexByCode maps a byte to a glyph through the font program's own
// built-in encoding — the Standard Encoding unless the program said otherwise.
// ok is false for a font that carries no such encoding, which is every
// TrueType font and every CFF font addressed by character identifier.
func (f *Font) GlyphIndexByCode(code byte) (GlyphIndex, bool) {
if f.t1 != nil {
return f.t1.glyphByCode(code)
}
if f.cff == nil || f.cff.isCID {
return 0, false
}
gid, ok := f.cff.glyphByCode(code)
if !ok || gid >= f.numGlyphs {
return 0, false
}
return GlyphIndex(gid), true
}
+1202
View File
File diff suppressed because it is too large Load Diff
+724
View File
@@ -0,0 +1,724 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import "fmt"
// This file adds CFF2 (variable Compact Font Format) outline support, so
// variable OpenType/CFF fonts (variable .otf, e.g. Source Serif Variable and
// many Adobe families) can be parsed and instanced. CFF2 is a close variant of
// CFF1/Type2 (cff.go): it shares the INDEX/DICT encodings and nearly all of the
// Type2 charstring path operators, so this file reuses cff.go's INDEX/DICT
// parsers, the Type2 operand decoder, and the whole Bézier-flattening pen
// (t2machine's moveTo/lineTo/curveTo and the curve helpers) to emit the same
// []contour model glyf.go produces.
//
// CFF2 differs from CFF1 in these ways, all handled here:
// - Header carries a 16-bit topDictLength; the Top DICT is a single DICT (no
// Top DICT INDEX), and every INDEX uses a 32-bit count (parseIndex2).
// - There is no per-glyph advance width, no endchar, no seac and no charset;
// charstrings simply end at their last byte.
// - Outlines vary with the design space: the 'blend' and 'vsindex' charstring
// operators pull deltas from the shared OpenType ItemVariationStore
// ('vstore') and interpolate operands in place at a normalized coordinate.
// - The Private DICT and its Local Subrs live per Font DICT in the FDArray,
// selected per glyph by FDSelect.
//
// CFF2 hinting is supported: hstem/vstem/hintmask stems and each Font DICT's
// Private-DICT hint parameters are recorded here and grid-fitted by cffhint.go
// exactly as for CFF1, so Face.SetHinting(true) grid-fits CFF2 glyphs too.
// Deferred (not implemented here, matching cff.go's scope): FDSelect format 4
// (for >65535 glyphs) and non-format-1 ItemVariationStores are rejected; metric
// variation (HVAR and friends) is out of scope. The interpreter's operand stack
// limit follows the CFF2 maximum.
// maxCFF2Stack is the CFF2 charstring operand-stack limit (the spec permits 513
// entries; blend expands the stack well past the Type2 limit of 48).
const maxCFF2Stack = 513
// cff2Table is a parsed CFF2 table: the CharStrings INDEX (one charstring per
// glyph), the Global Subr INDEX with its bias, the per-Font-DICT Local Subr
// INDEXes (with biases and default variation-store indices), the per-glyph
// Font-DICT selector, and the shared ItemVariationStore driving blends.
type cff2Table struct {
charStrings [][]byte
globalSubrs [][]byte
gsubrBias int
fdSelect []int // gid -> Font DICT index; nil means every glyph uses 0
localSubrs [][][]byte // per Font DICT: its Local Subr INDEX
localBias []int // per Font DICT: its Local Subr bias
privVsindex []int // per Font DICT: default vsindex from the Private DICT
privHints []*cffPrivate // per Font DICT: parsed Private-DICT hint parameters
vstore *itemVarStore
}
// regionAxis is one axis of one variation region: the tent function's lower,
// peak and upper normalized coordinates.
type regionAxis struct {
start, peak, end float64
}
// ivData is one ItemVariationData subtable reduced to what a CFF2 blend needs:
// the ordered list of region indices whose deltas the blend supplies. The delta
// values themselves come from the charstring, not from the store.
type ivData struct {
regionIndices []int
}
// itemVarStore is the shared OpenType ItemVariationStore (format 1) referenced
// by the Top DICT 'vstore' operator: the region list plus each variation-data
// subtable's region ordering.
type itemVarStore struct {
axisCount int
regions [][]regionAxis
data []ivData
}
// parseIndex2 decodes a CFF2 INDEX (identical to a CFF1 INDEX but with a 32-bit
// count) at r's current position, returning each stored object as a sub-slice of
// r.b and leaving r positioned just past the INDEX.
func parseIndex2(r *reader) ([][]byte, error) {
count := int(r.u32())
if r.err != nil {
return nil, fmt.Errorf("opentype: cff2 index count: %w", r.err)
}
if count == 0 {
return nil, nil
}
offSize := int(r.u8())
if offSize < 1 || offSize > 4 {
return nil, fmt.Errorf("opentype: cff2 index: bad offSize %d", offSize)
}
offsets := make([]int, count+1)
for k := range offsets {
offsets[k] = readOffset(r, offSize)
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cff2 index offsets: %w", r.err)
}
base := r.pos - offsets[0]
items := make([][]byte, count)
for k := 0; k < count; k++ {
start, end := base+offsets[k], base+offsets[k+1]
if start < 0 || end > len(r.b) || start > end {
return nil, fmt.Errorf("opentype: cff2 index: item %d out of range", k)
}
items[k] = r.b[start:end]
}
r.pos = base + offsets[count]
return items, nil
}
// parseDict2 decodes a CFF2 DICT into a map from operator to its operand list.
// It differs from cff.go's parseDict only in the operator range: CFF2 adds the
// single-byte operators vsindex (22) and blend (23) to the Private DICT and
// vstore (24) to the Top DICT, all of which are reserved in CFF1. Operand bytes
// (28, 29, 30 and 32..255) share cff.go's parseDictOperand decoder.
func parseDict2(b []byte) (map[int][]float64, error) {
d := map[int][]float64{}
var operands []float64
i := 0
for i < len(b) {
b0 := b[i]
if b0 < 28 { // operator (12 introduces a two-byte operator)
op := int(b0)
i++
if b0 == 12 {
if i >= len(b) {
return nil, fmt.Errorf("opentype: cff2 dict: truncated operator")
}
op = 1200 + int(b[i])
i++
}
d[op] = operands
operands = nil
continue
}
v, n, err := parseDictOperand(b, i)
if err != nil {
return nil, err
}
operands = append(operands, v)
i += n
}
return d, nil
}
// parseCFF2 decodes a CFF2 table: the 5-byte header, the Top DICT, the Global
// Subr INDEX, the CharStrings INDEX, the optional ItemVariationStore, and the
// FDArray/FDSelect that assign each glyph its Private DICT and Local Subrs.
func parseCFF2(data []byte) (*cff2Table, error) {
if len(data) < 5 {
return nil, fmt.Errorf("opentype: cff2 header: %w", errTruncated)
}
if data[0] != 2 {
return nil, fmt.Errorf("opentype: cff2: unsupported major version %d", data[0])
}
hdrSize := int(data[2])
hr := reader{b: data, pos: 3}
topDictLen := int(hr.u16())
if hdrSize < 5 || hdrSize > len(data) {
return nil, fmt.Errorf("opentype: cff2: bad headerSize %d", hdrSize)
}
if hdrSize+topDictLen > len(data) {
return nil, fmt.Errorf("opentype: cff2: Top DICT out of range")
}
top, err := parseDict2(data[hdrSize : hdrSize+topDictLen])
if err != nil {
return nil, err
}
r := &reader{b: data, pos: hdrSize + topDictLen}
gsubrs, err := parseIndex2(r) // Global Subr INDEX
if err != nil {
return nil, err
}
csOff, ok := dictInt(top, 17, 0)
if !ok {
return nil, fmt.Errorf("opentype: cff2: missing CharStrings offset")
}
if csOff < 0 || csOff > len(data) {
return nil, fmt.Errorf("opentype: cff2: CharStrings offset out of range")
}
charStrings, err := parseIndex2(&reader{b: data, pos: csOff})
if err != nil {
return nil, err
}
var vstore *itemVarStore
if vsOff, ok := dictInt(top, 24, 0); ok {
if vstore, err = parseItemVarStore(data, vsOff); err != nil {
return nil, err
}
}
localSubrs, localBias, privVsindex, privHints, err := parseCFF2FDArray(data, top)
if err != nil {
return nil, err
}
fdSelect, err := parseCFF2FDSelect(data, top, len(charStrings), len(localSubrs))
if err != nil {
return nil, err
}
return &cff2Table{
charStrings: charStrings,
globalSubrs: gsubrs,
gsubrBias: subrBias(len(gsubrs)),
fdSelect: fdSelect,
localSubrs: localSubrs,
localBias: localBias,
privVsindex: privVsindex,
privHints: privHints,
vstore: vstore,
}, nil
}
// parseItemVarStore decodes the ItemVariationStore at offset off in data, whose
// data is prefixed with a 16-bit length. Only format 1 is supported. For CFF2
// only the region list and each subtable's region ordering are needed; the
// stored delta sets are not read.
func parseItemVarStore(data []byte, off int) (*itemVarStore, error) {
lr := reader{b: data, pos: off}
length := int(lr.u16())
if lr.err != nil {
return nil, fmt.Errorf("opentype: cff2 vstore length: %w", lr.err)
}
start := lr.pos
if start+length > len(data) {
return nil, fmt.Errorf("opentype: cff2 vstore out of range")
}
ivs := data[start : start+length]
r := reader{b: ivs}
if format := r.u16(); format != 1 {
return nil, fmt.Errorf("opentype: cff2 vstore: unsupported format %d", format)
}
regionListOff := int(r.u32())
ivdCount := int(r.u16())
ivdOffsets := make([]int, ivdCount)
for i := range ivdOffsets {
ivdOffsets[i] = int(r.u32())
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cff2 vstore header: %w", r.err)
}
rr := reader{b: ivs, pos: regionListOff}
axisCount := int(rr.u16())
regionCount := int(rr.u16())
regions := make([][]regionAxis, regionCount)
for i := range regions {
axes := make([]regionAxis, axisCount)
for a := range axes {
axes[a].start = rr.f2dot14()
axes[a].peak = rr.f2dot14()
axes[a].end = rr.f2dot14()
}
regions[i] = axes
}
if rr.err != nil {
return nil, fmt.Errorf("opentype: cff2 vstore regions: %w", rr.err)
}
datas := make([]ivData, ivdCount)
for i, o := range ivdOffsets {
dr := reader{b: ivs, pos: o}
dr.u16() // itemCount (delta sets are not consumed by blend)
dr.u16() // wordDeltaCount
regionIndexCount := int(dr.u16())
idx := make([]int, regionIndexCount)
for k := range idx {
ri := int(dr.u16())
if ri >= regionCount {
return nil, fmt.Errorf("opentype: cff2 vstore: region index %d out of range", ri)
}
idx[k] = ri
}
if dr.err != nil {
return nil, fmt.Errorf("opentype: cff2 vstore data %d: %w", i, dr.err)
}
datas[i] = ivData{regionIndices: idx}
}
return &itemVarStore{axisCount: axisCount, regions: regions, data: datas}, nil
}
// regionScalar returns how strongly region ri applies at the normalized
// coordinate coords, reusing the shared tent-function evaluator (gvar.go).
func (vs *itemVarStore) regionScalar(ri int, coords []float64) float64 {
reg := vs.regions[ri]
lower := make([]float64, len(reg))
peak := make([]float64, len(reg))
upper := make([]float64, len(reg))
for a, ax := range reg {
lower[a], peak[a], upper[a] = ax.start, ax.peak, ax.end
}
return supportScalar(coords, lower, peak, upper)
}
// parseCFF2FDArray decodes the FDArray (an INDEX of Font DICTs) and each Font
// DICT's Private DICT, returning the per-Font-DICT Local Subr INDEXes, their
// biases and their default vsindex values. A CFF2 table without an FDArray is
// treated as having a single Font DICT with no Local Subrs.
func parseCFF2FDArray(data []byte, top map[int][]float64) (subrs [][][]byte, bias, vsindex []int, hints []*cffPrivate, err error) {
off, ok := dictInt(top, 1236, 0)
if !ok {
return [][][]byte{nil}, []int{subrBias(0)}, []int{0}, []*cffPrivate{nil}, nil
}
if off < 0 || off > len(data) {
return nil, nil, nil, nil, fmt.Errorf("opentype: cff2: FDArray offset out of range")
}
fontDicts, err := parseIndex2(&reader{b: data, pos: off})
if err != nil {
return nil, nil, nil, nil, err
}
if len(fontDicts) == 0 {
return nil, nil, nil, nil, fmt.Errorf("opentype: cff2: empty FDArray")
}
subrs = make([][][]byte, len(fontDicts))
bias = make([]int, len(fontDicts))
vsindex = make([]int, len(fontDicts))
hints = make([]*cffPrivate, len(fontDicts))
for i, fd := range fontDicts {
d, err := parseDict2(fd)
if err != nil {
return nil, nil, nil, nil, err
}
ls, vs, h, err := parseCFF2Private(data, d)
if err != nil {
return nil, nil, nil, nil, err
}
subrs[i] = ls
bias[i] = subrBias(len(ls))
vsindex[i] = vs
hints[i] = h
}
return subrs, bias, vsindex, hints, nil
}
// parseCFF2Private resolves a Font DICT's Private DICT and, if present, its
// Local Subr INDEX and default vsindex, and parses its hint parameters (blue
// zones, standard/snap stem widths) for the grid-fitter (cffhint.go).
func parseCFF2Private(data []byte, fd map[int][]float64) ([][]byte, int, *cffPrivate, error) {
pv, ok := fd[18]
if !ok {
return nil, 0, nil, nil
}
if len(pv) < 2 {
return nil, 0, nil, fmt.Errorf("opentype: cff2: bad Private DICT entry")
}
psize, poff := int(pv[0]), int(pv[1])
if psize < 0 || poff < 0 || poff+psize > len(data) {
return nil, 0, nil, fmt.Errorf("opentype: cff2: Private DICT out of range")
}
priv, err := parseDict2(data[poff : poff+psize])
if err != nil {
return nil, 0, nil, err
}
hints := parsePrivateHints(priv)
vsindex := 0
if v, ok := dictInt(priv, 22, 0); ok {
vsindex = v
}
soff, ok := dictInt(priv, 19, 0)
if !ok {
return nil, vsindex, hints, nil
}
soff += poff
if soff < 0 || soff > len(data) {
return nil, 0, nil, fmt.Errorf("opentype: cff2: Local Subrs offset out of range")
}
ls, err := parseIndex2(&reader{b: data, pos: soff})
if err != nil {
return nil, 0, nil, err
}
return ls, vsindex, hints, nil
}
// parseCFF2FDSelect decodes the FDSelect (glyph -> Font DICT index) in format 0
// or 3. A table without an FDSelect returns nil (every glyph uses Font DICT 0).
func parseCFF2FDSelect(data []byte, top map[int][]float64, numGlyphs, numFDs int) ([]int, error) {
off, ok := dictInt(top, 1237, 0)
if !ok {
return nil, nil
}
if off < 0 || off >= len(data) {
return nil, fmt.Errorf("opentype: cff2: FDSelect offset out of range")
}
r := reader{b: data, pos: off}
format := r.u8()
sel := make([]int, numGlyphs)
switch format {
case 0:
for g := 0; g < numGlyphs; g++ {
fd := int(r.u8())
if fd >= numFDs {
return nil, fmt.Errorf("opentype: cff2: FDSelect fd %d out of range", fd)
}
sel[g] = fd
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cff2 FDSelect: %w", r.err)
}
case 3:
nRanges := int(r.u16())
firsts := make([]int, nRanges)
fds := make([]int, nRanges)
for i := 0; i < nRanges; i++ {
firsts[i] = int(r.u16())
fds[i] = int(r.u8())
}
sentinel := int(r.u16())
if r.err != nil {
return nil, fmt.Errorf("opentype: cff2 FDSelect: %w", r.err)
}
for i := 0; i < nRanges; i++ {
if fds[i] >= numFDs {
return nil, fmt.Errorf("opentype: cff2: FDSelect fd %d out of range", fds[i])
}
next := sentinel
if i+1 < nRanges {
next = firsts[i+1]
}
for g := firsts[i]; g < next && g < numGlyphs; g++ {
sel[g] = fds[i]
}
}
default:
return nil, fmt.Errorf("opentype: cff2: unsupported FDSelect format %d", format)
}
return sel, nil
}
// cff2machine interprets one glyph's CFF2 charstring. It borrows a t2machine as
// its pen (pen position, contour under construction, finished contours and the
// Bézier-flattening path operators) and adds the CFF2-only variation state: the
// normalized coordinate, the active variation-store index and the blend vector
// (the per-region scalars, with a leading 1 for the default master).
type cff2machine struct {
c *cff2Table
tm *t2machine
localSubrs [][]byte
localBias int
coords []float64
vsindex int
bv []float64
}
// outline interprets glyph gid's CFF2 charstring at the normalized variation
// coordinate coords (F2Dot14 int16 per axis, as produced by NormalizeCoords);
// a nil or short coords vector leaves the remaining axes at their default. The
// result is the instanced outline as contours in font units, all points
// on-curve with cubics pre-flattened.
func (c *cff2Table) outline(gid int, coords []int16) ([]contour, error) {
cs, _, err := c.outlineHints(gid, coords)
return cs, err
}
// outlineHints is outline plus the hinting data (recorded stems, per-hintmask
// activation and the glyph's Font-DICT Private-DICT hint parameters) that
// cffhint.go's grid-fitter consumes. The outline is still instanced at coords.
func (c *cff2Table) outlineHints(gid int, coords []int16) ([]contour, *cffGlyphHints, error) {
if gid < 0 || gid >= len(c.charStrings) {
return nil, nil, fmt.Errorf("opentype: cff2 glyph %d out of range", gid)
}
fd := 0
if c.fdSelect != nil {
fd = c.fdSelect[gid]
}
axisCount := 0
if c.vstore != nil {
axisCount = c.vstore.axisCount
}
m := &cff2machine{
c: c,
tm: &t2machine{},
localSubrs: c.localSubrs[fd],
localBias: c.localBias[fd],
coords: make([]float64, axisCount),
vsindex: c.privVsindex[fd],
}
for a := 0; a < axisCount && a < len(coords); a++ {
m.coords[a] = float64(coords[a]) / 16384.0
}
if err := m.computeBV(); err != nil {
return nil, nil, err
}
if err := m.run(c.charStrings[gid], 0); err != nil {
return nil, nil, err
}
m.tm.finishContour()
gh := &cffGlyphHints{
stems: m.tm.stemHints,
hintMasks: m.tm.hintMasks,
cntrGroups: m.tm.cntrGroups,
flexRanges: m.tm.flexRanges,
priv: c.privHints[fd],
}
return m.tm.contours, gh, nil
}
// computeBV rebuilds the blend vector for the active vsindex: a leading 1 (the
// default master's scalar) followed by one scalar per region of the selected
// ItemVariationData. With no variation store the vector is just [1], so blend
// becomes a no-op that leaves its default operands unchanged.
func (m *cff2machine) computeBV() error {
m.bv = append(m.bv[:0], 1)
vs := m.c.vstore
if vs == nil {
return nil
}
if m.vsindex < 0 || m.vsindex >= len(vs.data) {
return fmt.Errorf("opentype: cff2: vsindex %d out of range", m.vsindex)
}
for _, ri := range vs.data[m.vsindex].regionIndices {
m.bv = append(m.bv, vs.regionScalar(ri, m.coords))
}
return nil
}
// push appends v to the operand stack, guarding the CFF2 stack-depth limit.
func (m *cff2machine) push(v float64) error {
if len(m.tm.stack) >= maxCFF2Stack {
return fmt.Errorf("opentype: cff2: charstring stack overflow")
}
m.tm.stack = append(m.tm.stack, v)
return nil
}
// run executes a charstring (or subroutine) body; depth guards recursion. It
// returns when the code is exhausted or a 'return' operator ends a subroutine.
func (m *cff2machine) run(code []byte, depth int) error {
if depth > maxT2Depth {
return fmt.Errorf("opentype: cff2: subroutine recursion too deep")
}
i := 0
for i < len(code) {
b0 := code[i]
i++
if b0 >= 32 || b0 == 28 {
v, n, err := m.tm.operand(code, i-1)
if err != nil {
return err
}
if err := m.push(v); err != nil {
return err
}
i += n - 1
continue
}
ni, ret, err := m.operator(b0, code, i, depth)
if err != nil {
return err
}
i = ni
if ret {
return nil
}
}
return nil
}
// operator executes one CFF2 operator byte b0. code/i give the stream position
// just past b0 (for hintmask bytes and the escape second byte); it returns the
// updated stream index, whether the current subroutine should stop ('return'),
// and any error. Unlike Type2 there is no leading-width handling.
func (m *cff2machine) operator(b0 byte, code []byte, i, depth int) (int, bool, error) {
tm := m.tm
switch b0 {
case 1, 3, 18, 23: // hstem, vstem, hstemhm, vstemhm
tm.recordStems(b0 == 1 || b0 == 18)
case 19, 20: // hintmask, cntrmask
tm.recordStems(false) // any operands here are implicit vstem hints
ni, herr := tm.readHintMask(code, i, b0 == 19)
if herr != nil {
return i, false, herr
}
i = ni
case 21: // rmoveto
if len(tm.stack) < 2 {
return i, false, fmt.Errorf("opentype: cff2: rmoveto needs 2 args")
}
n := len(tm.stack)
tm.moveTo(tm.stack[n-2], tm.stack[n-1])
tm.stack = tm.stack[:0]
case 22: // hmoveto
if len(tm.stack) < 1 {
return i, false, fmt.Errorf("opentype: cff2: hmoveto needs 1 arg")
}
tm.moveTo(tm.stack[len(tm.stack)-1], 0)
tm.stack = tm.stack[:0]
case 4: // vmoveto
if len(tm.stack) < 1 {
return i, false, fmt.Errorf("opentype: cff2: vmoveto needs 1 arg")
}
tm.moveTo(0, tm.stack[len(tm.stack)-1])
tm.stack = tm.stack[:0]
case 5: // rlineto
s := tm.stack
for j := 0; j+2 <= len(s); j += 2 {
tm.lineTo(s[j], s[j+1])
}
tm.stack = tm.stack[:0]
case 6: // hlineto
tm.altLine(true)
case 7: // vlineto
tm.altLine(false)
case 8: // rrcurveto
tm.rrcurveto(tm.stack)
tm.stack = tm.stack[:0]
case 24: // rcurveline
tm.rcurveline(tm.stack)
tm.stack = tm.stack[:0]
case 25: // rlinecurve
tm.rlinecurve(tm.stack)
tm.stack = tm.stack[:0]
case 26: // vvcurveto
tm.vvcurveto(tm.stack)
tm.stack = tm.stack[:0]
case 27: // hhcurveto
tm.hhcurveto(tm.stack)
tm.stack = tm.stack[:0]
case 30: // vhcurveto
tm.altCurve(tm.stack, false)
tm.stack = tm.stack[:0]
case 31: // hvcurveto
tm.altCurve(tm.stack, true)
tm.stack = tm.stack[:0]
case 10: // callsubr
if err := m.callSubr(m.localSubrs, m.localBias, depth); err != nil {
return i, false, err
}
case 29: // callgsubr
if err := m.callSubr(m.c.globalSubrs, m.c.gsubrBias, depth); err != nil {
return i, false, err
}
case 11: // return
return i, true, nil
case 15: // vsindex
if err := m.vsindexOp(); err != nil {
return i, false, err
}
case 16: // blend
if err := m.blendOp(); err != nil {
return i, false, err
}
case 12: // escape: two-byte operator (flex family)
if i >= len(code) {
return i, false, fmt.Errorf("opentype: cff2 charstring: truncated escape")
}
b1 := code[i]
i++
if err := tm.escape(b1); err != nil {
return i, false, err
}
default:
return i, false, fmt.Errorf("opentype: cff2: unknown operator %d", b0)
}
return i, false, nil
}
// callSubr executes the subroutine selected by the top-of-stack index (plus the
// bias) from subrs. A 'return' inside the subroutine ends only that subroutine.
func (m *cff2machine) callSubr(subrs [][]byte, bias, depth int) error {
st := m.tm.stack
if len(st) < 1 {
return fmt.Errorf("opentype: cff2: callsubr needs an index")
}
idx := int(st[len(st)-1]) + bias
m.tm.stack = st[:len(st)-1]
if idx < 0 || idx >= len(subrs) {
return fmt.Errorf("opentype: cff2: subr index %d out of range", idx)
}
return m.run(subrs[idx], depth+1)
}
// vsindexOp handles the vsindex operator: it selects the ItemVariationData that
// subsequent blends draw from and recomputes the blend vector.
func (m *cff2machine) vsindexOp() error {
st := m.tm.stack
if len(st) < 1 {
return fmt.Errorf("opentype: cff2: vsindex needs 1 arg")
}
m.vsindex = int(st[len(st)-1])
m.tm.stack = st[:0]
return m.computeBV()
}
// blendOp handles the blend operator: the top operand n gives the number of
// resulting values; below it are n default values followed by n groups of k
// region deltas (k = regions of the active vsindex). Each default value is
// replaced in place by default + Σ bv[region]·delta, and the deltas and n are
// popped, leaving the n blended values as operands for the following operator.
func (m *cff2machine) blendOp() error {
st := m.tm.stack
if len(st) < 1 {
return fmt.Errorf("opentype: cff2: blend needs a count")
}
numBlends := int(st[len(st)-1])
st = st[:len(st)-1]
if numBlends < 0 {
return fmt.Errorf("opentype: cff2: blend count %d invalid", numBlends)
}
k := len(m.bv) - 1
need := numBlends * (k + 1)
if need > len(st) {
return fmt.Errorf("opentype: cff2: blend needs %d operands, have %d", need, len(st))
}
base := len(st) - need
di := base + numBlends
for vi := 0; vi < numBlends; vi++ {
sum := st[base+vi]
for j := 1; j <= k; j++ {
sum += m.bv[j] * st[di]
di++
}
st[base+vi] = sum
}
m.tm.stack = st[:base+numBlends]
return nil
}
+649
View File
@@ -0,0 +1,649 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import (
"math"
"sort"
)
// This file grid-fits CFF/CFF2 (Type2) outlines the way FreeType's CFF hinter
// does: it aligns stem edges and blue-zone reference points to the pixel grid
// at a target ppem, then interpolates the rest of the outline between them. It
// is the CFF counterpart of the TrueType instruction interpreter (hint.go);
// where TrueType grid-fitting is imperative (a bytecode VM moves points), CFF
// grid-fitting is declarative — the font only states its stems (recorded by the
// Type2 interpreter in cff.go/cff2.go) and its Private-DICT hint parameters
// (blue zones, standard and snap stem widths, parsed here), and the renderer
// decides how to snap them.
//
// Pipeline (per glyph, per axis, independent x and y):
// 1. Scale every stem edge and blue-zone edge to device space (pixels/em).
// 2. Snap each active stem's width toward StdHW/StdVW / StemSnap and round it,
// then position the stem so one edge lands on an integer pixel boundary,
// anchoring horizontal (hstem) edges that fall in a blue zone to the rounded
// zone position instead. This yields, per stem edge, an original device
// position and its hinted (grid-fitted) position.
// 3. Build a piecewise-linear control map from those (original -> hinted)
// edge pairs plus a blue-zone flat-edge anchor per zone, and remap every
// outline point: a point on a control edge snaps with it, a point between
// two edges interpolates proportionally, and a point outside the outermost
// edges shifts rigidly with the nearest one. This is the same interpolation
// TrueType uses for untouched points (IUP), applied here to the whole
// outline.
//
// What is implemented: Private-DICT hint-parameter parsing (BlueValues,
// OtherBlues, FamilyBlues, FamilyOtherBlues, BlueScale, BlueShift, BlueFuzz,
// StdHW, StdVW, StemSnapH, StemSnapV); top/bottom blue-zone construction with
// overshoot, family-blue substitution and the BlueScale overshoot-suppression
// rule; stem-width snapping and edge grid-fitting; blue-zone alignment of stem
// edges and reference points; and per-point hintmask hint selection.
//
// The finer FreeType sub-nuances are also applied here:
// - counter control: a cntrmask hint group's counters (the gaps between the
// group's same-orientation stems, e.g. the three verticals of 'm') are
// equalised after grid-fitting so the stems keep even spacing;
// - mid-subpath hintmask changes: each point is remapped through the control
// map built from the stems active at that point (not the subpath's first
// point), so a mask that switches mid-contour hints each portion correctly;
// - flex regions: the near-flat span a flex (flex/flex1/hflex/hflex1) draws is
// interpolated smoothly between its grid-fitted anchors instead of letting an
// interior point snap to a stem edge as though it were a straight stem;
// - stem darkening: an optional ppem-dependent emboldening of stem edges that
// improves contrast on the anti-aliased rasteriser at small sizes (off by
// default, toggled by Face.SetStemDarkening; the output is byte-identical to
// the undarkened result when it is off).
// cffStemHint is one resolved stem hint in font units: the two edge positions
// (min < max) and whether it is a horizontal stem (hstem, edges are Y
// positions, controlling horizontal features and blue alignment) or a vertical
// stem (vstem, edges are X positions).
type cffStemHint struct {
horizontal bool
min, max float64
}
// cffHintMask is one recorded hintmask activation: the outline-point index at
// which it takes effect (so per-subpath hint selection can find the mask in
// force for a given point) and, per declared stem, whether that stem is active.
type cffHintMask struct {
pointIndex int
active []bool
}
// flexRange is one flex region's outline-point span (inclusive global point
// indices of the points the two flex curves emitted). The point at start-1 is
// the flex's entry anchor and the point at end is its exit anchor; the interior
// points between them are interpolated smoothly by the grid-fitter.
type flexRange struct {
start, end int
}
// cffGlyphHints bundles a glyph's recorded stems, hintmask activations,
// counter-control (cntrmask) stem groups and flex-region point spans with the
// font's parsed Private-DICT hint parameters, as returned by the CFF/CFF2
// interpreters alongside the outline.
type cffGlyphHints struct {
stems []cffStemHint
hintMasks []cffHintMask
cntrGroups [][]int
flexRanges []flexRange
priv *cffPrivate
}
// maskSelAt returns the index into hintMasks of the hintmask in force at the
// given outline-point index — the last hintmask whose pointIndex does not exceed
// it — or -1 when none applies yet (every stem is then treated as active). It is
// the primitive the grid-fitter keys its per-point control maps on so a hintmask
// that changes mid-subpath selects the right stems at each point.
func (gh *cffGlyphHints) maskSelAt(pointIndex int) int {
sel := -1
for i, hm := range gh.hintMasks {
if hm.pointIndex <= pointIndex {
sel = i
}
}
return sel
}
// activeMaskAt returns the per-stem activation of the hintmask in force at the
// given outline-point index. A nil result means "all stems active" (no hintmask
// applies yet, or the glyph declared none), which the grid-fitter treats as
// every stem on.
func (gh *cffGlyphHints) activeMaskAt(pointIndex int) []bool {
sel := gh.maskSelAt(pointIndex)
if sel < 0 {
return nil
}
return gh.hintMasks[sel].active
}
// cffPrivate holds the Private-DICT hint parameters that drive grid-fitting.
// Blue arrays are stored already accumulated (the DICT delta encoding undone)
// and in font units; BlueScale/BlueShift/BlueFuzz carry their CFF defaults when
// the DICT omits them.
type cffPrivate struct {
blueValues []float64 // paired: [0:2] baseline (bottom) zone, rest top zones
otherBlues []float64 // paired: additional bottom zones
familyBlues []float64 // paired: family baseline+top zones (consistency)
familyOtherBlues []float64 // paired: family bottom zones
blueScale float64 // overshoot-suppression threshold (default 0.039625)
blueShift float64 // minimum overshoot before it is shown (default 7)
blueFuzz float64 // blue-zone capture extension (default 1)
stdHW float64 // standard horizontal-stem width (hstem)
stdVW float64 // standard vertical-stem width (vstem)
stemSnapH []float64 // horizontal-stem snap widths
stemSnapV []float64 // vertical-stem snap widths
defaultWidthX float64 // the advance of a glyph whose charstring gives none
nominalWidthX float64 // what a charstring's width is measured from
}
// CFF Private-DICT defaults for the overshoot-control parameters (CFF spec).
const (
defaultBlueScale = 0.039625
defaultBlueShift = 7
defaultBlueFuzz = 1
)
// snapTolerancePx is how close (in device pixels) a stem's scaled width must be
// to a standard/snap width for the width to be rendered as that standard width
// (unifying near-equal stems) before rounding to whole pixels.
const snapTolerancePx = 0.5
// parsePrivateHints extracts the hint parameters from an already-decoded
// Private DICT (CFF or CFF2 share the operator codes). Absent overshoot-control
// operators take their CFF defaults; the delta-encoded blue and stem-snap
// arrays are accumulated into absolute font-unit values.
func parsePrivateHints(d map[int][]float64) *cffPrivate {
p := &cffPrivate{
blueScale: defaultBlueScale,
blueShift: defaultBlueShift,
blueFuzz: defaultBlueFuzz,
}
p.blueValues = accumDelta(d[6])
p.otherBlues = accumDelta(d[7])
p.familyBlues = accumDelta(d[8])
p.familyOtherBlues = accumDelta(d[9])
if v, ok := dictFloat(d, 10); ok {
p.stdHW = v
}
if v, ok := dictFloat(d, 11); ok {
p.stdVW = v
}
if v, ok := dictFloat(d, 1209); ok {
p.blueScale = v
}
if v, ok := dictFloat(d, 1210); ok {
p.blueShift = v
}
if v, ok := dictFloat(d, 1211); ok {
p.blueFuzz = v
}
p.stemSnapH = accumDelta(d[1212])
p.stemSnapV = accumDelta(d[1213])
if v, ok := d[20]; ok && len(v) > 0 {
p.defaultWidthX = v[0]
}
if v, ok := d[21]; ok && len(v) > 0 {
p.nominalWidthX = v[0]
}
return p
}
// dictFloat returns the first operand of operator op in d as a float, or
// ok=false when the operator is absent or carries no operand.
func dictFloat(d map[int][]float64, op int) (float64, bool) {
v, ok := d[op]
if !ok || len(v) == 0 {
return 0, false
}
return v[0], true
}
// accumDelta undoes the CFF DICT "delta" encoding: the stored operands are
// successive differences, so the absolute values are their running sum. A nil
// or empty input yields nil.
func accumDelta(v []float64) []float64 {
if len(v) == 0 {
return nil
}
out := make([]float64, len(v))
sum := 0.0
for i, d := range v {
sum += d
out[i] = sum
}
return out
}
// blueZone is one alignment zone in font units: flat is the reference edge that
// snaps to the grid (the baseline, x-height, cap height, …); over is the
// overshoot extent (below flat for a bottom zone, above for a top zone) where
// round glyph features spill past the flat edge.
type blueZone struct {
flat float64
over float64
}
// makeZone builds a zone from a (a, b) blue pair. A top zone's flat edge is the
// lower value with overshoot above; a bottom zone's flat edge is the upper
// value with overshoot below.
func makeZone(a, b float64, isTop bool) blueZone {
lo, hi := math.Min(a, b), math.Max(a, b)
if isTop {
return blueZone{flat: lo, over: hi}
}
return blueZone{flat: hi, over: lo}
}
// applyFamily substitutes the corresponding FamilyBlues flat edge for a zone's
// flat edge when the two are within one device pixel, so members of a type
// family align identically at small sizes (the CFF FamilyBlues rule). fam is
// the family array paired the same way as the primary array; k is the pair
// index.
func applyFamily(z *blueZone, fam []float64, k int, isTop bool, scale float64) {
if k+1 >= len(fam) {
return
}
fz := makeZone(fam[k], fam[k+1], isTop)
if math.Abs((fz.flat-z.flat)*scale) < 1 {
z.flat = fz.flat
}
}
// buildBlueZones assembles the blue zones from the Private-DICT arrays: the
// first BlueValues pair is the baseline (a bottom zone), the remaining
// BlueValues pairs are top zones, and every OtherBlues pair is a bottom zone.
// Family blues substitute for consistency (applyFamily).
func buildBlueZones(p *cffPrivate, scale float64) []blueZone {
var zones []blueZone
bv := p.blueValues
for k := 0; k+1 < len(bv); k += 2 {
isTop := k >= 2
z := makeZone(bv[k], bv[k+1], isTop)
applyFamily(&z, p.familyBlues, k, isTop, scale)
zones = append(zones, z)
}
ob := p.otherBlues
for k := 0; k+1 < len(ob); k += 2 {
z := makeZone(ob[k], ob[k+1], false)
applyFamily(&z, p.familyOtherBlues, k, false, scale)
zones = append(zones, z)
}
return zones
}
// capture reports whether edgeDev (a device-space stem edge) falls in this zone
// and, if so, the device position it should be aligned to. Within BlueFuzz of
// the zone the edge is captured; it snaps to the rounded flat edge unless
// overshoot is enabled (not suppressed) and the edge lies at least BlueShift
// beyond the flat edge, in which case a rounded overshoot of at least one pixel
// is preserved.
func (z blueZone) capture(edgeDev, scale, blueShift, blueFuzz float64, suppress bool) (float64, bool) {
flatDev := z.flat * scale
overDev := z.over * scale
lo, hi := math.Min(flatDev, overDev), math.Max(flatDev, overDev)
fuzz := blueFuzz * scale
if edgeDev < lo-fuzz || edgeDev > hi+fuzz {
return 0, false
}
base := math.Round(flatDev)
if suppress {
return base, true
}
over := edgeDev - flatDev
if math.Abs(over) < blueShift*scale {
return base, true
}
o := math.Round(math.Abs(over))
if o < 1 {
o = 1
}
if over < 0 {
o = -o
}
return base + o, true
}
// hintedStem is one stem after grid-fitting: the original device-space edge
// positions (oLo < oHi) and their hinted (snapped) positions.
type hintedStem struct {
oLo, oHi float64
hLo, hHi float64
}
// hintStem grid-fits one stem: its width is snapped toward the standard/snap
// widths and rounded; for a horizontal stem an edge that lands in a blue zone
// is anchored to the rounded zone position (and the opposite edge placed one
// snapped width away), otherwise the lower edge is rounded to the pixel grid.
func hintStem(s cffStemHint, scale float64, p *cffPrivate, blues []blueZone, suppress bool) hintedStem {
lo := math.Min(s.min, s.max) * scale
hi := math.Max(s.min, s.max) * scale
std, snaps := p.stdVW, p.stemSnapV
if s.horizontal {
std, snaps = p.stdHW, p.stemSnapH
}
w := snapWidth(hi-lo, std*scale, snaps, scale)
if s.horizontal {
if t, ok := captureEdge(hi, blues, p, scale, suppress); ok {
return hintedStem{oLo: lo, oHi: hi, hLo: t - w, hHi: t}
}
if t, ok := captureEdge(lo, blues, p, scale, suppress); ok {
return hintedStem{oLo: lo, oHi: hi, hLo: t, hHi: t + w}
}
}
hLo := math.Round(lo)
return hintedStem{oLo: lo, oHi: hi, hLo: hLo, hHi: hLo + w}
}
// captureEdge returns the aligned device position for a horizontal-stem edge
// that falls in one of the blue zones, testing each zone in turn.
func captureEdge(edgeDev float64, blues []blueZone, p *cffPrivate, scale float64, suppress bool) (float64, bool) {
for _, z := range blues {
if t, ok := z.capture(edgeDev, scale, p.blueShift, p.blueFuzz, suppress); ok {
return t, true
}
}
return 0, false
}
// snapWidth rounds a scaled stem width to whole pixels, first snapping it to the
// nearest standard or snap width (std and snaps are in font units, scaled here)
// when within snapTolerancePx so near-equal stems render at one consistent
// width. The result is at least one pixel.
func snapWidth(w, stdDev float64, snaps []float64, scale float64) float64 {
best, bestD := w, math.Inf(1)
consider := func(v float64) {
if v <= 0 {
return
}
if d := math.Abs(w - v); d < bestD {
best, bestD = v, d
}
}
consider(stdDev)
for _, s := range snaps {
consider(s * scale)
}
if bestD > snapTolerancePx {
best = w
}
r := math.Round(best)
if r < 1 {
r = 1
}
return r
}
// controlMap is a piecewise-linear 1-D remap built from (original -> hinted)
// device positions, sorted and deduplicated by original position.
type controlMap struct {
orig []float64
hint []float64
}
// add records one control point (an original device position and where it is
// hinted to).
func (cm *controlMap) add(o, h float64) {
cm.orig = append(cm.orig, o)
cm.hint = append(cm.hint, h)
}
// finalize sorts the control points by original position and merges points that
// share an original position (averaging their hinted targets), leaving a
// strictly increasing orig sequence for interpolation.
func (cm *controlMap) finalize() {
n := len(cm.orig)
if n == 0 {
return
}
idx := make([]int, n)
for i := range idx {
idx[i] = i
}
sort.SliceStable(idx, func(a, b int) bool { return cm.orig[idx[a]] < cm.orig[idx[b]] })
var os, hs []float64
for _, i := range idx {
o, h := cm.orig[i], cm.hint[i]
if len(os) > 0 && o == os[len(os)-1] {
// Same original edge from two hints: average the targets and keep one.
hs[len(hs)-1] = (hs[len(hs)-1] + h) / 2
continue
}
os = append(os, o)
hs = append(hs, h)
}
cm.orig, cm.hint = os, hs
}
// remap maps a device coordinate through the control map: below the first
// control point or above the last it shifts by that endpoint's delta; between
// two it interpolates linearly. An empty map is the identity.
func (cm *controlMap) remap(v float64) float64 {
n := len(cm.orig)
switch {
case n == 0:
return v
case v <= cm.orig[0]:
return v + (cm.hint[0] - cm.orig[0])
case v >= cm.orig[n-1]:
return v + (cm.hint[n-1] - cm.orig[n-1])
}
i := sort.Search(n, func(k int) bool { return cm.orig[k] >= v })
a, b := cm.orig[i-1], cm.orig[i]
ha, hb := cm.hint[i-1], cm.hint[i]
t := (v - a) / (b - a)
return ha + t*(hb-ha)
}
// hintPoint holds one outline point through grid-fitting: its original scaled
// (device) position (ox, oy), its hinted position (hx, hy) and its on-curve flag.
type hintPoint struct {
ox, oy, hx, hy float64
on bool
}
// cffGridFit grid-fits contours (in font units) using the recorded stems and
// Private-DICT hint parameters, returning the hinted contours in font units.
// scale is font-units-to-pixels and ppem is pixels/em (the BlueScale rule);
// darken enables stem darkening (weight compensation). It is a no-op returning
// equivalent geometry when there are no stems and no blue zones.
func cffGridFit(contours []contour, gh *cffGlyphHints, scale, ppem float64, darken bool) []contour {
priv := gh.priv
if priv == nil {
priv = &cffPrivate{blueScale: defaultBlueScale, blueShift: defaultBlueShift, blueFuzz: defaultBlueFuzz}
}
suppress := ppem*priv.blueScale < 1
blues := buildBlueZones(priv, scale)
// Grid-fit every stem once (its hinted position is independent of subpath),
// then equalise counter-group counters and optionally darken the stems.
hs := make([]hintedStem, len(gh.stems))
for i, s := range gh.stems {
hs[i] = hintStem(s, scale, priv, blues, suppress)
}
applyCounterControl(hs, gh.stems, gh.cntrGroups)
if darken {
darkenStems(hs, stemDarkenPixels(ppem))
}
// Remap every point through the control map built from the stems active at
// that point (per-point so a mid-subpath hintmask change hints correctly). The
// maps for a given active-hintmask selection are built once and cached.
cache := map[int]axisMaps{}
getMaps := func(sel int) axisMaps {
if m, ok := cache[sel]; ok {
return m
}
var active []bool
if sel >= 0 {
active = gh.hintMasks[sel].active
}
x, y := buildMaps(hs, gh.stems, active, blues, scale)
m := axisMaps{x: x, y: y}
cache[sel] = m
return m
}
var total int
for _, c := range contours {
total += len(c)
}
pts := make([]hintPoint, 0, total)
for _, c := range contours {
for _, p := range c {
ox, oy := p.x*scale, p.y*scale
m := getMaps(gh.maskSelAt(len(pts)))
pts = append(pts, hintPoint{ox: ox, oy: oy, hx: m.x.remap(ox), hy: m.y.remap(oy), on: p.on})
}
}
// Keep flex spans smooth: re-derive each flex region's interior points by
// interpolating between its grid-fitted entry (start-1) and exit (end) anchors.
for _, fr := range gh.flexRanges {
if fr.start-1 < 0 {
continue
}
interpolateFlex(pts, fr.start-1, fr.end)
}
out := make([]contour, len(contours))
idx := 0
for ci, c := range contours {
nc := make(contour, len(c))
for j := range c {
p := pts[idx]
nc[j] = outlinePoint{x: p.hx / scale, y: p.hy / scale, on: p.on}
idx++
}
out[ci] = nc
}
return out
}
// axisMaps pairs the x- and y-axis control maps for one active-hintmask
// selection.
type axisMaps struct {
x, y controlMap
}
// applyCounterControl equalises the counters of every recorded cntrmask group:
// within each group the same-orientation stems (independently for horizontal and
// vertical) are chained at an even, whole-pixel gap so features like the three
// verticals of 'm' keep uniform spacing after grid-fitting.
func applyCounterControl(hs []hintedStem, stems []cffStemHint, groups [][]int) {
for _, g := range groups {
equalizeCounters(hs, stems, g, true)
equalizeCounters(hs, stems, g, false)
}
}
// equalizeCounters repositions the horizontal (or vertical) stems of one counter
// group so the gaps between them are all the group's rounded average gap, keeping
// each stem's grid-fitted width and anchoring on the first (lowest) stem.
func equalizeCounters(hs []hintedStem, stems []cffStemHint, group []int, horizontal bool) {
var idx []int
for _, i := range group {
if stems[i].horizontal == horizontal {
idx = append(idx, i)
}
}
if len(idx) < 2 {
return
}
sort.Slice(idx, func(a, b int) bool { return hs[idx[a]].oLo < hs[idx[b]].oLo })
totalGap := 0.0
for k := 1; k < len(idx); k++ {
totalGap += hs[idx[k]].oLo - hs[idx[k-1]].oHi
}
gap := math.Round(totalGap / float64(len(idx)-1))
if gap < 1 {
gap = 1
}
for k := 1; k < len(idx); k++ {
w := hs[idx[k]].hHi - hs[idx[k]].hLo
lo := hs[idx[k-1]].hHi + gap
hs[idx[k]].hLo = lo
hs[idx[k]].hHi = lo + w
}
}
// darkenStems embiggens every stem by d device pixels per edge (its lower edge
// moves down and its upper edge up), the weight compensation that improves stem
// contrast on the anti-aliased rasteriser at small sizes.
func darkenStems(hs []hintedStem, d float64) {
for i := range hs {
hs[i].hLo -= d
hs[i].hHi += d
}
}
// stemDarkenPixels returns the per-edge stem emboldening in device pixels at the
// given ppem: strongest at small sizes and fading linearly to none at and above
// darkenMaxPPEM, mirroring FreeType's default stem darkening (a contrast boost
// for small text that is invisible once glyphs are large enough to hint cleanly).
func stemDarkenPixels(ppem float64) float64 {
if ppem <= 0 || ppem >= darkenMaxPPEM {
return 0
}
return darkenMaxPixels * (1 - ppem/darkenMaxPPEM)
}
// darkenMaxPPEM is the ppem at (and above) which stem darkening stops, and
// darkenMaxPixels is its per-edge strength (in device pixels) at ppem→0.
const (
darkenMaxPPEM = 48
darkenMaxPixels = 0.15
)
// interpolateFlex re-derives the hinted positions of a flex region's interior
// points (before+1 .. end-1) by IUP-style interpolation between the grid-fitted
// anchors at indices before and end, so the shallow flex curve keeps its smooth
// shape rather than each interior point snapping to a stem edge.
func interpolateFlex(pts []hintPoint, before, end int) {
for k := before + 1; k < end; k++ {
pts[k].hx = interp1D(pts[before].ox, pts[end].ox, pts[before].hx, pts[end].hx, pts[k].ox)
pts[k].hy = interp1D(pts[before].oy, pts[end].oy, pts[before].hy, pts[end].hy, pts[k].oy)
}
}
// interp1D maps o from the original interval [oA, oB] to the hinted interval,
// carrying the endpoint deltas: when the endpoints share an original coordinate
// (a degenerate interval) the point takes the anchor A's delta.
func interp1D(oA, oB, hA, hB, o float64) float64 {
dA := hA - oA
if oB == oA {
return o + dA
}
t := (o - oA) / (oB - oA)
return o + dA + t*((hB-oB)-dA)
}
// buildMaps assembles the x- and y-axis control maps for one subpath: each
// active vertical stem contributes its two edges to the x map and each active
// horizontal stem to the y map, and every blue zone contributes a flat-edge
// anchor to the y map (blue zones are always active). A nil active mask means
// every stem is active.
func buildMaps(hs []hintedStem, stems []cffStemHint, active []bool, blues []blueZone, scale float64) (x, y controlMap) {
for i, st := range hs {
if active != nil && !active[i] {
continue
}
if stems[i].horizontal {
y.add(st.oLo, st.hLo)
y.add(st.oHi, st.hHi)
} else {
x.add(st.oLo, st.hLo)
x.add(st.oHi, st.hHi)
}
}
for _, z := range blues {
f := z.flat * scale
y.add(f, math.Round(f))
}
x.finalize()
y.finalize()
return x, y
}
+88
View File
@@ -0,0 +1,88 @@
// Copyright (c) 2026, the go-opentype authors
// All rights reserved.
//
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
// cffStandardStrings are the 391 names every CFF font shares. A string id
// below this many is one of these; anything higher is an index into the
// font's own String INDEX. The list is fixed by the Compact Font Format
// specification and is the only way to learn what a glyph in such a font is
// called — which is what a PDF needs, because it addresses a simple font by
// name rather than by number.
//
// The first ninety-five of them are the printable ASCII characters in order,
// which is what makes the table checkable against the Standard Encoding
// rather than merely transcribed: see TestTheStandardStringsAgreeWithTheStandardEncoding.
var cffStandardStrings = [...]string{
".notdef", "space", "exclam", "quotedbl", "numbersign", "dollar", "percent",
"ampersand", "quoteright", "parenleft", "parenright", "asterisk", "plus",
"comma", "hyphen", "period", "slash", "zero", "one", "two", "three", "four",
"five", "six", "seven", "eight", "nine", "colon", "semicolon", "less",
"equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", "G",
"H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
"W", "X", "Y", "Z", "bracketleft", "backslash", "bracketright",
"asciicircum", "underscore", "quoteleft", "a", "b", "c", "d", "e", "f", "g",
"h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
"w", "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde",
"exclamdown", "cent", "sterling", "fraction", "yen", "florin", "section",
"currency", "quotesingle", "quotedblleft", "guillemotleft", "guilsinglleft",
"guilsinglright", "fi", "fl", "endash", "dagger", "daggerdbl",
"periodcentered", "paragraph", "bullet", "quotesinglbase", "quotedblbase",
"quotedblright", "guillemotright", "ellipsis", "perthousand", "questiondown",
"grave", "acute", "circumflex", "tilde", "macron", "breve", "dotaccent",
"dieresis", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "emdash",
"AE", "ordfeminine", "Lslash", "Oslash", "OE", "ordmasculine", "ae",
"dotlessi", "lslash", "oslash", "oe", "germandbls", "onesuperior",
"logicalnot", "mu", "trademark", "Eth", "onehalf", "plusminus", "Thorn",
"onequarter", "divide", "brokenbar", "degree", "thorn", "threequarters",
"twosuperior", "registered", "minus", "eth", "multiply", "threesuperior",
"copyright", "Aacute", "Acircumflex", "Adieresis", "Agrave", "Aring",
"Atilde", "Ccedilla", "Eacute", "Ecircumflex", "Edieresis", "Egrave",
"Iacute", "Icircumflex", "Idieresis", "Igrave", "Ntilde", "Oacute",
"Ocircumflex", "Odieresis", "Ograve", "Otilde", "Scaron", "Uacute",
"Ucircumflex", "Udieresis", "Ugrave", "Yacute", "Ydieresis", "Zcaron",
"aacute", "acircumflex", "adieresis", "agrave", "aring", "atilde",
"ccedilla", "eacute", "ecircumflex", "edieresis", "egrave", "iacute",
"icircumflex", "idieresis", "igrave", "ntilde", "oacute", "ocircumflex",
"odieresis", "ograve", "otilde", "scaron", "uacute", "ucircumflex",
"udieresis", "ugrave", "yacute", "ydieresis", "zcaron", "exclamsmall",
"Hungarumlautsmall", "dollaroldstyle", "dollarsuperior", "ampersandsmall",
"Acutesmall", "parenleftsuperior", "parenrightsuperior", "twodotenleader",
"onedotenleader", "zerooldstyle", "oneoldstyle", "twooldstyle",
"threeoldstyle", "fouroldstyle", "fiveoldstyle", "sixoldstyle",
"sevenoldstyle", "eightoldstyle", "nineoldstyle", "commasuperior",
"threequartersemdash", "periodsuperior", "questionsmall", "asuperior",
"bsuperior", "centsuperior", "dsuperior", "esuperior", "isuperior",
"lsuperior", "msuperior", "nsuperior", "osuperior", "rsuperior", "ssuperior",
"tsuperior", "ff", "ffi", "ffl", "parenleftinferior", "parenrightinferior",
"Circumflexsmall", "hyphensuperior", "Gravesmall", "Asmall", "Bsmall",
"Csmall", "Dsmall", "Esmall", "Fsmall", "Gsmall", "Hsmall", "Ismall",
"Jsmall", "Ksmall", "Lsmall", "Msmall", "Nsmall", "Osmall", "Psmall",
"Qsmall", "Rsmall", "Ssmall", "Tsmall", "Usmall", "Vsmall", "Wsmall",
"Xsmall", "Ysmall", "Zsmall", "colonmonetary", "onefitted", "rupiah",
"Tildesmall", "exclamdownsmall", "centoldstyle", "Lslashsmall",
"Scaronsmall", "Zcaronsmall", "Dieresissmall", "Brevesmall", "Caronsmall",
"Dotaccentsmall", "Macronsmall", "figuredash", "hypheninferior",
"Ogoneksmall", "Ringsmall", "Cedillasmall", "questiondownsmall",
"oneeighth", "threeeighths", "fiveeighths", "seveneighths", "onethird",
"twothirds", "zerosuperior", "foursuperior", "fivesuperior", "sixsuperior",
"sevensuperior", "eightsuperior", "ninesuperior", "zeroinferior",
"oneinferior", "twoinferior", "threeinferior", "fourinferior",
"fiveinferior", "sixinferior", "seveninferior", "eightinferior",
"nineinferior", "centinferior", "dollarinferior", "periodinferior",
"commainferior", "Agravesmall", "Aacutesmall", "Acircumflexsmall",
"Atildesmall", "Adieresissmall", "Aringsmall", "AEsmall", "Ccedillasmall",
"Egravesmall", "Eacutesmall", "Ecircumflexsmall", "Edieresissmall",
"Igravesmall", "Iacutesmall", "Icircumflexsmall", "Idieresissmall",
"Ethsmall", "Ntildesmall", "Ogravesmall", "Oacutesmall", "Ocircumflexsmall",
"Otildesmall", "Odieresissmall", "OEsmall", "Oslashsmall", "Ugravesmall",
"Uacutesmall", "Ucircumflexsmall", "Udieresissmall", "Yacutesmall",
"Thornsmall", "Ydieresissmall", "001.000", "001.001", "001.002", "001.003",
"Black", "Bold", "Book", "Light", "Medium", "Regular", "Roman", "Semibold",
}
// nStdStrings is how many names every CFF font shares.
const nStdStrings = len(cffStandardStrings)
+75
View File
@@ -0,0 +1,75 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
// A font's cmap table carries several subtables, each written for one
// platform and encoding, and Font.GlyphIndex answers through whichever of
// them can represent the most codepoints. That is the right answer for a font
// addressed by character, and the wrong one for a font addressed some other
// way: a PDF says how its fonts are addressed, and a symbolic TrueType font
// embedded in one is addressed through its own Microsoft Symbol or Macintosh
// Roman subtable by raw byte, not through its Unicode subtable by codepoint.
//
// Only the document can make that choice, so the subtables are listed here
// rather than chosen here.
// NumCharacterMaps returns how many cmap subtables the font carries in a form
// this package decodes. It is zero for a font with no character map at all.
//
// Format 14 (Unicode Variation Sequences) is not one of them: it maps a pair
// rather than a code, and is reached through Font.GlyphIndexVariation.
func (f *Font) NumCharacterMaps() int { return len(f.cmaps) }
// CharacterMap reports which platform and encoding the i'th subtable was
// written for, and which format it is written in. ok is false when i is not
// the index of a subtable.
//
// The pairs worth recognising are (3,1) and (3,10), Microsoft Unicode, and
// platform 0, Unicode; (3,0), Microsoft Symbol, whose codes are the font's
// own and conventionally live at 0xF000 + code; and (1,0), Macintosh Roman.
func (f *Font) CharacterMap(i int) (platform, encoding, format uint16, ok bool) {
if i < 0 || i >= len(f.cmaps) {
return 0, 0, 0, false
}
c := f.cmaps[i]
return c.platform, c.encoding, c.format, true
}
// GlyphIndexInMap maps a code to a glyph through one named subtable rather
// than through the one the font would choose. ok is false when i is not the
// index of a subtable, or when that subtable does not map the code (including
// a mapping to the .notdef glyph).
//
// The code is whatever the subtable is indexed by: a Unicode codepoint for a
// Unicode subtable, and a raw byte of the font's own encoding for a Microsoft
// Symbol or Macintosh Roman one.
func (f *Font) GlyphIndexInMap(i int, code rune) (GlyphIndex, bool) {
if i < 0 || i >= len(f.cmaps) {
return 0, false
}
return f.cmaps[i].lookup.lookup(code)
}
// RuneOfGlyphInMap inverts one subtable: which code reaches this glyph. ok is
// false when i is not the index of a subtable, or when no code in it reaches
// that glyph.
//
// A subtable maps many codes onto one glyph — a space and a no-break space
// commonly share one — and the lowest of them is the one reported. Inverting a
// Unicode subtable is how the character a glyph stands for is recovered when
// the document says nothing about it and the program names nothing: the font
// itself said which codepoint that glyph is for.
//
// The inverse is built once per subtable, on the first call, and kept.
func (f *Font) RuneOfGlyphInMap(i int, gid GlyphIndex) (rune, bool) {
if i < 0 || i >= len(f.cmaps) {
return 0, false
}
if f.cmaps[i].inverse == nil {
f.cmaps[i].inverse = f.cmaps[i].lookup.reverse(maxReverseCodes)
}
r, ok := f.cmaps[i].inverse[gid]
return r, ok
}
+933
View File
@@ -0,0 +1,933 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import "fmt"
// cmapLookup maps a rune to a glyph index. ok is false when the rune is not
// mapped by the selected subtable (including a mapping to the .notdef glyph).
//
// reverse answers the other question: which code reaches a given glyph. A
// subtable is a many-to-one map — a space and a no-break space commonly share
// one glyph — so the inverse keeps the lowest code that reaches each glyph,
// and is built once for the whole subtable rather than searched per glyph.
//
// budget bounds how many codes the walk may visit. Most formats describe no
// more codes than their own bytes can hold and ignore it; formats 4, 8 and 12
// do not — one group of a format-12 subtable can claim the whole Unicode
// space, and a damaged one can claim more — so those three spend it.
type cmapLookup interface {
lookup(r rune) (GlyphIndex, bool)
reverse(budget int) map[GlyphIndex]rune
}
// maxReverseCodes is the budget Font.RuneOfGlyphInMap gives a walk. A subtable
// that describes more than a million codes is describing far more than any
// inverse of it will be asked about, and the bound keeps a malformed font from
// costing more than a well-formed one.
const maxReverseCodes = 1 << 20
// reverseRange records the codes [first, first+count) as reaching the glyphs
// starting at gid, one glyph per code, into inv. It keeps the lowest code for
// each glyph and reports how many codes it consumed of the budget left.
func reverseRange(inv map[GlyphIndex]rune, first, count, gid uint32, budget int) int {
if uint32(budget) < count {
count = uint32(budget)
}
for i := uint32(0); i < count; i++ {
g := GlyphIndex(gid + i)
if g == 0 {
continue
}
if _, seen := inv[g]; !seen {
inv[g] = rune(first + i)
}
}
return int(count)
}
// cmapSubtable is one decoded cmap subtable together with the platform and
// encoding it was written for. Which of them to address a font through is the
// caller's decision and cannot be made here: a font embedded in a PDF is
// addressed the way the document says, and only the document knows.
type cmapSubtable struct {
platform uint16
encoding uint16
format uint16
lookup cmapLookup
inverse map[GlyphIndex]rune // built on first use by Font.RuneOfGlyphInMap
}
// parseCmap selects and decodes a Unicode cmap subtable. It understands
// formats 0, 2, 4, 6, 8, 10, 12 and 13 for the base rune->glyph mapping,
// preferring whichever can represent the most codepoints:
// 13 = 12 > 10 > 8 > 4 > 6 > 2 > 0.
// Format 14 (Unicode Variation Sequences) is a separate, always-parsed side
// table exposed through Font.GlyphIndexVariation; it never competes for f.cmap
// and its presence does not change ordinary GlyphIndex behaviour.
func (f *Font) parseCmap(b []byte) error {
r := reader{b: b}
r.skip(2) // version
numTables := int(r.u16())
if r.err != nil {
return fmt.Errorf("opentype: cmap header: %w", r.err)
}
var best cmapLookup
bestScore := 0
for i := 0; i < numTables; i++ {
platform := r.u16()
encoding := r.u16()
offset := int(r.u32())
if r.err != nil {
return fmt.Errorf("opentype: cmap record: %w", r.err)
}
if offset+2 > len(b) {
return fmt.Errorf("opentype: cmap subtable offset: %w", errTruncated)
}
sub := b[offset:]
format := be16(sub)
var (
lk cmapLookup
score int
err error
)
switch format {
case 0:
lk, err = parseCmap0(sub)
score = 1
case 2:
lk, err = parseCmap2(sub)
score = 2
case 6:
lk, err = parseCmap6(sub)
score = 3
case 4:
lk, err = parseCmap4(sub)
score = 4
case 8:
lk, err = parseCmap8(sub)
score = 5
case 10:
lk, err = parseCmap10(sub)
score = 6
case 12:
lk, err = parseCmap12(sub)
score = 7
case 13:
lk, err = parseCmap13(sub)
score = 7
case 14:
vs, verr := parseCmap14(sub)
if verr != nil {
return verr
}
f.cmapVS = vs
continue
default:
continue
}
if err != nil {
return err
}
f.cmaps = append(f.cmaps, cmapSubtable{
platform: platform,
encoding: encoding,
format: format,
lookup: lk,
})
if score > bestScore {
best = lk
bestScore = score
}
}
if best == nil {
return fmt.Errorf("opentype: no supported cmap subtable")
}
f.cmap = best
return nil
}
// cmap4 is a decoded format-4 (segmented, BMP) cmap subtable.
type cmap4 struct {
segCount int
endCode []uint16
startCode []uint16
idDelta []int16
idRangeOffset []uint16
glyphIDArray []uint16
}
// parseCmap4 decodes a format-4 subtable from sub (which starts at the format
// field).
func parseCmap4(sub []byte) (cmapLookup, error) {
r := reader{b: sub}
r.skip(6) // format, length, language
segX2 := int(r.u16())
segCount := segX2 / 2
r.skip(6) // searchRange, entrySelector, rangeShift
c := &cmap4{
segCount: segCount,
endCode: make([]uint16, segCount),
startCode: make([]uint16, segCount),
idDelta: make([]int16, segCount),
idRangeOffset: make([]uint16, segCount),
}
for i := 0; i < segCount; i++ {
c.endCode[i] = r.u16()
}
r.skip(2) // reservedPad
for i := 0; i < segCount; i++ {
c.startCode[i] = r.u16()
}
for i := 0; i < segCount; i++ {
c.idDelta[i] = r.i16()
}
for i := 0; i < segCount; i++ {
c.idRangeOffset[i] = r.u16()
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 4: %w", r.err)
}
// The remaining bytes are the glyphIdArray (uint16 each).
rest := (len(sub) - r.pos) / 2
c.glyphIDArray = make([]uint16, rest)
for i := 0; i < rest; i++ {
c.glyphIDArray[i] = r.u16()
}
return c, nil
}
// lookup implements the format-4 segment search with idDelta/idRangeOffset.
func (c *cmap4) lookup(r rune) (GlyphIndex, bool) {
if uint32(r) > 0xFFFF {
return 0, false
}
cc := uint16(r)
for i := 0; i < c.segCount; i++ {
if c.endCode[i] < cc {
continue
}
if c.startCode[i] > cc {
return 0, false
}
var g uint16
if c.idRangeOffset[i] == 0 {
g = cc + uint16(c.idDelta[i])
} else {
idx := int(c.idRangeOffset[i]/2) + int(cc-c.startCode[i]) - (c.segCount - i)
if idx < 0 || idx >= len(c.glyphIDArray) {
return 0, false
}
g = c.glyphIDArray[idx]
if g != 0 {
g += uint16(c.idDelta[i])
}
}
if g == 0 {
return 0, false
}
return GlyphIndex(g), true
}
return 0, false
}
// reverse walks the segments in code order, resolving each code the same way
// lookup does, and keeps the lowest code that reaches each glyph.
func (c *cmap4) reverse(budget int) map[GlyphIndex]rune {
inv := make(map[GlyphIndex]rune)
for i := 0; i < c.segCount; i++ {
if c.startCode[i] > c.endCode[i] {
continue
}
for cc := uint32(c.startCode[i]); cc <= uint32(c.endCode[i]); cc++ {
if budget == 0 {
return inv
}
budget--
g, ok := c.lookup(rune(cc))
if !ok {
continue
}
if _, seen := inv[g]; !seen {
inv[g] = rune(cc)
}
}
}
return inv
}
// cmap12group is one sequential-map group of a format-12 subtable.
type cmap12group struct {
start uint32
end uint32
startGID uint32
}
// cmap12 is a decoded format-12 (segmented coverage, full Unicode) subtable.
type cmap12 struct {
groups []cmap12group
}
// parseCmap12 decodes a format-12 subtable from sub (starting at the format
// field).
func parseCmap12(sub []byte) (cmapLookup, error) {
r := reader{b: sub}
r.skip(12) // format, reserved, length, language
numGroups := int(r.u32())
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 12 header: %w", r.err)
}
c := &cmap12{groups: make([]cmap12group, numGroups)}
for i := 0; i < numGroups; i++ {
c.groups[i].start = r.u32()
c.groups[i].end = r.u32()
c.groups[i].startGID = r.u32()
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 12 groups: %w", r.err)
}
return c, nil
}
// lookup implements the format-12 binary search over sorted groups.
func (c *cmap12) lookup(r rune) (GlyphIndex, bool) {
cc := uint32(r)
lo, hi := 0, len(c.groups)
for lo < hi {
mid := (lo + hi) / 2
g := c.groups[mid]
if cc < g.start {
hi = mid
} else if cc > g.end {
lo = mid + 1
} else {
return GlyphIndex(g.startGID + (cc - g.start)), true
}
}
return 0, false
}
// reverse expands each group: a group advances one glyph per code point, so
// the codes it covers reach a contiguous run of glyphs.
func (c *cmap12) reverse(budget int) map[GlyphIndex]rune {
inv := make(map[GlyphIndex]rune)
for _, g := range c.groups {
if g.end < g.start {
continue
}
budget -= reverseRange(inv, g.start, g.end-g.start+1, g.startGID, budget)
if budget == 0 {
break
}
}
return inv
}
// cmap13group is one constant-map group of a format-13 subtable: every code
// point in [start, end] maps to the single glyph glyphID.
type cmap13group struct {
start uint32
end uint32
glyphID uint32
}
// cmap13 is a decoded format-13 (many-to-one range mappings) subtable. Its
// on-disk layout is identical to format 12, but the third group field is read
// with different semantics: format 12's startGlyphID advances one glyph per
// code point across a group, whereas format 13's glyphID is a constant so the
// whole range collapses onto a single glyph. This is what LastResort and other
// fallback fonts use to point every code point in a block at one "missing
// glyph" placeholder.
type cmap13 struct {
groups []cmap13group
}
// parseCmap13 decodes a format-13 subtable from sub (starting at the format
// field).
func parseCmap13(sub []byte) (cmapLookup, error) {
r := reader{b: sub}
r.skip(12) // format, reserved, length, language
numGroups := int(r.u32())
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 13 header: %w", r.err)
}
c := &cmap13{groups: make([]cmap13group, numGroups)}
for i := 0; i < numGroups; i++ {
c.groups[i].start = r.u32()
c.groups[i].end = r.u32()
c.groups[i].glyphID = r.u32()
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 13 groups: %w", r.err)
}
return c, nil
}
// lookup implements the format-13 binary search over sorted groups, returning
// the group's constant glyph for any code point it covers.
func (c *cmap13) lookup(r rune) (GlyphIndex, bool) {
cc := uint32(r)
lo, hi := 0, len(c.groups)
for lo < hi {
mid := (lo + hi) / 2
g := c.groups[mid]
if cc < g.start {
hi = mid
} else if cc > g.end {
lo = mid + 1
} else {
return GlyphIndex(g.glyphID), true
}
}
return 0, false
}
// reverse keeps one code per group: every code point a format-13 group covers
// reaches the same glyph, so only the lowest of them is worth recording, and a
// group covering a whole Unicode block costs one entry rather than a million.
// The budget goes unspent: a group costs one entry however many codes it
// covers, so the walk is only as long as the subtable itself.
func (c *cmap13) reverse(int) map[GlyphIndex]rune {
inv := make(map[GlyphIndex]rune)
for _, g := range c.groups {
if g.end < g.start || g.glyphID == 0 {
continue
}
gid := GlyphIndex(g.glyphID)
if _, seen := inv[gid]; !seen {
inv[gid] = rune(g.start)
}
}
return inv
}
// cmap8Group is one sequential-map group of a format-8 subtable; identical in
// shape and semantics to a format-12 group (a range advancing one glyph per
// code point from startGID).
type cmap8Group struct {
start uint32
end uint32
startGID uint32
}
// cmap8 is a decoded format-8 (mixed 16/32-bit coverage) subtable. is32 is
// the on-disk 65536-bit bitmap (8192 bytes) flagging which 16-bit lead code
// units begin a 32-bit (surrogate-pair) character rather than standing alone
// as a BMP character; it is retained for spec fidelity but, like other
// implementations, is not consulted by lookup, since lookup already receives
// a fully decoded rune rather than raw UTF-16 code units — the groups alone
// are sufficient to resolve it, exactly as in format 12.
type cmap8 struct {
is32 [8192]byte
groups []cmap8Group
}
// parseCmap8 decodes a format-8 subtable from sub (starting at the format
// field).
func parseCmap8(sub []byte) (cmapLookup, error) {
r := reader{b: sub}
r.skip(12) // format, reserved, length, language
var c cmap8
for i := range c.is32 {
c.is32[i] = r.u8()
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 8 is32: %w", r.err)
}
numGroups := int(r.u32())
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 8 header: %w", r.err)
}
c.groups = make([]cmap8Group, numGroups)
for i := range c.groups {
c.groups[i].start = r.u32()
c.groups[i].end = r.u32()
c.groups[i].startGID = r.u32()
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 8 groups: %w", r.err)
}
return &c, nil
}
// lookup implements the format-8 binary search over sorted groups, the same
// algorithm as format 12's lookup (the is32 bitmap plays no role once the
// input is already a decoded rune).
func (c *cmap8) lookup(r rune) (GlyphIndex, bool) {
cc := uint32(r)
lo, hi := 0, len(c.groups)
for lo < hi {
mid := (lo + hi) / 2
g := c.groups[mid]
if cc < g.start {
hi = mid
} else if cc > g.end {
lo = mid + 1
} else {
return GlyphIndex(g.startGID + (cc - g.start)), true
}
}
return 0, false
}
// reverse expands each group, exactly as format 12's does: the two formats
// describe their groups the same way.
func (c *cmap8) reverse(budget int) map[GlyphIndex]rune {
inv := make(map[GlyphIndex]rune)
for _, g := range c.groups {
if g.end < g.start {
continue
}
budget -= reverseRange(inv, g.start, g.end-g.start+1, g.startGID, budget)
if budget == 0 {
break
}
}
return inv
}
// cmap0 is a decoded format-0 (byte encoding table) subtable: a flat 256-byte
// glyph-id array covering U+0000-U+00FF.
type cmap0 struct {
glyphIDArray [256]byte
}
// parseCmap0 decodes a format-0 subtable from sub (starting at the format
// field).
func parseCmap0(sub []byte) (cmapLookup, error) {
r := reader{b: sub}
r.skip(6) // format, length, language
var c cmap0
for i := range c.glyphIDArray {
c.glyphIDArray[i] = r.u8()
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 0: %w", r.err)
}
return &c, nil
}
// lookup returns the direct array entry for r, or false when r is outside
// U+0000-U+00FF or maps to the .notdef glyph.
func (c *cmap0) lookup(r rune) (GlyphIndex, bool) {
if uint32(r) > 0xFF {
return 0, false
}
g := c.glyphIDArray[r]
if g == 0 {
return 0, false
}
return GlyphIndex(g), true
}
// reverse walks the 256-entry array, keeping the lowest code per glyph.
// The budget goes unspent: the array is 256 entries.
func (c *cmap0) reverse(int) map[GlyphIndex]rune {
inv := make(map[GlyphIndex]rune)
for code, g := range c.glyphIDArray {
if g == 0 {
continue
}
if _, seen := inv[GlyphIndex(g)]; !seen {
inv[GlyphIndex(g)] = rune(code)
}
}
return inv
}
// cmap6 is a decoded format-6 (trimmed table mapping) subtable: a
// contiguous, zero-based glyph-id array starting at firstCode.
type cmap6 struct {
firstCode uint16
glyphIDArray []uint16
}
// parseCmap6 decodes a format-6 subtable from sub (starting at the format
// field).
func parseCmap6(sub []byte) (cmapLookup, error) {
r := reader{b: sub}
r.skip(6) // format, length, language
firstCode := r.u16()
entryCount := int(r.u16())
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 6 header: %w", r.err)
}
c := &cmap6{firstCode: firstCode, glyphIDArray: make([]uint16, entryCount)}
for i := range c.glyphIDArray {
c.glyphIDArray[i] = r.u16()
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 6: %w", r.err)
}
return c, nil
}
// lookup returns the trimmed-table entry for r, or false when r falls
// outside [firstCode, firstCode+len(glyphIDArray)) or maps to .notdef.
func (c *cmap6) lookup(r rune) (GlyphIndex, bool) {
if uint32(r) < uint32(c.firstCode) {
return 0, false
}
idx := uint32(r) - uint32(c.firstCode)
if idx >= uint32(len(c.glyphIDArray)) {
return 0, false
}
g := c.glyphIDArray[idx]
if g == 0 {
return 0, false
}
return GlyphIndex(g), true
}
// reverse walks the trimmed array, keeping the lowest code per glyph.
// The budget goes unspent: the array is no longer than the subtable said.
func (c *cmap6) reverse(int) map[GlyphIndex]rune {
inv := make(map[GlyphIndex]rune)
for i, g := range c.glyphIDArray {
if g == 0 {
continue
}
if _, seen := inv[GlyphIndex(g)]; !seen {
inv[GlyphIndex(g)] = rune(uint32(c.firstCode) + uint32(i))
}
}
return inv
}
// cmap10 is a decoded format-10 (trimmed array) subtable: the 32-bit
// analogue of format 6 — a contiguous, zero-based glyph-id array starting at
// startCharCode, wide enough to cover astral code points.
type cmap10 struct {
startCharCode uint32
glyphIDArray []uint16
}
// parseCmap10 decodes a format-10 subtable from sub (starting at the format
// field).
func parseCmap10(sub []byte) (cmapLookup, error) {
r := reader{b: sub}
r.skip(12) // format, reserved, length, language
startCharCode := r.u32()
numChars := int(r.u32())
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 10 header: %w", r.err)
}
c := &cmap10{startCharCode: startCharCode, glyphIDArray: make([]uint16, numChars)}
for i := range c.glyphIDArray {
c.glyphIDArray[i] = r.u16()
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 10: %w", r.err)
}
return c, nil
}
// lookup returns the trimmed-table entry for r, or false when r falls
// outside [startCharCode, startCharCode+len(glyphIDArray)) or maps to
// .notdef.
func (c *cmap10) lookup(r rune) (GlyphIndex, bool) {
if uint32(r) < c.startCharCode {
return 0, false
}
idx := uint32(r) - c.startCharCode
if idx >= uint32(len(c.glyphIDArray)) {
return 0, false
}
g := c.glyphIDArray[idx]
if g == 0 {
return 0, false
}
return GlyphIndex(g), true
}
// reverse walks the trimmed array, keeping the lowest code per glyph.
// The budget goes unspent: the array is no longer than the subtable said.
func (c *cmap10) reverse(int) map[GlyphIndex]rune {
inv := make(map[GlyphIndex]rune)
for i, g := range c.glyphIDArray {
if g == 0 {
continue
}
if _, seen := inv[GlyphIndex(g)]; !seen {
inv[GlyphIndex(g)] = rune(c.startCharCode + uint32(i))
}
}
return inv
}
// cmap2SubHeader is one decoded subHeader record of a format-2 subtable.
// fieldOffset is the byte offset of this subHeader's idRangeOffset field
// relative to the start of glyphIndexArray, precomputed so lookup can turn
// (fieldOffset + idRangeOffset + 2*index) into a glyphIndexArray index the
// same way format 4 turns its own idRangeOffset into an array index.
type cmap2SubHeader struct {
firstCode uint16
entryCount uint16
idDelta int16
idRangeOffset uint16
fieldOffset int
}
// cmap2 is a decoded format-2 (high-byte mapping through table) subtable,
// the legacy encoding for mixed single/double-byte CJK charsets. The code
// passed to lookup is treated as a raw 1- or 2-byte legacy code (its low 16
// bits), exactly like the other cmap formats treat their input as a raw
// code rather than interpreting charset semantics.
type cmap2 struct {
subHeaderKeys [256]uint16 // per high-byte value, subHeaders' byte offset (index*8)
subHeaders []cmap2SubHeader
glyphIndexArray []uint16
}
// parseCmap2 decodes a format-2 subtable from sub (starting at the format
// field). The subtable has no explicit subHeader count; it is derived from
// the highest offset referenced by subHeaderKeys, following the same
// convention used by other OpenType/TrueType cmap format-2 parsers.
func parseCmap2(sub []byte) (cmapLookup, error) {
r := reader{b: sub}
r.skip(6) // format, length, language
var c cmap2
for i := range c.subHeaderKeys {
c.subHeaderKeys[i] = r.u16()
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 2 keys: %w", r.err)
}
var maxKey uint16
for _, k := range c.subHeaderKeys {
if k > maxKey {
maxKey = k
}
}
numSubHeaders := int(maxKey)/8 + 1
glyphIndexArrayStart := r.pos + numSubHeaders*8
c.subHeaders = make([]cmap2SubHeader, numSubHeaders)
for i := range c.subHeaders {
fieldPos := r.pos + 6 // firstCode(2)+entryCount(2)+idDelta(2) precede idRangeOffset
c.subHeaders[i] = cmap2SubHeader{
firstCode: r.u16(),
entryCount: r.u16(),
idDelta: r.i16(),
idRangeOffset: r.u16(),
fieldOffset: fieldPos - glyphIndexArrayStart,
}
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 2 subheaders: %w", r.err)
}
// The remaining bytes are the glyphIndexArray (uint16 each); rest is sized
// to whatever fits, so this run of reads never overruns.
rest := (len(sub) - r.pos) / 2
c.glyphIndexArray = make([]uint16, rest)
for i := range c.glyphIndexArray {
c.glyphIndexArray[i] = r.u16()
}
return &c, nil
}
// lookup implements the format-2 high-byte dispatch: the high byte of r
// selects a subHeader via subHeaderKeys (a value of 0 selects subHeader 0,
// which also serves single-byte codes, since a single-byte code has an
// implicit high byte of 0), then the low byte is checked against that
// subHeader's [firstCode, firstCode+entryCount) range and translated through
// glyphIndexArray and idDelta, mirroring format 4's idRangeOffset scheme.
func (c *cmap2) lookup(r rune) (GlyphIndex, bool) {
if uint32(r) > 0xFFFF {
return 0, false
}
code := uint32(r)
hi := (code >> 8) & 0xFF
lo := code & 0xFF
sh := c.subHeaders[int(c.subHeaderKeys[hi])/8]
rel := lo - uint32(sh.firstCode)
if rel >= uint32(sh.entryCount) || sh.idRangeOffset == 0 {
return 0, false
}
gidx := (sh.fieldOffset + int(sh.idRangeOffset) + 2*int(rel)) / 2
if gidx < 0 || gidx >= len(c.glyphIndexArray) {
return 0, false
}
g := c.glyphIndexArray[gidx]
if g == 0 {
return 0, false
}
return GlyphIndex(uint16(int32(g) + int32(sh.idDelta))), true
}
// reverse walks every code the high-byte dispatch can reach, resolving each
// the same way lookup does. A single-byte code has an implicit high byte of
// zero, so the walk covers the whole 16-bit space rather than only the codes
// some subHeader claims.
// The budget goes unspent: the walk is the 16-bit code space, once.
func (c *cmap2) reverse(int) map[GlyphIndex]rune {
inv := make(map[GlyphIndex]rune)
for cc := uint32(0); cc <= 0xFFFF; cc++ {
g, ok := c.lookup(rune(cc))
if !ok {
continue
}
if _, seen := inv[g]; !seen {
inv[g] = rune(cc)
}
}
return inv
}
// cmap14DefaultRange is one entry of a format-14 default-UVS table: base
// runes in [start, start+count) pair with this variation selector to
// resolve through the font's ordinary cmap rather than an explicit glyph.
type cmap14DefaultRange struct {
start uint32
count uint32
}
// cmap14NonDefault is one entry of a format-14 non-default-UVS table: base
// rune unicode paired with this variation selector resolves to glyph
// directly, bypassing the ordinary cmap.
type cmap14NonDefault struct {
unicode uint32
glyph GlyphIndex
}
// cmap14Record is one variation-selector record of a format-14 subtable.
type cmap14Record struct {
varSelector uint32
defaultRanges []cmap14DefaultRange
nonDefault []cmap14NonDefault
}
// cmap14 is a decoded format-14 (Unicode Variation Sequences) subtable. It is
// never installed as Font.cmap: it is a side table consulted only through
// GlyphIndexVariation.
type cmap14 struct {
records []cmap14Record
}
// parseCmap14 decodes a format-14 subtable from sub (starting at the format
// field).
func parseCmap14(sub []byte) (*cmap14, error) {
r := reader{b: sub}
r.skip(6) // format, length
numRecords := int(r.u32())
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 14 header: %w", r.err)
}
type rawRecord struct {
varSelector uint32
defaultOff, nonDefaultOff uint32
}
raws := make([]rawRecord, numRecords)
for i := range raws {
raws[i] = rawRecord{
varSelector: r.u24(),
defaultOff: r.u32(),
nonDefaultOff: r.u32(),
}
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 14 records: %w", r.err)
}
c := &cmap14{records: make([]cmap14Record, numRecords)}
for i, raw := range raws {
c.records[i].varSelector = raw.varSelector
if raw.defaultOff != 0 {
ranges, err := parseUVSDefaultTable(sub, int(raw.defaultOff))
if err != nil {
return nil, err
}
c.records[i].defaultRanges = ranges
}
if raw.nonDefaultOff != 0 {
maps, err := parseUVSNonDefaultTable(sub, int(raw.nonDefaultOff))
if err != nil {
return nil, err
}
c.records[i].nonDefault = maps
}
}
return c, nil
}
// parseUVSDefaultTable decodes a Default UVS table at byte offset off within
// sub (the format-14 subtable, offsets are subtable-relative). The offset
// itself is only checked against sub's length here; a short read of the
// table's own fields is reported separately by the reader below.
func parseUVSDefaultTable(sub []byte, off int) ([]cmap14DefaultRange, error) {
if off > len(sub) {
return nil, fmt.Errorf("opentype: cmap format 14 default UVS offset: %w", errTruncated)
}
r := reader{b: sub[off:]}
numRanges := int(r.u32())
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 14 default UVS header: %w", r.err)
}
ranges := make([]cmap14DefaultRange, numRanges)
for i := range ranges {
start := r.u24()
additionalCount := r.u8()
ranges[i] = cmap14DefaultRange{start: start, count: uint32(additionalCount) + 1}
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 14 default UVS ranges: %w", r.err)
}
return ranges, nil
}
// parseUVSNonDefaultTable decodes a Non-Default UVS table at byte offset off
// within sub (the format-14 subtable, offsets are subtable-relative). The
// offset itself is only checked against sub's length here; a short read of
// the table's own fields is reported separately by the reader below.
func parseUVSNonDefaultTable(sub []byte, off int) ([]cmap14NonDefault, error) {
if off > len(sub) {
return nil, fmt.Errorf("opentype: cmap format 14 non-default UVS offset: %w", errTruncated)
}
r := reader{b: sub[off:]}
numMappings := int(r.u32())
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 14 non-default UVS header: %w", r.err)
}
maps := make([]cmap14NonDefault, numMappings)
for i := range maps {
maps[i] = cmap14NonDefault{unicode: r.u24(), glyph: GlyphIndex(r.u16())}
}
if r.err != nil {
return nil, fmt.Errorf("opentype: cmap format 14 non-default UVS mappings: %w", r.err)
}
return maps, nil
}
// lookupVariation resolves (r, vs) against c's records. A non-default
// mapping (an explicit glyph) takes precedence over a default-range match
// (which falls back to f's ordinary cmap); a vs with no record, or an r
// matched by neither its non-default mappings nor its default ranges,
// reports not-ok.
func (c *cmap14) lookupVariation(f *Font, r, vs rune) (GlyphIndex, bool) {
target := uint32(vs)
for _, rec := range c.records {
if rec.varSelector != target {
continue
}
cc := uint32(r)
for _, nd := range rec.nonDefault {
if nd.unicode == cc {
return nd.glyph, true
}
}
for _, dr := range rec.defaultRanges {
if cc >= dr.start && cc-dr.start < dr.count {
return f.GlyphIndex(r)
}
}
return 0, false
}
return 0, false
}
+163
View File
@@ -0,0 +1,163 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
// This file surfaces the font-level descriptor scalars a PDF FontDescriptor (and
// general typographic use) needs, without a consumer having to re-parse the sfnt
// container itself. The values are decoded from head (bounding box, macStyle),
// OS/2 (weight/width class, cap/x height, family class, selection flags) and post
// (italic angle, fixed-pitch) at Parse time; the accessors below expose them.
//
// Every scalar has a defined value even when its source table is absent: the
// head-derived ones are always available, while the OS/2 and post ones fall back
// to a documented default (0, or the ascender for a missing cap height) so a
// caller never has to special-case a partial font.
// parseDescriptor decodes the optional OS/2 and post tables into the descriptor
// scalars. Both are optional: a font without them keeps the zero defaults, which
// the accessors translate into sensible fallbacks. A too-short table is treated
// as absent rather than an error, matching the tolerant handling a descriptor
// consumer expects (the required metrics all come from head/hhea, already parsed).
func (f *Font) parseDescriptor(tables map[string][]byte) {
if b, ok := tables["OS/2"]; ok && len(b) >= 78 {
f.os2Present = true
f.os2Version = int(be16(b))
f.weightClass = int(be16(b[4:]))
f.widthClass = int(be16(b[6:]))
f.sFamilyClass = int(sbe16(b[30:]))
f.fsSelection = be16(b[62:])
// sxHeight and sCapHeight were added in OS/2 version 2; older tables (and
// truncated version-2 ones) simply do not carry them.
if f.os2Version >= 2 && len(b) >= 90 {
f.sxHeight = int(sbe16(b[86:]))
f.sCapHeight = int(sbe16(b[88:]))
}
}
if b, ok := tables["post"]; ok && len(b) >= 16 {
f.postPresent = true
f.italicAngle = float64(int32(be32(b[4:]))) / 65536.0
f.isFixedPitch = be32(b[12:]) != 0
}
}
// UnitsPerEm returns the font's design units per em (from head), the scale in
// which every other font-unit metric this package reports is expressed. It is
// never zero (Parse rejects a font whose head declares zero).
func (f *Font) UnitsPerEm() int { return f.unitsPerEm }
// Ascent returns the typographic ascender in font units (hhea.ascender): the
// height above the baseline. It is the same value Face.Metrics scales to pixels.
func (f *Font) Ascent() int { return f.ascender }
// Descent returns the typographic descender in font units (hhea.descender). It
// is conventionally negative (below the baseline), as stored in hhea.
func (f *Font) Descent() int { return f.descender }
// LineGap returns the typographic line gap in font units (hhea.lineGap): the
// recommended extra spacing between lines beyond ascent minus descent.
func (f *Font) LineGap() int { return f.lineGap }
// FontBBox returns the font bounding box in font units (head.xMin/yMin/xMax/yMax):
// the union of every glyph's bounds. A PDF FontDescriptor's /FontBBox is this box
// scaled to glyph space.
func (f *Font) FontBBox() (xMin, yMin, xMax, yMax int) {
return f.xMin, f.yMin, f.xMax, f.yMax
}
// CapHeight returns the cap height in font units (OS/2.sCapHeight). It is zero
// when the font has no OS/2 table or an OS/2 version below 2, which do not carry
// it; a PDF consumer that needs a non-zero value substitutes the ascender.
func (f *Font) CapHeight() int { return f.sCapHeight }
// XHeight returns the x height in font units (OS/2.sxHeight). It is zero when the
// font has no OS/2 table or an OS/2 version below 2 (which do not carry it).
func (f *Font) XHeight() int { return f.sxHeight }
// ItalicAngle returns the italic (slant) angle in counter-clockwise degrees from
// vertical (post.italicAngle): zero for an upright font, negative for the usual
// forward slant. It is zero when the font has no post table.
func (f *Font) ItalicAngle() float64 { return f.italicAngle }
// WeightClass returns the OS/2 usWeightClass (100 thin .. 400 normal .. 900 black),
// or 0 when the font carries no OS/2 table.
func (f *Font) WeightClass() int { return f.weightClass }
// WidthClass returns the OS/2 usWidthClass (1 ultra-condensed .. 5 normal ..
// 9 ultra-expanded), or 0 when the font carries no OS/2 table.
func (f *Font) WidthClass() int { return f.widthClass }
// IsFixedPitch reports whether the font is monospaced (post.isFixedPitch).
func (f *Font) IsFixedPitch() bool { return f.isFixedPitch }
// IsItalic reports whether the font is italic or oblique, from any of the three
// places a font may record it: a non-zero post italic angle, the head macStyle
// italic bit, or the OS/2 fsSelection italic bit.
func (f *Font) IsItalic() bool {
return f.italicAngle != 0 || f.macStyle&0x02 != 0 || f.fsSelection&0x01 != 0
}
// IsSerif reports whether the font is a serif design, read from the high byte of
// OS/2.sFamilyClass (the IBM font-class id): classes 1-7 are serif families,
// class 8 is sans-serif, and the rest (script, symbolic, ...) are neither. It is
// false when the font has no OS/2 table.
func (f *Font) IsSerif() bool {
class := f.sFamilyClass >> 8
return class >= 1 && class <= 7
}
// isSymbolic reports whether the font is a symbol font (OS/2 sFamilyClass 12).
// It drives the PDF Symbolic/Nonsymbolic flag: symbol fonts must be flagged
// Symbolic, every other font Nonsymbolic.
func (f *Font) isSymbolic() bool { return f.sFamilyClass>>8 == 12 }
// PDF FontDescriptor flag bits (PDF 32000-1:2008, Table 123).
const (
pdfFlagFixedPitch = 1 << 0
pdfFlagSerif = 1 << 1
pdfFlagSymbolic = 1 << 2
pdfFlagNonsymbolic = 1 << 5
pdfFlagItalic = 1 << 6
)
// Flags returns the PDF FontDescriptor /Flags value for the font: a bit set
// combining FixedPitch, Serif, Symbolic-or-Nonsymbolic and Italic, derived from
// the post, OS/2 and head tables. A caller embeds it verbatim as /Flags. Exactly
// one of the Symbolic and Nonsymbolic bits is always set.
func (f *Font) Flags() int {
flags := 0
if f.isFixedPitch {
flags |= pdfFlagFixedPitch
}
if f.IsSerif() {
flags |= pdfFlagSerif
}
if f.isSymbolic() {
flags |= pdfFlagSymbolic
} else {
flags |= pdfFlagNonsymbolic
}
if f.IsItalic() {
flags |= pdfFlagItalic
}
return flags
}
// StemV returns an estimate, in font units, of the dominant vertical stem width
// a PDF FontDescriptor requires as /StemV. OpenType fonts do not store StemV, so
// it is approximated from the OS/2 weight class with the linear relation
// 50 + (weightClass-100)*11/100 (about 83 at the normal weight 400, ~116 at bold
// 700), clamped to a floor of 50. A font without an OS/2 table is treated as the
// normal weight. Callers that can do better should read WeightClass directly.
func (f *Font) StemV() int {
w := f.weightClass
if w == 0 {
w = 400
}
v := 50 + (w-100)*11/100
if v < 50 {
v = 50
}
return v
}
+51
View File
@@ -0,0 +1,51 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
// Package opentype is a pure-Go, CGO=0, standard-library-only parser and
// anti-aliased rasteriser for TrueType/OpenType fonts.
//
// It is a functional replacement for the narrow slice of
// golang.org/x/image/font/opentype needed by a glyph-blitting UI: parse a
// font blob, build a Face at a pixel size, then obtain per-rune advances and
// 8-bit alpha coverage masks.
//
// # Scope
//
// Phase 1 covers TrueType 'glyf' outlines: the sfnt container, the head,
// maxp, hhea, hmtx, cmap (formats 4 and 12), loca and glyf tables, simple and
// composite glyphs, implied on-curve points, and a non-zero-winding
// supersampling rasteriser. OpenType/CFF ('OTTO') outlines, GPOS/GSUB
// shaping, kerning and hinting are not implemented; see the README for the
// full support matrix.
//
// The OpenType MATH table (math-typesetting metrics: constants, per-glyph
// italic correction and kerning, and stretchy-glyph size variants and
// assemblies) is decoded and exposed pixel-scaled through a Face; see
// [Font.HasMath] and math.go. Math layout itself (box building, the TeX math
// rules) is a higher-level engine that consumes these values.
//
// # Usage
//
// f, err := opentype.Parse(ttf)
// if err != nil { /* handle */ }
// face := f.NewFace(16)
// adv := face.Measure("Hello")
// bounds, mask, maskp, advance, ok := face.GlyphMask('H', penX, baselineY)
//
// A Font is immutable after Parse and safe for concurrent use; a Face caches
// rasterised glyphs and is not safe for concurrent use.
//
// # Embedding and subsetting
//
// For a consumer that embeds a font (a PDF or EPUB writer), the package exposes
// the primitives such an embedder otherwise re-implements by re-parsing the sfnt:
// the PDF FontDescriptor scalars ([Font.UnitsPerEm], [Font.FontBBox],
// [Font.CapHeight], [Font.XHeight], [Font.ItalicAngle], [Font.StemV],
// [Font.Flags], ...); raw table access ([Font.TableTags], [Font.Table]);
// by-glyph-index advances that honour the current variation
// ([Face.AdvanceIndex], [Face.AdvanceIndexUnits], [Face.VerticalAdvanceIndex]);
// TrueType glyph subsetting ([Font.SubsetTrueType]) and CFF charstring subsetting
// ([Font.SubsetCFF]); and baking a variable font at a chosen axis position into a
// static instance ([Font.Instance], [Font.InstanceBytes]).
package opentype
+358
View File
@@ -0,0 +1,358 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import (
"image"
"math"
)
// Metrics holds a Face's vertical metrics in whole pixels.
type Metrics struct {
Ascent int // baseline-to-top distance (a positive height above the baseline)
Descent int // baseline-to-bottom distance (a positive depth below the baseline)
Height int // line height: ascent + descent + line gap
}
// Face renders a Font at a fixed pixel size. It caches rasterised glyphs and
// is not safe for concurrent use; build one Face per goroutine if needed.
type Face struct {
font *Font
sizePx int
scale float64
cache map[GlyphIndex]cachedGlyph
// varCoords, when non-nil, instances a variable font's glyf outlines at the
// given user-space axis coordinates before rasterising (see SetVariation).
// varNorm caches those coordinates normalized (NormalizeCoords) so the
// metric-variation tables (HVAR/VVAR/MVAR) can be evaluated per call.
varCoords map[string]float64
varNorm []int16
// hinting enables the TrueType instruction interpreter for glyf glyphs that
// carry instructions (see SetHinting). interp is built lazily on first use
// and reused across glyphs; interpReady guards that one-time build.
hinting bool
interp *interp
interpReady bool
// stemDarken enables CFF/CFF2 stem darkening (weight compensation) when
// hinting is on: stems are embiggened by a small ppem-dependent amount to
// improve contrast on the anti-aliased rasteriser at small sizes (see
// SetStemDarkening). Off by default, so the default hinted output is
// byte-identical to the undarkened result.
stemDarken bool
}
// cachedGlyph is a rasterised glyph held in the Face cache. ok is false when
// the glyph could not be decoded (a corrupt outline), in which case it renders
// as nothing.
type cachedGlyph struct {
bounds image.Rectangle
mask *image.Alpha
ok bool
}
// roundInt rounds a float to the nearest integer (halves away from zero).
func roundInt(v float64) int { return int(math.Round(v)) }
// NewFace returns a Face that renders f at sizePx pixels per em. The scale
// factor is sizePx / unitsPerEm; sizePx should be positive.
func (f *Font) NewFace(sizePx int) *Face {
return &Face{
font: f,
sizePx: sizePx,
scale: float64(sizePx) / float64(f.unitsPerEm),
cache: map[GlyphIndex]cachedGlyph{},
}
}
// Font returns the Font this Face renders. It lets a shaper reach the font's
// layout tables ([Font.GSUB], [Font.GPOS]) and per-glyph metrics
// ([Font.GlyphAdvance]) while keeping the Face for pixel sizing.
func (fc *Face) Font() *Font { return fc.font }
// Scale returns the face's font-unit-to-pixel scale factor (the render size in
// pixels divided by the font's unitsPerEm). A shaper multiplies a font-unit
// advance or offset by it to obtain whole pixels at the face's size.
func (fc *Face) Scale() float64 { return fc.scale }
// Metrics returns the face's vertical metrics in pixels. When a variation is
// set and the font carries an MVAR table, the global metric deltas for the
// ascender ("hasc"), descender ("hdsc") and line gap ("hlgp") are folded in.
func (fc *Face) Metrics() Metrics {
s := fc.scale
asc := float64(fc.font.ascender)
desc := float64(fc.font.descender)
gap := float64(fc.font.lineGap)
if fc.varCoords != nil && fc.font.mvar != nil {
asc += fc.font.mvar.delta("hasc", fc.varNorm)
desc += fc.font.mvar.delta("hdsc", fc.varNorm)
gap += fc.font.mvar.delta("hlgp", fc.varNorm)
}
return Metrics{
Ascent: roundInt(asc * s),
Descent: roundInt(-desc * s),
Height: roundInt((asc - desc + gap) * s),
}
}
// hAdvanceUnits returns glyph gid's horizontal advance in font units at the
// face's current variation: the base advance plus, when a variation is set, the
// HVAR delta (or the gvar phantom-point fallback). With no variation set the
// result is exactly the base advance.
func (fc *Face) hAdvanceUnits(gid GlyphIndex) float64 {
adv := float64(fc.font.GlyphAdvance(gid))
if fc.varCoords != nil {
adv += fc.font.horizontalAdvanceDelta(int(gid), fc.varNorm)
}
return adv
}
// Advance returns the horizontal advance of r in pixels, or 0 if the rune is
// not mapped by the font's cmap. When a variation is set the HVAR delta (or the
// gvar phantom-point fallback) is included.
func (fc *Face) Advance(r rune) int {
gid, ok := fc.font.GlyphIndex(r)
if !ok {
return 0
}
return roundInt(fc.hAdvanceUnits(gid) * fc.scale)
}
// Measure returns the total advance width of s in pixels (the sum of each
// rune's Advance).
func (fc *Face) Measure(s string) int {
total := 0
for _, r := range s {
total += fc.Advance(r)
}
return total
}
// glyph returns the cached rasterisation of glyph gid, rendering it on first
// use.
func (fc *Face) glyph(gid GlyphIndex) cachedGlyph {
if cg, ok := fc.cache[gid]; ok {
return cg
}
cg := fc.render(gid)
fc.cache[gid] = cg
return cg
}
// render decodes and rasterises glyph gid at the face's size.
func (fc *Face) render(gid GlyphIndex) cachedGlyph {
contours, err := fc.outline(gid)
if err != nil {
return cachedGlyph{ok: false}
}
polys := make([][]fpoint, 0, len(contours))
for _, c := range contours {
p := flattenContour(c, fc.scale)
if len(p) >= 3 {
polys = append(polys, p)
}
}
bounds, mask := rasterize(polys)
return cachedGlyph{bounds: bounds, mask: mask, ok: true}
}
// GlyphMask rasterises r and positions it with (x, y) as the pen origin on the
// baseline. It returns the destination bounds, an *image.Alpha coverage mask,
// the offset into that mask corresponding to bounds.Min (always the origin),
// the advance width in pixels, and ok.
//
// ok is false when the rune is not mapped by the cmap or its glyph outline is
// corrupt; callers should render nothing in that case. A mapped-but-empty
// glyph (for example a space) returns ok true with a nil mask, an empty
// bounds, and its advance.
func (fc *Face) GlyphMask(r rune, x, y int) (bounds image.Rectangle, mask *image.Alpha, maskp image.Point, advance int, ok bool) {
gid, mapped := fc.font.GlyphIndex(r)
if !mapped {
return image.Rectangle{}, nil, image.Point{}, 0, false
}
cg := fc.glyph(gid)
if !cg.ok {
return image.Rectangle{}, nil, image.Point{}, 0, false
}
advance = roundInt(fc.hAdvanceUnits(gid) * fc.scale)
bounds = cg.bounds.Add(image.Point{X: x, Y: y})
return bounds, cg.mask, image.Point{}, advance, true
}
// GlyphMaskIndex rasterises the glyph with index gid directly — bypassing the
// cmap — and positions it with (x, y) as the pen origin on the baseline. It is
// the by-glyph-index counterpart of [Face.GlyphMask]: a complex-text shaper
// (see github.com/go-opentype/shape) emits glyph indices after applying GSUB
// substitutions and GPOS positioning, so the runes have already been resolved
// to glyphs and a caller must render each glyph by its index rather than by a
// rune. Callers add the shaper's per-glyph pixel offset to (x, y) before the
// call and advance the pen by the shaper's advance afterwards.
//
// It returns the destination bounds, an *image.Alpha coverage mask, the offset
// into that mask corresponding to bounds.Min (always the origin), the glyph's
// own horizontal advance width in pixels, and ok.
//
// ok is false when gid is out of range or its outline is corrupt; callers
// should render nothing in that case. A valid-but-empty glyph (for example a
// space) returns ok true with a nil mask, an empty bounds, and its advance.
func (fc *Face) GlyphMaskIndex(gid GlyphIndex, x, y int) (bounds image.Rectangle, mask *image.Alpha, maskp image.Point, advance int, ok bool) {
cg := fc.glyph(gid)
if !cg.ok {
return image.Rectangle{}, nil, image.Point{}, 0, false
}
advance = roundInt(fc.hAdvanceUnits(gid) * fc.scale)
bounds = cg.bounds.Add(image.Point{X: x, Y: y})
return bounds, cg.mask, image.Point{}, advance, true
}
// outline returns glyph gid's outline as contours in font units, selecting the
// outline source (CFF2 vs CFF vs glyf), applying variable-font instancing when a
// variation is set, and applying hinting when it is enabled.
//
// A CFF2 font is inherently variable: its outlines are instanced at the face's
// variation coordinates (the default master when none is set), converted to the
// normalized F2Dot14 form cff2.outline expects via NormalizeCoords. When hinting
// is enabled both CFF2 and CFF1 outlines are grid-fitted (see cffhint.go), so
// SetHinting affects all three outline flavours; with hinting off they are
// returned unhinted.
func (fc *Face) outline(gid GlyphIndex) ([]contour, error) {
if fc.font.t1 != nil {
// A Type 1 program carries hints of its own, in a form nothing here
// grid-fits; its outlines are drawn as they were written.
return fc.font.t1.outline(int(gid))
}
if fc.font.cff2 != nil {
contours, gh, err := fc.font.cff2.outlineHints(int(gid), fc.font.NormalizeCoords(fc.varCoords))
if err != nil {
return nil, err
}
if fc.hinting {
contours = cffGridFit(contours, gh, fc.scale, float64(fc.sizePx), fc.stemDarken)
}
return contours, nil
}
if fc.font.cff != nil {
contours, gh, err := fc.font.cff.outlineHints(int(gid))
if err != nil {
return nil, err
}
if fc.hinting {
contours = cffGridFit(contours, gh, fc.scale, float64(fc.sizePx), fc.stemDarken)
}
return contours, nil
}
var (
contours []contour
err error
)
if fc.varCoords != nil {
contours, err = fc.font.InstancePoints(int(gid), fc.varCoords)
} else {
contours, err = fc.font.glyphContours(gid)
}
if err != nil {
return nil, err
}
if fc.hinting {
contours = fc.hintedOutline(gid, contours)
}
return contours, nil
}
// SetVariation instances the font at the user-space axis coordinates coords
// (keyed by axis tag, e.g. {"wght": 700}); axes absent from coords take their
// default. Subsequent GlyphMask calls reflect the instanced outlines, for both
// glyf (via gvar) and CFF2 (via its blend/vsindex operators) variable fonts.
// Pass nil to return to the default master. A non-variable font (or a CFF1 font,
// which has no variation) is unaffected. The glyph cache is invalidated so the
// change takes effect at once.
func (fc *Face) SetVariation(coords map[string]float64) {
fc.varCoords = coords
fc.varNorm = fc.font.NormalizeCoords(coords)
fc.cache = map[GlyphIndex]cachedGlyph{}
}
// SetHinting enables or disables the TrueType instruction interpreter. When
// enabled, a glyf glyph that carries instructions is grid-fitted at the face's
// pixel size before rasterising; glyphs without instructions, composite glyphs
// and CFF fonts are unaffected. Hinting is off by default (the default output is
// unhinted anti-aliased coverage). The glyph cache is invalidated.
func (fc *Face) SetHinting(on bool) {
fc.hinting = on
fc.cache = map[GlyphIndex]cachedGlyph{}
}
// SetStemDarkening enables or disables stem darkening (weight compensation) for
// CFF/CFF2 glyphs. When both hinting and darkening are on, every grid-fitted stem
// is embiggened by a small ppem-dependent amount (strongest at small sizes,
// fading to none at larger sizes) so stems keep enough contrast on the
// anti-aliased rasteriser at small text sizes, as FreeType's CFF engine does by
// default. It has no effect while hinting is off or on glyf (TrueType) outlines.
// Darkening is off by default, so the default hinted output is byte-identical to
// the undarkened result. The glyph cache is invalidated.
func (fc *Face) SetStemDarkening(on bool) {
fc.stemDarken = on
fc.cache = map[GlyphIndex]cachedGlyph{}
}
// hintInterp lazily builds the interpreter for this face (running the font
// program and control-value program once) and reuses it across glyphs. It
// returns nil when the font has no interpreter or its programs fail to run, in
// which case the caller renders the glyph unhinted.
func (fc *Face) hintInterp() *interp {
if !fc.interpReady {
fc.interpReady = true
if it, err := newInterp(fc.font, fc.sizePx); err == nil {
fc.interp = it
}
}
return fc.interp
}
// hintedOutline grid-fits glyph gid's contours through the TrueType interpreter
// and returns the hinted contours in font units. It falls back to the input
// contours when the glyph carries no instructions, the interpreter is
// unavailable, or the program fails. The whole outline is treated as one closed
// contour for IUP, matching the interpreter's model (see hint.go).
func (fc *Face) hintedOutline(gid GlyphIndex, contours []contour) []contour {
instr := fc.font.glyphInstructions(gid)
if len(instr) == 0 {
return contours
}
it := fc.hintInterp()
if it == nil {
return contours
}
// Scale to pixel space (what the interpreter expects), recording contour
// boundaries so the hinted points can be re-split into contours.
var pts []outlinePoint
ends := make([]int, 0, len(contours))
for _, c := range contours {
for _, p := range c {
pts = append(pts, outlinePoint{x: p.x * fc.scale, y: p.y * fc.scale, on: p.on})
}
ends = append(ends, len(pts)-1)
}
out, err := it.run(pts, instr)
if err != nil {
return contours
}
// Back to font units so the shared rasteriser (which re-applies scale)
// reproduces the hinted pixel positions.
result := make([]contour, len(contours))
start := 0
for i, e := range ends {
cc := make(contour, e-start+1)
for j := range cc {
p := out[start+j]
cc[j] = outlinePoint{x: p.x / fc.scale, y: p.y / fc.scale, on: p.on}
}
result[i] = cc
start = e + 1
}
return result
}
+345
View File
@@ -0,0 +1,345 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import (
"encoding/binary"
"errors"
"fmt"
)
// GlyphIndex is a glyph identifier within a font: an index into the font's
// glyph store, as produced by the cmap. Zero is the ".notdef" glyph.
type GlyphIndex uint16
// errTruncated is the sentinel a bounded reader records when a read runs past
// the end of its slice. Callers wrap it with table context.
var errTruncated = errors.New("unexpected end of data")
// Font is a parsed TrueType/OpenType font. It is immutable after Parse and
// safe for concurrent use by multiple goroutines. Build a Face from it with
// NewFace to obtain sized metrics and rasterised glyphs.
type Font struct {
unitsPerEm int
indexToLocFormat int
numGlyphs int
ascender int
descender int
lineGap int
numberOfHMetrics int
advances []int // advanceWidth per glyph, in font units
lsbs []int // left side bearing per glyph, in font units
loca []uint32
glyf []byte
cmap cmapLookup
cmaps []cmapSubtable // every decoded cmap subtable, with the platform and encoding it was written for
cmapVS *cmap14 // format-14 Unicode Variation Sequences subtable, if present
cff *cffTable // CFF/Type2 outlines for an OpenType ("OTTO") font, if present
t1 *type1Font // PostScript Type 1 outlines, for a program read by ParseType1
glyphNames map[string]GlyphIndex // what the font calls its glyphs, when it names them
cff2 *cff2Table // CFF2 (variable Compact Font Format) outlines, if present
fvar *fvarTable // optional: variation axes and named instances
avar *avarTable // optional: axis-value segment maps
gvar *gvarTable // optional: glyph variation (delta) data
hvar *hvarTable // optional: horizontal metrics variation (advances)
vvar *vvarTable // optional: vertical metrics variation (advances)
mvar *mvarTable // optional: global font-metric variation
// OpenType Layout tables, all optional. Absence is not an error.
gdef *gdefTable // glyph definitions (classes, mark sets) for lookup flags
gsub *gsub // glyph substitution (ligatures, single substitution)
gpos *gpos // glyph positioning (pair kerning)
kern *kern // legacy kern table (kerning fallback)
// OpenType MATH table (math-typesetting metrics), optional. Absence is not an
// error; it powers Font.HasMath and the Face math accessors (see math.go).
math *mathTable
// Vertical writing-mode metrics (vhea/vmtx/VORG), all optional. They enable
// top-to-bottom (CJK tategaki) layout; absence means vertical metrics are
// unavailable. See vertical.go.
vertAscender int // vhea vertTypoAscender, font units
vertDescender int // vhea vertTypoDescender, font units
vertLineGap int // vhea vertTypoLineGap, font units
numOfLongVerMetrics int // vhea count of full vmtx entries
vertAdvances []int // advanceHeight per glyph, font units (vmtx)
tsbs []int // top side bearing per glyph, font units (vmtx)
vorgDefault int // VORG defaultVertOriginY, font units
vorg map[GlyphIndex]int // VORG per-glyph vertical origin overrides
hasVhea bool
hasVmtx bool
hasVORG bool
// TrueType instruction (hinting) tables and limits. These are optional:
// a font without them simply cannot be hinted. See hint.go.
fpgm []byte // font program (function definitions)
prep []byte // control-value program
cvt []int16
maxStorage int
maxFunctionDefs int
maxStackElements int
maxTwilightPts int
// Descriptor scalars decoded from head, OS/2 and post at Parse time. They
// back the PDF FontDescriptor accessors and general layout use (descriptor.go).
xMin, yMin, xMax, yMax int // head glyph bounding box, font units
macStyle uint16 // head macStyle bits (bit 0 bold, bit 1 italic)
os2Present bool // an OS/2 table was present and long enough to decode
os2Version int // OS/2 table version (governs the availability of x/CapHeight)
weightClass int // OS/2 usWeightClass (100..900), 0 when absent
widthClass int // OS/2 usWidthClass (1..9), 0 when absent
sFamilyClass int // OS/2 sFamilyClass (high byte is the IBM font class)
fsSelection uint16 // OS/2 fsSelection bits (bit 0 italic)
sCapHeight int // OS/2 sCapHeight, font units (version >= 2), 0 when absent
sxHeight int // OS/2 sxHeight, font units (version >= 2), 0 when absent
postPresent bool // a post table was present and long enough to decode
italicAngle float64 // post italicAngle (degrees, 0 for upright)
isFixedPitch bool // post isFixedPitch (monospaced)
// tables retains the raw, undecoded sfnt table slices keyed by four-byte tag,
// so callers can read table bytes (Font.Table/Font.TableTags) and the
// subsetting/instancing code can rebuild a container (subset.go, instance.go).
tables map[string][]byte
}
// be16 reads a big-endian uint16 from b[0:2]; the caller guarantees len(b)>=2.
func be16(b []byte) uint16 { return binary.BigEndian.Uint16(b) }
// be32 reads a big-endian uint32 from b[0:4]; the caller guarantees len(b)>=4.
func be32(b []byte) uint32 { return binary.BigEndian.Uint32(b) }
// sbe16 reads a big-endian int16 from b[0:2]; the caller guarantees len(b)>=2.
func sbe16(b []byte) int16 { return int16(binary.BigEndian.Uint16(b)) }
// Recognised sfnt version tags.
const (
versionTrueType = 0x00010000 // 'glyf' outlines
versionTrue = 0x74727565 // "true", Apple TrueType
versionOTTO = 0x4F54544F // "OTTO", CFF/OpenType outlines (phase 2)
)
// Parse decodes a TrueType/OpenType font from b and returns a Font. The byte
// slice is retained (not copied) and must not be mutated by the caller.
//
// It fails on a corrupt or unsupported container: a bad sfnt magic, truncated
// data, or a missing required table — which does not include the character
// map: see [Font.HasCharacterMap]. All three outline flavours are supported:
// TrueType ('glyf'/'loca'), CFF/OpenType (a "OTTO" sfnt, or any sfnt carrying a
// 'CFF ' table, decoded via cff.go) and variable CFF2 (a sfnt carrying a 'CFF2'
// table, decoded via cff2.go).
func Parse(b []byte) (*Font, error) {
if len(b) < 12 {
return nil, fmt.Errorf("opentype: short header: %w", errTruncated)
}
version := be32(b)
switch version {
case versionTrueType, versionTrue, versionOTTO:
// supported: TrueType ('glyf') and CFF/OpenType ("OTTO") outlines.
default:
return nil, fmt.Errorf("opentype: bad sfnt version 0x%08x", version)
}
numTables := int(be16(b[4:]))
tables := make(map[string][]byte, numTables)
// The table directory is numTables records of 16 bytes starting at 12.
if 12+numTables*16 > len(b) {
return nil, fmt.Errorf("opentype: table directory: %w", errTruncated)
}
for i := 0; i < numTables; i++ {
rec := b[12+i*16:]
tag := string(rec[0:4])
off := int(be32(rec[8:]))
length := int(be32(rec[12:]))
if off+length > len(b) {
return nil, fmt.Errorf("opentype: table %q out of range", tag)
}
tables[tag] = b[off : off+length]
}
// The outline flavour is CFF/CFF2 when the sfnt is "OTTO" or carries a 'CFF '
// or 'CFF2' table; such a font needs its CFF table but not 'glyf'/'loca'. A
// 'CFF2' table (variable CFF) takes precedence and requires 'CFF2'; otherwise
// a CFF font requires 'CFF '. A TrueType font needs 'glyf'/'loca'.
_, hasCFF := tables["CFF "]
_, hasCFF2 := tables["CFF2"]
isCFF := version == versionOTTO || hasCFF || hasCFF2
// A 'cmap' is not among them. A font embedded in a PDF as a CIDFontType2
// is addressed by glyph number through the document's own map, and the
// subset that is embedded routinely leaves its character map out; refusing
// such a font would leave every glyph in it undrawable for want of a table
// nothing was going to consult.
required := []string{"head", "maxp", "hhea", "hmtx"}
switch {
case hasCFF2:
required = append(required, "CFF2")
case isCFF:
required = append(required, "CFF ")
default:
required = append(required, "loca", "glyf")
}
for _, name := range required {
if _, ok := tables[name]; !ok {
return nil, fmt.Errorf("opentype: missing required table %q", name)
}
}
f := &Font{tables: tables}
if err := f.parseHead(tables["head"]); err != nil {
return nil, err
}
if err := f.parseMaxp(tables["maxp"]); err != nil {
return nil, err
}
if err := f.parseHhea(tables["hhea"]); err != nil {
return nil, err
}
if err := f.parseHmtx(tables["hmtx"]); err != nil {
return nil, err
}
switch {
case hasCFF2:
c2, err := parseCFF2(tables["CFF2"])
if err != nil {
return nil, err
}
f.cff2 = c2
case isCFF:
cff, err := parseCFF(tables["CFF "])
if err != nil {
return nil, err
}
f.cff = cff
default:
if err := f.parseLoca(tables["loca"]); err != nil {
return nil, err
}
f.glyf = tables["glyf"]
}
if cm, ok := tables["cmap"]; ok {
if err := f.parseCmap(cm); err != nil {
return nil, err
}
}
// Optional TrueType instruction tables. Absence is not an error; it just
// means the font carries no hinting program (see hint.go).
f.fpgm = tables["fpgm"]
f.prep = tables["prep"]
f.parseCvt(tables["cvt "])
// Optional OpenType Font Variations tables. A font without them parses and
// renders exactly as before; when present they enable instancing at a
// chosen axis coordinate (see fvar.go, avar.go, gvar.go).
if err := f.parseVariations(tables); err != nil {
return nil, err
}
// Optional metric-variation tables (HVAR, VVAR, MVAR): advance widths,
// advance heights and global font metrics that track the axis coordinate.
// Absence is not an error (see metricvar.go).
if err := f.parseMetricVariations(tables); err != nil {
return nil, err
}
// Optional OpenType Layout tables: substitution (GSUB), positioning (GPOS)
// and the legacy kern table. Absence is not an error (see gsub.go, gpos.go,
// kern.go); they power Face.Shape and Face.Kern.
if err := f.parseLayout(tables); err != nil {
return nil, err
}
// Optional vertical writing-mode tables (vhea, vmtx, VORG). Absence is not an
// error; they supply the metrics for top-to-bottom layout (see vertical.go).
if err := f.parseVertical(tables); err != nil {
return nil, err
}
// Optional OpenType MATH table: the font-level metrics a math-typesetting
// engine consumes. Absence is not an error (see math.go).
if err := f.parseMath(tables); err != nil {
return nil, err
}
// Optional descriptor tables (OS/2, post): the metadata a PDF FontDescriptor
// and general styling consume. Absence is not an error (see descriptor.go).
f.parseDescriptor(tables)
return f, nil
}
// parseLayout decodes the optional GSUB, GPOS and kern tables. Each is
// independent; any subset (or none) may be present, and a malformed table is
// reported as an error so a corrupt font fails cleanly.
func (f *Font) parseLayout(tables map[string][]byte) error {
// GDEF is parsed first so its glyph classes and mark sets can be shared with
// the GSUB and GPOS appliers, which honour it through their lookup flags.
if b, ok := tables["GDEF"]; ok {
gd, err := parseGDEF(b)
if err != nil {
return err
}
f.gdef = gd
}
if b, ok := tables["GSUB"]; ok {
g, err := parseGSUB(b)
if err != nil {
return err
}
g.gdef = f.gdef
f.gsub = g
}
if b, ok := tables["GPOS"]; ok {
g, err := parseGPOS(b)
if err != nil {
return err
}
g.gdef = f.gdef
f.gpos = g
}
if b, ok := tables["kern"]; ok {
k, err := parseKern(b)
if err != nil {
return err
}
f.kern = k
}
return nil
}
// parseCvt decodes the Control Value Table: a run of int16 FUnit values. A
// missing or odd-length table yields as many whole entries as fit.
func (f *Font) parseCvt(b []byte) {
f.cvt = make([]int16, len(b)/2)
for i := range f.cvt {
f.cvt[i] = sbe16(b[i*2:])
}
}
// NumGlyphs returns the number of glyphs in the font (from the maxp table).
func (f *Font) NumGlyphs() int { return f.numGlyphs }
// GlyphIndex maps a rune to its glyph index via the selected cmap subtable.
// ok is false when the rune has no glyph in that subtable, and for a font that
// carries no character map at all — one addressed by glyph number, which
// [Font.HasCharacterMap] reports.
func (f *Font) GlyphIndex(r rune) (GlyphIndex, bool) {
if f.cmap == nil {
return 0, false
}
return f.cmap.lookup(r)
}
// HasCharacterMap reports whether the font can say which glyph a rune is. A
// font subset embedded in a PDF as a CIDFontType2 usually cannot: the document
// carries that map instead, and the font is addressed by glyph number.
func (f *Font) HasCharacterMap() bool { return f.cmap != nil }
// GlyphIndexVariation resolves a Unicode variation sequence (a base rune
// followed by a variation selector) to a glyph index, using the font's
// format-14 cmap subtable.
//
// ok is false when the font has no format-14 subtable, the variation
// selector is not registered in it, or the sequence is registered as a
// "default" mapping whose base rune has no glyph in the ordinary cmap.
func (f *Font) GlyphIndexVariation(r, vs rune) (GlyphIndex, bool) {
if f.cmapVS == nil {
return 0, false
}
return f.cmapVS.lookupVariation(f, r, vs)
}
+213
View File
@@ -0,0 +1,213 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import (
"fmt"
"math"
)
// This file implements the 'fvar' (Font Variations) table: the declaration of
// a variable font's design-variation axes and its named instances. Together
// with 'avar' (avar.go) and 'gvar' (gvar.go) it lets a variable font be
// instantiated at a chosen point in its design space instead of only at its
// default master.
//
// The three tables are parsed as optional add-ons: a font without them is a
// plain static font and is unaffected. Metric variations (HVAR/VVAR/MVAR) are
// implemented separately in metricvar.go; CFF2 outline variation lives in
// cff2.go. Deferred (not implemented here): style-attribute variations (STAT).
// Axis is one design-variation axis of a variable font, decoded from the
// 'fvar' table. Min, Default and Max are user-space coordinates (for example
// 100, 400 and 900 for a typical weight axis).
type Axis struct {
Tag string // four-character axis tag, e.g. "wght", "wdth", "slnt"
Min float64 // minimum user coordinate
Default float64 // default user coordinate (the font's default master)
Max float64 // maximum user coordinate
Flags uint16 // axis flags (bit 0: hidden axis)
NameID uint16 // 'name' table entry describing the axis
}
// NamedInstance is a named position in the variation space (for example
// "Bold" or "Condensed"), decoded from the 'fvar' table.
type NamedInstance struct {
SubfamilyNameID uint16 // 'name' entry for the instance subfamily
Flags uint16 // instance flags (reserved)
Coordinates map[string]float64 // axis tag -> user coordinate
PostScriptNameID uint16 // 'name' entry for the PostScript name, 0 if absent
}
// fvarTable is the parsed 'fvar' table.
type fvarTable struct {
axes []Axis
instances []NamedInstance
}
// parseVariations parses the optional variation tables. Each is independent:
// any subset (or none) may be present. A malformed table is reported as an
// error so a corrupt font fails cleanly rather than silently mis-rendering.
func (f *Font) parseVariations(tables map[string][]byte) error {
if b, ok := tables["fvar"]; ok {
if err := f.parseFvar(b); err != nil {
return err
}
}
if b, ok := tables["avar"]; ok {
if err := f.parseAvar(b); err != nil {
return err
}
}
if b, ok := tables["gvar"]; ok {
if err := f.parseGvar(b); err != nil {
return err
}
}
return nil
}
// readTag reads a four-byte OpenType tag as a string.
func readTag(r *reader) string {
if r.pos+4 > len(r.b) {
r.err = errTruncated
return ""
}
s := string(r.b[r.pos : r.pos+4])
r.pos += 4
return s
}
// readFixed reads a 16.16 signed fixed-point value as a float64.
func readFixed(r *reader) float64 {
return float64(int32(r.u32())) / 65536.0
}
// parseFvar decodes the 'fvar' table: a header, an array of axis records and
// an array of named-instance records. The header carries the offset to the
// axis array and the (forward-compatible) record sizes, which are honoured so
// that unknown trailing fields are skipped.
func (f *Font) parseFvar(b []byte) error {
r := reader{b: b}
r.u16() // majorVersion
r.u16() // minorVersion
axesOffset := int(r.u16())
r.u16() // reserved
axisCount := int(r.u16())
axisSize := int(r.u16())
instanceCount := int(r.u16())
instanceSize := int(r.u16())
if r.err != nil {
return fmt.Errorf("opentype: fvar table: %w", r.err)
}
axes := make([]Axis, axisCount)
for i := range axes {
ar := reader{b: b, pos: axesOffset + i*axisSize}
tag := readTag(&ar)
min := readFixed(&ar)
def := readFixed(&ar)
max := readFixed(&ar)
flags := ar.u16()
nameID := ar.u16()
if ar.err != nil {
return fmt.Errorf("opentype: fvar axis %d: %w", i, ar.err)
}
axes[i] = Axis{Tag: tag, Min: min, Default: def, Max: max, Flags: flags, NameID: nameID}
}
instances := make([]NamedInstance, instanceCount)
for i := range instances {
ir := reader{b: b, pos: axesOffset + axisCount*axisSize + i*instanceSize}
sub := ir.u16()
flags := ir.u16()
coords := make(map[string]float64, axisCount)
for a := 0; a < axisCount; a++ {
coords[axes[a].Tag] = readFixed(&ir)
}
var psID uint16
if instanceSize >= axisCount*4+6 {
psID = ir.u16()
}
if ir.err != nil {
return fmt.Errorf("opentype: fvar instance %d: %w", i, ir.err)
}
instances[i] = NamedInstance{
SubfamilyNameID: sub,
Flags: flags,
Coordinates: coords,
PostScriptNameID: psID,
}
}
f.fvar = &fvarTable{axes: axes, instances: instances}
return nil
}
// Axes returns the font's variation axes, or nil for a non-variable font.
func (f *Font) Axes() []Axis {
if f.fvar == nil {
return nil
}
return f.fvar.axes
}
// NamedInstances returns the font's named instances, or nil if the font is not
// variable or declares none.
func (f *Font) NamedInstances() []NamedInstance {
if f.fvar == nil {
return nil
}
return f.fvar.instances
}
// NormalizeCoords converts a set of user-space axis coordinates (keyed by axis
// tag) into per-axis normalized F2Dot14 values in the range [-1, 1], the form
// gvar and avar operate in. Axes not present in user take their default (which
// normalizes to 0). Values outside an axis's [min, max] are clamped. If the
// font declares an 'avar' table its segment maps are applied. The result has
// one entry per fvar axis, in axis order; it is nil for a non-variable font.
func (f *Font) NormalizeCoords(user map[string]float64) []int16 {
if f.fvar == nil {
return nil
}
axes := f.fvar.axes
out := make([]int16, len(axes))
for i, ax := range axes {
v := ax.Default
if u, ok := user[ax.Tag]; ok {
v = u
}
if v < ax.Min {
v = ax.Min
}
if v > ax.Max {
v = ax.Max
}
// v is clamped to [min, max]; whenever v < default the axis has
// default > min, and whenever v > default it has max > default, so the
// divisors below are always positive. v == default yields n == 0.
var n float64
switch {
case v < ax.Default:
n = (v - ax.Default) / (ax.Default - ax.Min)
case v > ax.Default:
n = (v - ax.Default) / (ax.Max - ax.Default)
}
if f.avar != nil {
n = f.avar.apply(i, n)
}
if n < -1 {
n = -1
}
if n > 1 {
n = 1
}
out[i] = int16(math.Round(n * 16384))
}
return out
}
+263
View File
@@ -0,0 +1,263 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import "fmt"
// This file decodes the GDEF (Glyph Definition) table and implements the
// OpenType Layout lookup flags that GSUB and GPOS honour during matching. GDEF
// classifies glyphs (base / ligature / mark / component), assigns marks to
// attachment classes and defines mark glyph sets; the lookupFlag of each lookup
// then selects, via those classifications, which glyphs a lookup skips as it
// walks the run. Without a GDEF table (or with a zero lookupFlag) nothing is
// skipped and matching is identical to a plain contiguous walk.
//
// The flag bits (OpenType "LookupFlag") are:
//
// - RightToLeft (0x0001): cursive attachment joins right-to-left.
// - IgnoreBaseGlyphs (0x0002): skip GDEF class 1 (base) glyphs.
// - IgnoreLigatures (0x0004): skip GDEF class 2 (ligature) glyphs.
// - IgnoreMarks (0x0008): skip GDEF class 3 (mark) glyphs.
// - UseMarkFilteringSet (0x0010): skip marks absent from the referenced
// mark glyph set (its index is the lookup's markFilteringSet field).
// - MarkAttachmentType (0xFF00): skip marks whose GDEF mark-attachment class
// differs from the type in the flag's high byte.
// lookupFlag bit masks.
const (
flagRightToLeft = 0x0001
flagIgnoreBaseGlyphs = 0x0002
flagIgnoreLigatures = 0x0004
flagIgnoreMarks = 0x0008
flagUseMarkFilteringSet = 0x0010
flagMarkAttachTypeMask = 0xFF00
)
// GDEF GlyphClassDef class values.
const (
gdefClassBase = 1 // simple glyph (a letter)
gdefClassLigature = 2 // ligature glyph (multiple combined glyphs)
gdefClassMark = 3 // combining mark (diacritic)
gdefClassComponent = 4 // ligature component (part of a ligature)
)
// gdefTable is a decoded GDEF table. All three maps/slices are optional (an
// absent subtable yields an empty map or nil slice); a font may carry a GDEF
// table with only some of them present.
type gdefTable struct {
glyphClass map[GlyphIndex]int // GlyphClassDef: base/ligature/mark/component
markAttach map[GlyphIndex]int // MarkAttachClassDef: mark-attachment class per mark
markGlyphSets []map[GlyphIndex]int // MarkGlyphSets: one Coverage per set
}
// parseGDEF decodes a GDEF table. AttachList and LigCaretList are tolerated but
// not decoded (they carry hinting/caret data GSUB/GPOS matching does not need).
func parseGDEF(b []byte) (*gdefTable, error) {
r := reader{b: b}
r.skip(2) // majorVersion
minor := r.u16()
glyphClassOff := int(r.u16())
r.skip(2) // attachListOffset (tolerated, not decoded)
r.skip(2) // ligCaretListOffset (tolerated, not decoded)
markAttachOff := int(r.u16())
markSetsOff := 0
if minor >= 2 {
markSetsOff = int(r.u16())
}
// minor >= 3 adds an itemVarStore Offset32 here; it is not needed for
// matching and is left unread.
if r.err != nil {
return nil, fmt.Errorf("opentype: gdef header: %w", r.err)
}
t := &gdefTable{glyphClass: map[GlyphIndex]int{}, markAttach: map[GlyphIndex]int{}}
if glyphClassOff != 0 {
cd, err := parseClassDef(subslice(b, glyphClassOff))
if err != nil {
return nil, err
}
t.glyphClass = cd
}
if markAttachOff != 0 {
cd, err := parseClassDef(subslice(b, markAttachOff))
if err != nil {
return nil, err
}
t.markAttach = cd
}
if markSetsOff != 0 {
sets, err := parseMarkGlyphSets(subslice(b, markSetsOff))
if err != nil {
return nil, err
}
t.markGlyphSets = sets
}
return t, nil
}
// parseMarkGlyphSets decodes a MarkGlyphSetsDef table: a format, a set count and
// that many Offset32 to Coverage tables (from the start of b).
func parseMarkGlyphSets(b []byte) ([]map[GlyphIndex]int, error) {
r := reader{b: b}
r.skip(2) // format (1)
n := int(r.u16())
offs := make([]int, n)
for i := 0; i < n; i++ {
offs[i] = int(r.u32())
}
if r.err != nil {
return nil, fmt.Errorf("opentype: markGlyphSets: %w", r.err)
}
sets := make([]map[GlyphIndex]int, n)
for i, off := range offs {
cov, err := parseCoverage(subslice(b, off))
if err != nil {
return nil, err
}
sets[i] = cov
}
return sets, nil
}
// markSetCovers reports whether mark glyph g is a member of mark glyph set i.
func (t *gdefTable) markSetCovers(i int, g GlyphIndex) bool {
if i < 0 || i >= len(t.markGlyphSets) {
return false
}
_, ok := t.markGlyphSets[i][g]
return ok
}
// skipper decides which glyphs a lookup ignores while matching, from the
// lookup's flag (and mark-filtering-set index) against the font's GDEF table. A
// zero-value skipper (no GDEF) skips nothing, so matching degrades to a plain
// contiguous walk and behaviour is identical to a font without GDEF.
type skipper struct {
gdef *gdefTable
flag uint16
markFilterSet uint16
}
// rtl reports whether the lookup's RightToLeft flag is set (consulted by
// cursive attachment).
func (sk skipper) rtl() bool { return sk.flag&flagRightToLeft != 0 }
// isMark reports whether g is a GDEF mark glyph (class 3). It is false without a
// GDEF table, so mark-aware base finding degrades to the plain search.
func (sk skipper) isMark(g GlyphIndex) bool {
return sk.gdef != nil && sk.gdef.glyphClass[g] == gdefClassMark
}
// skip reports whether glyph g is ignored by this lookup: a base/ligature/mark
// the corresponding Ignore flag suppresses, or a mark filtered out by the
// mark-attachment type or the mark-filtering set.
func (sk skipper) skip(g GlyphIndex) bool {
if sk.gdef == nil {
return false
}
class := sk.gdef.glyphClass[g]
if sk.flag&flagIgnoreBaseGlyphs != 0 && class == gdefClassBase {
return true
}
if sk.flag&flagIgnoreLigatures != 0 && class == gdefClassLigature {
return true
}
if sk.flag&flagIgnoreMarks != 0 && class == gdefClassMark {
return true
}
if class == gdefClassMark {
if sk.flag&flagUseMarkFilteringSet != 0 {
return !sk.gdef.markSetCovers(int(sk.markFilterSet), g)
}
if attach := sk.flag & flagMarkAttachTypeMask; attach != 0 {
return sk.gdef.markAttach[g] != int(attach>>8)
}
}
return false
}
// next returns the index of the first non-skipped glyph at position >= from, or
// len(g) when none remains.
func (sk skipper) next(g []GlyphIndex, from int) int {
for from < len(g) && sk.skip(g[from]) {
from++
}
return from
}
// prev returns the index of the first non-skipped glyph at position <= from, or
// -1 when none remains.
func (sk skipper) prev(g []GlyphIndex, from int) int {
for from >= 0 && sk.skip(g[from]) {
from--
}
return from
}
// glyphPred tests one glyph during sequence matching (by glyph id, class or
// coverage membership, depending on the sub-format).
type glyphPred func(GlyphIndex) bool
// matchForward matches preds against successive non-skipped positions starting
// at the first non-skipped glyph at or after `from`. It returns the matched
// absolute positions, the index just past the last match (where a following
// lookahead search begins), and whether every predicate matched. An empty preds
// list matches trivially at `from`.
func (sk skipper) matchForward(g []GlyphIndex, from int, preds []glyphPred) (pos []int, end int, ok bool) {
pos = make([]int, len(preds))
p := from
for k, pred := range preds {
p = sk.next(g, p)
if p >= len(g) || !pred(g[p]) {
return nil, 0, false
}
pos[k] = p
p++
}
return pos, p, true
}
// matchBackward matches preds against successive non-skipped positions walking
// backward from `from` (used for backtrack sequences, which are stored in the
// reverse of text order).
func (sk skipper) matchBackward(g []GlyphIndex, from int, preds []glyphPred) bool {
p := from
for _, pred := range preds {
p = sk.prev(g, p)
if p < 0 || !pred(g[p]) {
return false
}
p--
}
return true
}
// matchContext matches a format-3 coverage input sequence: covs[0] must cover
// the anchor glyph g[i], and covs[1:] must cover successive non-skipped
// positions after i. It returns the matched absolute positions (starting with
// i), the index just past the input and whether it matched. An empty covs list
// does not match.
func (sk skipper) matchContext(g []GlyphIndex, i int, covs []map[GlyphIndex]int) (pos []int, end int, ok bool) {
if len(covs) == 0 {
return nil, 0, false
}
if _, ok := covs[0][g[i]]; !ok {
return nil, 0, false
}
rest, end, ok := sk.matchForward(g, i+1, covPreds(covs[1:]))
if !ok {
return nil, 0, false
}
return append([]int{i}, rest...), end, true
}
// covPreds builds coverage-membership predicates from a list of Coverage maps.
func covPreds(covs []map[GlyphIndex]int) []glyphPred {
preds := make([]glyphPred, len(covs))
for k := range covs {
c := covs[k]
preds[k] = func(g GlyphIndex) bool { _, ok := c[g]; return ok }
}
return preds
}
+291
View File
@@ -0,0 +1,291 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import (
"errors"
"fmt"
)
// outlinePoint is one point of a glyph contour in font units. on reports
// whether it lies on the curve (true) or is an off-curve quadratic control
// point (false).
type outlinePoint struct {
x, y float64
on bool
}
// contour is one closed sequence of outline points.
type contour []outlinePoint
// Simple-glyph flag bits (TrueType spec).
const (
flagOnCurve = 0x01
flagXShort = 0x02
flagYShort = 0x04
flagRepeat = 0x08
flagXSamePosX = 0x10 // X is same (long form) or positive (short form)
flagYSamePosY = 0x20 // Y is same (long form) or positive (short form)
)
// Composite-glyph flag bits (TrueType spec).
const (
compArgsAreWords = 0x0001
compArgsAreXY = 0x0002
compWeHaveScale = 0x0008
compMoreComponents = 0x0020
compXAndYScale = 0x0040
compTwoByTwo = 0x0080
)
// maxCompositeDepth bounds composite-glyph recursion; anything deeper is
// rejected as malformed (in addition to explicit cycle detection).
const maxCompositeDepth = 8
// glyphContours returns the outline of glyph gid, resolving composites, with
// a fresh recursion-tracking state. It returns the default-master outline; a
// nil norm disables variation (see loadGlyph).
func (f *Font) glyphContours(gid GlyphIndex) ([]contour, error) {
return f.loadGlyph(gid, 0, map[GlyphIndex]bool{}, nil)
}
// glyphInstructions returns the TrueType instruction bytecode attached to a
// simple glyph gid, or nil for an empty glyph, a composite glyph, or a glyph
// with no instructions. It is only called after glyphContours has successfully
// decoded gid, so the loca range and the simple-glyph header are known valid.
func (f *Font) glyphInstructions(gid GlyphIndex) []byte {
start, end := f.loca[gid], f.loca[gid+1]
if start == end {
return nil // empty glyph (e.g. space)
}
r := reader{b: f.glyf[start:end]}
n := int(r.i16())
if n < 0 {
return nil // composite glyph: not hinted here
}
r.skip(8) // xMin, yMin, xMax, yMax
for i := 0; i < n; i++ {
r.u16() // endPtsOfContours[i]
}
instrLen := int(r.u16())
if instrLen == 0 {
return nil
}
return r.b[r.pos : r.pos+instrLen]
}
// loadGlyph decodes glyph gid's contours in font units. depth and visited
// guard composite recursion against over-deep nesting and cycles. norm, when
// non-nil, is the normalized variation coordinate at which the glyph is
// instanced: simple-glyph points get their 'gvar' deltas (with IUP) and
// composite-component offsets get theirs (see gvar.go). A nil norm yields the
// unvaried default-master outline.
func (f *Font) loadGlyph(gid GlyphIndex, depth int, visited map[GlyphIndex]bool, norm []int16) ([]contour, error) {
if depth > maxCompositeDepth {
return nil, errors.New("opentype: composite glyph nesting too deep")
}
if int(gid) >= f.numGlyphs {
return nil, fmt.Errorf("opentype: glyph index %d out of range", int(gid))
}
if visited[gid] {
return nil, fmt.Errorf("opentype: cyclic composite glyph %d", int(gid))
}
start, end := f.loca[gid], f.loca[gid+1]
if start > end {
return nil, fmt.Errorf("opentype: glyph %d: loca not monotonic", int(gid))
}
if int(end) > len(f.glyf) {
return nil, fmt.Errorf("opentype: glyph %d: %w", int(gid), errTruncated)
}
if start == end {
return nil, nil // empty glyph (e.g. space)
}
visited[gid] = true
defer delete(visited, gid)
r := reader{b: f.glyf[start:end]}
numberOfContours := int(r.i16())
r.skip(8) // xMin, yMin, xMax, yMax
if numberOfContours >= 0 {
contours, err := f.simpleGlyph(&r, numberOfContours)
if err != nil {
return nil, err
}
return f.varySimple(int(gid), contours, norm), nil
}
return f.compositeGlyph(int(gid), &r, depth, visited, norm)
}
// simpleGlyph decodes a simple (non-composite) glyph's n contours.
func (f *Font) simpleGlyph(r *reader, n int) ([]contour, error) {
endPts := make([]uint16, n)
for i := range endPts {
endPts[i] = r.u16()
}
numPoints := 0
if n > 0 {
numPoints = int(endPts[n-1]) + 1
}
instrLen := int(r.u16())
r.skip(instrLen)
flags := make([]uint8, numPoints)
for i := 0; i < numPoints; {
fl := r.u8()
flags[i] = fl
i++
if fl&flagRepeat != 0 {
rep := int(r.u8())
for j := 0; j < rep && i < numPoints; j++ {
flags[i] = fl
i++
}
}
}
xs := readCoords(r, flags, numPoints, flagXShort, flagXSamePosX)
ys := readCoords(r, flags, numPoints, flagYShort, flagYSamePosY)
if r.err != nil {
return nil, fmt.Errorf("opentype: simple glyph: %w", r.err)
}
contours := make([]contour, n)
p := 0
for ci := 0; ci < n; ci++ {
endp := int(endPts[ci])
var cnt contour
for ; p <= endp; p++ {
cnt = append(cnt, outlinePoint{
x: float64(xs[p]),
y: float64(ys[p]),
on: flags[p]&flagOnCurve != 0,
})
}
contours[ci] = cnt
}
return contours, nil
}
// readCoords decodes one axis (x or y) of point coordinates, accumulating the
// signed deltas per the short/same flag encoding.
func readCoords(r *reader, flags []uint8, numPoints int, shortBit, samePosBit uint8) []int {
coords := make([]int, numPoints)
v := 0
for i := 0; i < numPoints; i++ {
fl := flags[i]
if fl&shortBit != 0 {
d := int(r.u8())
if fl&samePosBit == 0 {
d = -d
}
v += d
} else if fl&samePosBit == 0 {
v += int(r.i16())
}
coords[i] = v
}
return coords
}
// glyphComponent is one decoded component record of a composite glyph: the
// referenced glyph, its placement offset (arg1/arg2, when argsAreXY) and its
// 2x2 transform (a,b,c,d).
type glyphComponent struct {
flags uint16
gid GlyphIndex
arg1, arg2 int
a, b, c, d float64
argsAreXY bool
}
// parseComponents decodes a composite glyph's component records; r must be
// positioned just past the glyph header. Components are returned in order.
func parseComponents(r *reader) ([]glyphComponent, error) {
var comps []glyphComponent
for {
var comp glyphComponent
comp.flags = r.u16()
comp.gid = GlyphIndex(r.u16())
if comp.flags&compArgsAreWords != 0 {
comp.arg1 = int(r.i16())
comp.arg2 = int(r.i16())
} else {
comp.arg1 = int(int8(r.u8()))
comp.arg2 = int(int8(r.u8()))
}
comp.argsAreXY = comp.flags&compArgsAreXY != 0
comp.a, comp.b, comp.c, comp.d = 1.0, 0.0, 0.0, 1.0
switch {
case comp.flags&compWeHaveScale != 0:
comp.a = r.f2dot14()
comp.d = comp.a
case comp.flags&compXAndYScale != 0:
comp.a = r.f2dot14()
comp.d = r.f2dot14()
case comp.flags&compTwoByTwo != 0:
comp.a = r.f2dot14()
comp.b = r.f2dot14()
comp.c = r.f2dot14()
comp.d = r.f2dot14()
}
if r.err != nil {
return nil, fmt.Errorf("opentype: composite glyph: %w", r.err)
}
comps = append(comps, comp)
if comp.flags&compMoreComponents == 0 {
return comps, nil
}
}
}
// compositeGlyph assembles a composite glyph from its components, recursively
// decoding and transforming each referenced glyph. When norm is non-nil the
// component offsets are first varied by 'gvar' (varyComponentOffsets) and each
// component is instanced recursively at the same coordinate.
func (f *Font) compositeGlyph(gid int, r *reader, depth int, visited map[GlyphIndex]bool, norm []int16) ([]contour, error) {
comps, err := parseComponents(r)
if err != nil {
return nil, err
}
dxs := make([]float64, len(comps))
dys := make([]float64, len(comps))
for i := range comps {
dxs[i] = float64(comps[i].arg1)
dys[i] = float64(comps[i].arg2)
}
f.varyComponentOffsets(gid, comps, dxs, dys, norm)
var out []contour
for i := range comps {
comp := comps[i]
if !comp.argsAreXY {
return nil, errors.New("opentype: unsupported: composite point matching")
}
sub, err := f.loadGlyph(comp.gid, depth+1, visited, norm)
if err != nil {
return nil, err
}
out = append(out, transformContours(sub, comp.a, comp.b, comp.c, comp.d, dxs[i], dys[i])...)
}
return out, nil
}
// transformContours applies the affine transform [a b c d] with translation
// (dx, dy) to every point of cs, returning fresh contours.
func transformContours(cs []contour, a, b, c, d, dx, dy float64) []contour {
out := make([]contour, len(cs))
for ci, cont := range cs {
nc := make(contour, len(cont))
for i, pt := range cont {
nc[i] = outlinePoint{
x: a*pt.x + c*pt.y + dx,
y: b*pt.x + d*pt.y + dy,
on: pt.on,
}
}
out[ci] = nc
}
return out
}
+164
View File
@@ -0,0 +1,164 @@
// Copyright (c) the go-opentype/opentype authors.
// SPDX-License-Identifier: BSD-3-Clause
package opentype
import (
"strconv"
"strings"
)
// Point is a 2-D coordinate in a glyph outline.
type Point struct{ X, Y float64 }
// SegmentOp identifies a glyph-outline path command.
type SegmentOp uint8
const (
// SegMoveTo starts a new contour at P[0].
SegMoveTo SegmentOp = iota
// SegLineTo draws a straight line to P[0].
SegLineTo
// SegQuadTo draws a quadratic Bézier with control point P[0] to endpoint P[1].
SegQuadTo
// SegClose closes the current contour; it carries no points.
SegClose
)
// Segment is one command of a glyph outline. SegMoveTo and SegLineTo use P[0];
// SegQuadTo uses P[0] as the quadratic control point and P[1] as the on-curve
// endpoint; SegClose uses neither.
type Segment struct {
Op SegmentOp
P [2]Point
}
// GlyphOutline returns glyph gid's outline as path segments in font design
// units, in the font's native Y-up orientation with the pen origin at (0, 0).
//
// TrueType off-curve points are emitted as [SegQuadTo] control points, with the
// implied on-curve midpoint synthesised between two consecutive off-curve
// points. CFF/CFF2 outlines are stored by this package pre-flattened to
// polylines, so they yield [SegLineTo] segments only.
//
// ok is false when gid is out of range or its outline is corrupt; a valid but
// empty glyph (for example a space) returns nil, true.
func (fc *Face) GlyphOutline(gid GlyphIndex) (segs []Segment, ok bool) {
cs, err := fc.outline(gid)
if err != nil {
return nil, false
}
for _, c := range cs {
segs = appendContour(segs, c)
}
return segs, true
}
// mid returns the midpoint of a and b.
func mid(a, b outlinePoint) Point {
return Point{X: (a.x + b.x) / 2, Y: (a.y + b.y) / 2}
}
// appendContour converts one closed contour of on/off-curve points into path
// segments (a MoveTo, the body, and a Close), following the TrueType rule that
// consecutive off-curve points imply an on-curve midpoint between them.
func appendContour(dst []Segment, c contour) []Segment {
n := len(c)
if n == 0 {
return dst
}
// Choose the starting on-curve point; an all-off-curve contour starts at the
// synthesised midpoint of its last and first points.
var start Point
var body contour
switch {
case c[0].on:
start, body = Point{c[0].x, c[0].y}, c[1:]
case c[n-1].on:
start, body = Point{c[n-1].x, c[n-1].y}, c[:n-1]
default:
start, body = mid(c[n-1], c[0]), c
}
dst = append(dst, Segment{Op: SegMoveTo, P: [2]Point{start}})
var ctrl Point
haveCtrl := false
for _, p := range body {
cur := Point{p.x, p.y}
if p.on {
if haveCtrl {
dst = append(dst, Segment{Op: SegQuadTo, P: [2]Point{ctrl, cur}})
haveCtrl = false
} else {
dst = append(dst, Segment{Op: SegLineTo, P: [2]Point{cur}})
}
continue
}
if haveCtrl {
// Two consecutive off-curve points: emit the implied midpoint.
m := Point{(ctrl.X + cur.X) / 2, (ctrl.Y + cur.Y) / 2}
dst = append(dst, Segment{Op: SegQuadTo, P: [2]Point{ctrl, m}})
}
ctrl, haveCtrl = cur, true
}
if haveCtrl {
dst = append(dst, Segment{Op: SegQuadTo, P: [2]Point{ctrl, start}})
}
return append(dst, Segment{Op: SegClose})
}
// GlyphSVGPath returns glyph gid's outline as an SVG path "d" string at the
// face's pixel size, in SVG's Y-down coordinate space with the pen origin at
// (0, 0) — so the baseline is y = 0 and ascenders have negative y. Callers place
// the glyph by wrapping the path in a translate transform.
//
// ok is false when gid is out of range or its outline is corrupt; a valid but
// empty glyph returns "", true.
func (fc *Face) GlyphSVGPath(gid GlyphIndex) (d string, ok bool) {
segs, ok := fc.GlyphOutline(gid)
if !ok {
return "", false
}
return segmentsToSVG(segs, fc.scale), true
}
// segmentsToSVG renders outline segments (font units, Y-up) into an SVG path
// "d" string scaled by scale and flipped to SVG's Y-down space.
func segmentsToSVG(segs []Segment, scale float64) string {
var b strings.Builder
writePoint := func(p Point) {
b.WriteString(ftoa(p.X * scale))
b.WriteByte(' ')
b.WriteString(ftoa(-p.Y * scale))
}
for _, s := range segs {
switch s.Op {
case SegMoveTo:
b.WriteByte('M')
writePoint(s.P[0])
case SegLineTo:
b.WriteByte('L')
writePoint(s.P[0])
case SegQuadTo:
b.WriteByte('Q')
writePoint(s.P[0])
b.WriteByte(' ')
writePoint(s.P[1])
case SegClose:
b.WriteByte('Z')
}
}
return b.String()
}
// ftoa formats v with up to three decimals and no trailing zeros (so "12.000"
// becomes "12" and "1.250" becomes "1.25"), keeping SVG paths compact. The
// fixed-precision form always contains a decimal point, so trimming is safe.
func ftoa(v float64) string {
if v == 0 { // collapse both +0 and the -0 produced by the Y flip
return "0"
}
s := strings.TrimRight(strconv.FormatFloat(v, 'f', 3, 64), "0")
return strings.TrimSuffix(s, ".")
}
+1653
View File
File diff suppressed because it is too large Load Diff
+1197
View File
File diff suppressed because it is too large Load Diff
+504
View File
@@ -0,0 +1,504 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import "fmt"
// This file implements the optional 'gvar' (Glyph Variations) table for 'glyf'
// outlines: the per-glyph store of point deltas that move a glyph's outline
// from the default master to a requested position in the variation space.
//
// A glyph's variation is a set of "tuple variations". Each tuple has a peak
// (and optionally an intermediate region) in normalized axis space, a set of
// point numbers it touches and a delta (dx, dy) per touched point. At a given
// normalized coordinate every tuple gets a scalar (how strongly it applies);
// the scaled deltas of all tuples are summed. Deltas for points a tuple does
// not touch are recovered by IUP (Interpolation of Untouched Points), done per
// contour against the default-master coordinates.
//
// Both simple and composite glyphs are variable: simple-glyph points get their
// deltas with per-contour IUP, while a composite glyph's per-component
// placement offsets (and the four phantom points) get theirs directly, with no
// IUP (each component point is independent). The four TrueType phantom points
// are carried through delta application (so "all points" and explicit point
// sets index correctly) but are dropped from the result.
//
// Metric variation via HVAR/VVAR/MVAR (advance widths, advance heights and
// global font metrics) is implemented separately in metricvar.go.
// gvarTable is the parsed 'gvar' table.
type gvarTable struct {
axisCount int
sharedTuples [][]float64 // sharedTupleCount peak tuples, each axisCount F2Dot14
dataOffsets []uint32 // glyphCount+1 offsets into the data array
dataArrayOff int // offset of the glyph-variation-data array within data
data []byte // the whole 'gvar' table
}
// Tuple index / header flag bits (gvar TupleVariationHeader.tupleIndex).
const (
gvEmbeddedPeak = 0x8000 // an embedded peak tuple follows the header
gvIntermediate = 0x4000 // embedded intermediate-region tuples follow
gvPrivatePoints = 0x2000 // this tuple has private point numbers
gvTupleIndexMsk = 0x0FFF // shared-tuple index (when no embedded peak)
)
// gvSharedPointNumbers is the bit in GlyphVariationData.tupleVariationCount
// flagging a shared point-number set at the head of the serialized data.
const gvSharedPointNumbers = 0x8000
// parseGvar decodes the 'gvar' header, the per-glyph offset table and the
// shared-tuple array. Per-glyph variation data is decoded lazily in
// applyVariation, so a glyph is only touched when it is actually instanced.
func (f *Font) parseGvar(b []byte) error {
r := reader{b: b}
r.u16() // majorVersion
r.u16() // minorVersion
axisCount := int(r.u16())
sharedTupleCount := int(r.u16())
sharedTuplesOffset := int(r.u32())
glyphCount := int(r.u16())
flags := r.u16()
dataArrayOffset := int(r.u32())
if r.err != nil {
return fmt.Errorf("opentype: gvar table: %w", r.err)
}
longOffsets := flags&1 != 0
offsets := make([]uint32, glyphCount+1)
for i := range offsets {
if longOffsets {
offsets[i] = r.u32()
} else {
offsets[i] = uint32(r.u16()) * 2
}
}
if r.err != nil {
return fmt.Errorf("opentype: gvar offsets: %w", r.err)
}
shared := make([][]float64, sharedTupleCount)
sr := reader{b: b, pos: sharedTuplesOffset}
for i := range shared {
t := make([]float64, axisCount)
for a := range t {
t[a] = sr.f2dot14()
}
shared[i] = t
}
if sr.err != nil {
return fmt.Errorf("opentype: gvar shared tuples: %w", sr.err)
}
f.gvar = &gvarTable{
axisCount: axisCount,
sharedTuples: shared,
dataOffsets: offsets,
dataArrayOff: dataArrayOffset,
data: b,
}
return nil
}
// applyVariation returns pts with glyph gid's 'gvar' deltas applied at the
// normalized coordinate norm (as produced by NormalizeCoords). pts are the
// glyph's default-master outline points in point order; ends holds the last
// point index of each contour (as in glyf's endPtsOfContours). Points untouched
// by a tuple are filled by per-contour IUP against pts. A font without 'gvar',
// a glyph with no variation data, or malformed data yields an unchanged copy.
func (f *Font) applyVariation(gid int, pts []outlinePoint, ends []int, norm []int16) []outlinePoint {
out := make([]outlinePoint, len(pts))
copy(out, pts)
gv := f.gvar
if gv == nil || gid < 0 || gid+1 >= len(gv.dataOffsets) {
return out
}
start := gv.dataArrayOff + int(gv.dataOffsets[gid])
end := gv.dataArrayOff + int(gv.dataOffsets[gid+1])
if start >= end || end > len(gv.data) {
return out
}
coords := make([]float64, gv.axisCount)
for i := range coords {
if i < len(norm) {
coords[i] = float64(norm[i]) / 16384.0
}
}
numReal := len(pts)
accX := make([]float64, numReal+4) // + four phantom points
accY := make([]float64, numReal+4)
if err := gv.applyGlyph(gv.data[start:end], coords, ends, pts, accX, accY, false); err != nil {
return out
}
for i := 0; i < numReal; i++ {
out[i].x += accX[i]
out[i].y += accY[i]
}
return out
}
// tupleHeader is a decoded TupleVariationHeader: the region it applies to and
// the size and point-set kind of its serialized data.
type tupleHeader struct {
size int // serialized data size in bytes (private points + deltas)
peak []float64 // per-axis peak coordinate
lower []float64 // per-axis region lower bound
upper []float64 // per-axis region upper bound
privatePoint bool // has private point numbers
}
// applyGlyph decodes one glyph's GlyphVariationData and accumulates every
// applicable tuple's scaled deltas into accX/accY (length numPts). For a simple
// glyph (composite false) untouched points of an explicit set are filled by
// per-contour IUP against pts; for a composite glyph (composite true, ends and
// pts nil) each point is a component offset or phantom point and untouched
// points simply stay zero — no IUP.
func (gv *gvarTable) applyGlyph(gd []byte, coords []float64, ends []int, pts []outlinePoint, accX, accY []float64, composite bool) error {
numPts := len(accX)
r := reader{b: gd}
countAndFlags := r.u16()
dataOffset := int(r.u16())
tupleCount := int(countAndFlags & gvTupleIndexMsk)
hasShared := countAndFlags&gvSharedPointNumbers != 0
headers := make([]tupleHeader, tupleCount)
for i := range headers {
h := tupleHeader{
size: int(r.u16()),
lower: make([]float64, gv.axisCount),
upper: make([]float64, gv.axisCount),
}
idx := r.u16()
if idx&gvEmbeddedPeak != 0 {
h.peak = make([]float64, gv.axisCount)
for a := range h.peak {
h.peak[a] = r.f2dot14()
}
} else {
si := int(idx & gvTupleIndexMsk)
if si >= len(gv.sharedTuples) {
return fmt.Errorf("opentype: gvar: shared tuple index %d out of range", si)
}
h.peak = gv.sharedTuples[si]
}
if idx&gvIntermediate != 0 {
for a := 0; a < gv.axisCount; a++ {
h.lower[a] = r.f2dot14()
}
for a := 0; a < gv.axisCount; a++ {
h.upper[a] = r.f2dot14()
}
} else {
for a := 0; a < gv.axisCount; a++ {
if h.peak[a] < 0 {
h.lower[a] = h.peak[a]
}
if h.peak[a] > 0 {
h.upper[a] = h.peak[a]
}
}
}
h.privatePoint = idx&gvPrivatePoints != 0
headers[i] = h
}
if r.err != nil {
return fmt.Errorf("opentype: gvar tuple headers: %w", r.err)
}
sd := reader{b: gd, pos: dataOffset}
var sharedPoints []int
sharedAll := false
if hasShared {
sharedPoints, sharedAll = readPackedPoints(&sd)
}
if sd.err != nil {
return fmt.Errorf("opentype: gvar shared points: %w", sd.err)
}
pos := sd.pos
for i := range headers {
h := headers[i]
tr := reader{b: gd, pos: pos}
points, all := sharedPoints, sharedAll
if h.privatePoint {
points, all = readPackedPoints(&tr)
}
n := len(points)
if all {
n = numPts
}
dx := readPackedDeltas(&tr, n)
dy := readPackedDeltas(&tr, n)
if tr.err != nil {
return fmt.Errorf("opentype: gvar tuple %d data: %w", i, tr.err)
}
scalar := supportScalar(coords, h.lower, h.peak, h.upper)
if scalar != 0 {
accumulate(scalar, all, points, dx, dy, ends, pts, accX, accY, !composite)
}
pos += h.size
}
return nil
}
// readPackedPoints decodes a packed point-number set. all is true for the
// "all points" encoding (a leading count of zero), in which case pts is nil.
// Numbers are stored as run-length-encoded deltas from the previous number.
func readPackedPoints(r *reader) (pts []int, all bool) {
first := r.u8()
count := int(first)
if first&0x80 != 0 {
count = int(first&0x7f)<<8 | int(r.u8())
}
if count == 0 {
return nil, true
}
pts = make([]int, 0, count)
val := 0
for len(pts) < count {
control := r.u8()
run := int(control&0x7f) + 1
words := control&0x80 != 0
for j := 0; j < run && len(pts) < count; j++ {
if words {
val += int(r.u16())
} else {
val += int(r.u8())
}
pts = append(pts, val)
}
if r.err != nil {
return pts, false
}
}
return pts, false
}
// readPackedDeltas decodes count run-length-encoded deltas (one axis's worth).
func readPackedDeltas(r *reader, count int) []float64 {
out := make([]float64, 0, count)
for len(out) < count {
control := r.u8()
run := int(control&0x3f) + 1
switch {
case control&0x80 != 0: // DELTAS_ARE_ZERO
for j := 0; j < run && len(out) < count; j++ {
out = append(out, 0)
}
case control&0x40 != 0: // DELTAS_ARE_WORDS
for j := 0; j < run && len(out) < count; j++ {
out = append(out, float64(int16(r.u16())))
}
default:
for j := 0; j < run && len(out) < count; j++ {
out = append(out, float64(int8(r.u8())))
}
}
if r.err != nil {
return out
}
}
return out
}
// supportScalar returns how strongly a tuple with the given per-axis region
// (lower <= peak <= upper) applies at coords: 1 at the peak, tapering linearly
// to 0 at the region edges, and 0 outside. An axis whose peak is 0, or whose
// region is degenerate or spans zero, does not constrain the tuple.
func supportScalar(coords, lower, peak, upper []float64) float64 {
scalar := 1.0
for a := range peak {
p := peak[a]
if p == 0 {
continue
}
lo, up := lower[a], upper[a]
if lo > p || p > up {
continue
}
if lo < 0 && up > 0 {
continue
}
v := coords[a]
if v == p {
continue
}
if v <= lo || up <= v {
return 0
}
if v < p {
scalar *= (v - lo) / (p - lo)
} else {
scalar *= (up - v) / (up - p)
}
}
return scalar
}
// accumulate adds one tuple's contribution to accX/accY. For an explicit point
// set it places each touched point's delta; iup then fills the untouched points
// per contour by interpolation (simple glyphs) or leaves them at zero
// (composite glyphs). For the "all points" set every point is touched and
// deltas apply directly.
func accumulate(scalar float64, all bool, points []int, dx, dy []float64, ends []int, pts []outlinePoint, accX, accY []float64, iup bool) {
numPts := len(accX)
fullX := make([]float64, numPts)
fullY := make([]float64, numPts)
if all {
for i := 0; i < numPts && i < len(dx); i++ {
fullX[i] = dx[i]
fullY[i] = dy[i]
}
} else {
touched := make([]bool, numPts)
for k, pi := range points {
if pi >= numPts {
continue
}
fullX[pi] = dx[k]
fullY[pi] = dy[k]
touched[pi] = true
}
if iup {
startIdx := 0
for _, e := range ends {
iupContour(pts[startIdx:e+1], fullX[startIdx:e+1], fullY[startIdx:e+1], touched[startIdx:e+1])
startIdx = e + 1
}
}
}
for i := 0; i < numPts; i++ {
accX[i] += scalar * fullX[i]
accY[i] += scalar * fullY[i]
}
}
// iupContour fills deltas for the untouched points of a single contour in
// place, interpolating each coordinate against the default-master positions in
// pts. With no touched point the contour is left unmoved; with exactly one the
// whole contour is shifted by that delta; otherwise untouched points between
// each pair of consecutive touched points (wrapping around the contour) are
// interpolated.
func iupContour(pts []outlinePoint, dx, dy []float64, touched []bool) {
n := len(pts)
var tp []int
for i := 0; i < n; i++ {
if touched[i] {
tp = append(tp, i)
}
}
if len(tp) == 0 {
return
}
if len(tp) == 1 {
for i := 0; i < n; i++ {
dx[i] = dx[tp[0]]
dy[i] = dy[tp[0]]
}
return
}
for k := 0; k < len(tp); k++ {
a := tp[k]
b := tp[(k+1)%len(tp)]
for i := (a + 1) % n; i != b; i = (i + 1) % n {
dx[i] = iupInterp(pts[i].x, pts[a].x, pts[b].x, dx[a], dx[b])
dy[i] = iupInterp(pts[i].y, pts[a].y, pts[b].y, dy[a], dy[b])
}
}
}
// iupInterp interpolates the delta of one untouched point at coordinate target,
// bracketed by two touched points at coordinates lo/hi with deltas dlo/dhi.
// Outside the bracket the nearer touched delta is held constant; inside, the
// delta is linearly interpolated by coordinate.
func iupInterp(target, lo, hi, dlo, dhi float64) float64 {
if lo > hi {
lo, hi = hi, lo
dlo, dhi = dhi, dlo
}
if target <= lo {
return dlo
}
if target >= hi {
return dhi
}
return dlo + (dhi-dlo)*(target-lo)/(hi-lo)
}
// varySimple applies glyph gid's 'gvar' point deltas to a simple glyph's
// contours at normalized coordinate norm, returning fresh contours. A nil norm
// or a font without 'gvar' returns contours unchanged (the default outline).
func (f *Font) varySimple(gid int, contours []contour, norm []int16) []contour {
if norm == nil || f.gvar == nil {
return contours
}
var pts []outlinePoint
var ends []int
for _, c := range contours {
pts = append(pts, c...)
ends = append(ends, len(pts)-1)
}
varied := f.applyVariation(gid, pts, ends, norm)
out := make([]contour, len(contours))
startIdx := 0
for i, e := range ends {
out[i] = varied[startIdx : e+1]
startIdx = e + 1
}
return out
}
// varyComponentOffsets applies glyph gid's 'gvar' deltas to a composite glyph's
// component placement offsets in place (dxs/dys, one entry per component). The
// gvar point set for a composite indexes the components in order followed by
// four phantom points; deltas apply with no IUP. A nil norm, a font without
// 'gvar', or malformed data leaves dxs/dys unchanged.
func (f *Font) varyComponentOffsets(gid int, comps []glyphComponent, dxs, dys []float64, norm []int16) {
if norm == nil {
return
}
gv := f.gvar
if gv == nil || gid < 0 || gid+1 >= len(gv.dataOffsets) {
return
}
start := gv.dataArrayOff + int(gv.dataOffsets[gid])
end := gv.dataArrayOff + int(gv.dataOffsets[gid+1])
if start >= end || end > len(gv.data) {
return
}
coords := make([]float64, gv.axisCount)
for i := range coords {
if i < len(norm) {
coords[i] = float64(norm[i]) / 16384.0
}
}
accX := make([]float64, len(comps)+4) // component offsets + four phantom points
accY := make([]float64, len(comps)+4)
if err := gv.applyGlyph(gv.data[start:end], coords, nil, nil, accX, accY, true); err != nil {
return
}
for i := range comps {
dxs[i] += accX[i]
dys[i] += accY[i]
}
}
// InstancePoints returns glyph gid's outline, as contours in font units,
// instanced at the user-space axis coordinates coords (keyed by axis tag).
// Axes absent from coords take their default. For a non-variable font, or a
// glyph with no variation data, it returns the default outline. Both simple
// and composite glyphs are varied: a composite's component offsets get their
// deltas and each component is instanced recursively at the same coordinate.
func (f *Font) InstancePoints(gid int, coords map[string]float64) ([]contour, error) {
norm := f.NormalizeCoords(coords)
return f.loadGlyph(GlyphIndex(gid), 0, map[GlyphIndex]bool{}, norm)
}
+1755
View File
File diff suppressed because it is too large Load Diff
+295
View File
@@ -0,0 +1,295 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import (
"encoding/binary"
"fmt"
)
// This file bakes a variable ('glyf'/'gvar') font at a chosen axis position into
// a plain static font, so an embedder that cannot ship a variable font (a PDF
// writer, say) can ship the exact instance a document uses. Every glyph's outline
// is instanced (gvar deltas applied, composites flattened) and re-encoded as a
// simple glyph, every advance width is instanced (HVAR, or the gvar phantom-point
// fallback), and the variation tables (fvar, avar, gvar, HVAR/VVAR/MVAR, STAT) are
// dropped so the result advertises — and behaves as — a single static master.
//
// Scope (documented): the CFF2 (variable CFF) outline path is not baked; a CFF2
// font returns an error. TrueType instructions and the shared hinting programs
// (cvt/fpgm/prep) are dropped, since flattening composites into simple glyphs
// renumbers the points the instructions address; the instance renders unhinted.
// Global metric variations (MVAR) are not folded into hhea/OS-2; the instance
// keeps the default master's global metrics.
// glyfWriter is a minimal big-endian byte writer for re-encoding glyf/loca and
// patching the cloned metric tables.
type glyfWriter struct{ b []byte }
func (w *glyfWriter) u16(v uint16) { w.b = binary.BigEndian.AppendUint16(w.b, v) }
func (w *glyfWriter) i16(v int16) { w.u16(uint16(v)) }
// Instance bakes the font at the user-space axis coordinates coords into a new,
// static Font: a font with the requested variation permanently applied and no
// variation axes. Axes absent from coords take their default. It is the parsed
// counterpart of InstanceBytes.
//
// It returns an error for a non-variable font, a CFF2 (variable CFF) font, or when
// coords names an axis the font does not have.
func (f *Font) Instance(coords map[string]float64) (*Font, error) {
b, err := f.InstanceBytes(coords)
if err != nil {
return nil, err
}
return Parse(b)
}
// InstanceBytes bakes the font at coords into the bytes of a new static TrueType
// sfnt (see Instance for the semantics). The returned font carries instanced glyf
// outlines and advance widths and no variation tables; Instance parses these bytes.
func (f *Font) InstanceBytes(coords map[string]float64) ([]byte, error) {
if f.cff2 != nil {
return nil, fmt.Errorf("opentype: Instance: CFF2 (variable CFF) fonts are not supported")
}
if f.fvar == nil {
return nil, fmt.Errorf("opentype: Instance: font is not variable (no fvar table)")
}
if f.glyf == nil {
return nil, fmt.Errorf("opentype: Instance: font has no glyf outlines to instance")
}
known := make(map[string]bool, len(f.fvar.axes))
for _, a := range f.fvar.axes {
known[a.Tag] = true
}
for tag := range coords {
if !known[tag] {
return nil, fmt.Errorf("opentype: Instance: unknown axis %q", tag)
}
}
norm := f.NormalizeCoords(coords)
glyphs := make([][]byte, f.numGlyphs)
advances := make([]int, f.numGlyphs)
lsbs := make([]int, f.numGlyphs)
haveBox := false
var xMin, yMin, xMax, yMax int
for gid := 0; gid < f.numGlyphs; gid++ {
contours, err := f.loadGlyph(GlyphIndex(gid), 0, map[GlyphIndex]bool{}, norm)
if err != nil {
return nil, err
}
glyphs[gid] = encodeSimpleGlyph(contours)
advances[gid] = roundInt(float64(f.advances[gid]) + f.horizontalAdvanceDelta(gid, norm))
gx0, gy0, gx1, gy1, ok := contourBounds(contours)
if ok {
lsbs[gid] = gx0
if !haveBox {
xMin, yMin, xMax, yMax, haveBox = gx0, gy0, gx1, gy1, true
} else {
xMin, yMin = min(xMin, gx0), min(yMin, gy0)
xMax, yMax = max(xMax, gx1), max(yMax, gy1)
}
}
}
loca, glyf := packGlyf(glyphs)
tables := map[string][]byte{
"glyf": glyf,
"loca": loca,
"head": f.instanceHead(xMin, yMin, xMax, yMax),
"hhea": f.subsetHhea(f.numGlyphs),
"maxp": instanceMaxp(f.tables["maxp"], glyphs),
"hmtx": buildHmtx(advances, lsbs),
}
// Carry over every non-variation table verbatim. The variation tables and the
// hinting tables are dropped (see the file comment); the six rebuilt tables
// above are already set and must not be overwritten by the originals.
drop := map[string]bool{
"fvar": true, "avar": true, "gvar": true,
"HVAR": true, "VVAR": true, "MVAR": true, "STAT": true,
"cvt ": true, "fpgm": true, "prep": true,
"glyf": true, "loca": true, "head": true,
"hhea": true, "maxp": true, "hmtx": true,
}
for tag, b := range f.tables {
if drop[tag] {
continue
}
tables[tag] = append([]byte(nil), b...)
}
return buildSFNT(versionTrueType, tables), nil
}
// packGlyf concatenates the per-glyph blobs into a glyf table and its matching
// long-format loca (numGlyphs+1 offsets), 2-byte-aligning each glyph.
func packGlyf(glyphs [][]byte) (loca, glyf []byte) {
offsets := make([]uint32, len(glyphs)+1)
for i, g := range glyphs {
offsets[i] = uint32(len(glyf))
glyf = append(glyf, g...)
for len(glyf)%2 != 0 {
glyf = append(glyf, 0)
}
}
offsets[len(glyphs)] = uint32(len(glyf))
loca = make([]byte, len(offsets)*4)
for i, o := range offsets {
binary.BigEndian.PutUint32(loca[i*4:], o)
}
return loca, glyf
}
// buildHmtx builds a full hmtx table: one (advanceWidth, leftSideBearing) pair for
// every glyph (numberOfHMetrics equals the glyph count).
func buildHmtx(advances, lsbs []int) []byte {
w := &glyfWriter{}
for i := range advances {
w.u16(uint16(advances[i]))
w.i16(int16(lsbs[i]))
}
return w.b
}
// instanceHead clones the head table with checkSumAdjustment zeroed (buildSFNT
// recomputes it), indexToLocFormat set to long, and the recomputed instanced glyph
// bounding box.
func (f *Font) instanceHead(xMin, yMin, xMax, yMax int) []byte {
head := append([]byte(nil), f.tables["head"]...)
binary.BigEndian.PutUint32(head[8:], 0)
binary.BigEndian.PutUint16(head[36:], uint16(int16(xMin)))
binary.BigEndian.PutUint16(head[38:], uint16(int16(yMin)))
binary.BigEndian.PutUint16(head[40:], uint16(int16(xMax)))
binary.BigEndian.PutUint16(head[42:], uint16(int16(yMax)))
binary.BigEndian.PutUint16(head[50:], 1)
return head
}
// instanceMaxp clones maxp with the outline statistics of the instanced glyphs:
// the max simple-glyph point and contour counts are recomputed, and the composite
// statistics are zeroed since the instance carries only simple glyphs. A
// version-0.5 maxp (too short to hold these fields) is returned unchanged.
func instanceMaxp(orig []byte, glyphs [][]byte) []byte {
maxp := append([]byte(nil), orig...)
if len(maxp) < 32 {
return maxp
}
maxPoints, maxContours := 0, 0
for _, g := range glyphs {
p, c := simpleGlyphStats(g)
maxPoints = max(maxPoints, p)
maxContours = max(maxContours, c)
}
binary.BigEndian.PutUint16(maxp[6:], uint16(maxPoints))
binary.BigEndian.PutUint16(maxp[8:], uint16(maxContours))
binary.BigEndian.PutUint16(maxp[10:], 0) // maxCompositePoints
binary.BigEndian.PutUint16(maxp[12:], 0) // maxCompositeContours
return maxp
}
// simpleGlyphStats returns the point and contour counts of a re-encoded simple
// glyph blob (as encodeSimpleGlyph produced it), or (0,0) for an empty glyph.
func simpleGlyphStats(g []byte) (points, contours int) {
if len(g) < 10 {
return 0, 0
}
n := int(int16(binary.BigEndian.Uint16(g)))
if n <= 0 {
return 0, 0
}
// endPtsOfContours[n-1] + 1 is the point count; the last end point sits at
// offset 10 + 2*(n-1).
last := 10 + 2*(n-1)
if last+2 > len(g) {
return 0, 0
}
return int(binary.BigEndian.Uint16(g[last:])) + 1, n
}
// contourBounds returns the integer bounding box of the (rounded) contour points
// and whether any point was present.
func contourBounds(contours []contour) (xMin, yMin, xMax, yMax int, ok bool) {
for _, c := range contours {
for _, p := range c {
x, y := roundInt(p.x), roundInt(p.y)
if !ok {
xMin, yMin, xMax, yMax, ok = x, y, x, y, true
continue
}
xMin, yMin = min(xMin, x), min(yMin, y)
xMax, yMax = max(xMax, x), max(yMax, y)
}
}
return xMin, yMin, xMax, yMax, ok
}
// encodeSimpleGlyph re-encodes instanced contours as a TrueType simple glyph, with
// coordinates rounded to integers and no instructions. It returns nil (an empty
// glyph) when the contours hold no points. Off-curve (quadratic control) points
// are preserved, so the re-encoded glyph re-parses to the same outline.
func encodeSimpleGlyph(contours []contour) []byte {
var pts []outlinePoint
var ends []int
for _, c := range contours {
if len(c) == 0 {
continue
}
pts = append(pts, c...)
ends = append(ends, len(pts)-1)
}
if len(pts) == 0 {
return nil
}
xMin, yMin, xMax, yMax, _ := contourBounds(contours)
w := &glyfWriter{}
w.i16(int16(len(ends)))
w.i16(int16(xMin))
w.i16(int16(yMin))
w.i16(int16(xMax))
w.i16(int16(yMax))
for _, e := range ends {
w.u16(uint16(e))
}
w.u16(0) // instructionLength
var flags, xs, ys []byte
px, py := 0, 0
for _, p := range pts {
x, y := roundInt(p.x), roundInt(p.y)
var fl byte
if p.on {
fl |= flagOnCurve
}
fx, xb := encodeGlyfDelta(x-px, flagXShort, flagXSamePosX)
fy, yb := encodeGlyfDelta(y-py, flagYShort, flagYSamePosY)
fl |= fx | fy
flags = append(flags, fl)
xs = append(xs, xb...)
ys = append(ys, yb...)
px, py = x, y
}
w.b = append(w.b, flags...)
w.b = append(w.b, xs...)
w.b = append(w.b, ys...)
return w.b
}
// encodeGlyfDelta encodes one coordinate delta with the short/same/long flag
// scheme readCoords decodes: zero deltas set the "same" bit and emit no bytes,
// deltas in [-255,255] use one byte with a sign carried by the short/positive
// bits, and larger deltas fall back to a signed 16-bit value.
func encodeGlyfDelta(delta int, shortBit, samePosBit byte) (flag byte, data []byte) {
switch {
case delta == 0:
return samePosBit, nil
case delta > 0 && delta <= 255:
return shortBit | samePosBit, []byte{byte(delta)}
case delta < 0 && delta >= -255:
return shortBit, []byte{byte(-delta)}
default:
d := int16(delta)
return 0, []byte{byte(uint16(d) >> 8), byte(d)}
}
}
+81
View File
@@ -0,0 +1,81 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import "fmt"
// This file parses the legacy 'kern' table in its Microsoft/OpenType form
// (table version 0), decoding only format-0 subtables: a flat list of
// (left, right) glyph pairs each with a signed kerning value in font units.
// Non-format-0 subtables are skipped by their declared length. The Apple
// version-1 ('kern' with a 32-bit version) header is not handled.
// kern is a decoded legacy kern table: kerning values keyed by the packed
// (left, right) glyph pair.
type kern struct {
pairs map[uint32]int
}
// kernKey packs a glyph pair into the map key.
func kernKey(left, right GlyphIndex) uint32 {
return uint32(left)<<16 | uint32(right)
}
// parseKern decodes a legacy kern table (version 0).
func parseKern(b []byte) (*kern, error) {
r := reader{b: b}
r.skip(2) // version (0)
nTables := int(r.u16())
if r.err != nil {
return nil, fmt.Errorf("opentype: kern header: %w", r.err)
}
k := &kern{pairs: map[uint32]int{}}
for t := 0; t < nTables; t++ {
stStart := r.pos
r.skip(2) // subtable version
length := int(r.u16())
coverage := r.u16()
if r.err != nil {
return nil, fmt.Errorf("opentype: kern subtable header: %w", r.err)
}
if coverage>>8 == 0 { // format is the high byte of the coverage field
if err := k.parseFormat0(&r); err != nil {
return nil, err
}
}
// Advance to the next subtable using its declared length. For format 0
// this matches where the sequential read left off; for skipped formats
// it steps over the unread body.
next := stStart + length
if next > len(r.b) {
return nil, fmt.Errorf("opentype: kern subtable length: %w", errTruncated)
}
r.pos = next
}
return k, nil
}
// parseFormat0 reads a format-0 subtable body (positioned just after the 6-byte
// subtable header) into the pair map.
func (k *kern) parseFormat0(r *reader) error {
nPairs := int(r.u16())
r.skip(6) // searchRange, entrySelector, rangeShift
for i := 0; i < nPairs; i++ {
left := GlyphIndex(r.u16())
right := GlyphIndex(r.u16())
value := int(r.i16())
k.pairs[kernKey(left, right)] = value
}
if r.err != nil {
return fmt.Errorf("opentype: kern format 0: %w", r.err)
}
return nil
}
// Pair returns the legacy kerning adjustment between left and right in font
// units, or 0 when the pair is absent.
func (k *kern) Pair(left, right GlyphIndex) int {
return k.pairs[kernKey(left, right)]
}
+376
View File
@@ -0,0 +1,376 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import (
"fmt"
"sort"
)
// This file parses the structures shared by the OpenType Layout tables GSUB
// and GPOS: the ScriptList, FeatureList and LookupList, plus the Coverage and
// ClassDef tables their subtables point at. The lookup subtables themselves are
// table-specific and decoded in gsub.go and gpos.go.
//
// All offsets inside a Layout table are unsigned 16-bit values measured from
// the start of the structure that contains them, so parsing proceeds by
// re-slicing at each offset with subslice and recursing.
// subslice returns b[off:], or nil when off is past the end of b. A nil result
// makes any subsequent bounded read record errTruncated, so callers can defer
// the error check to a single site.
func subslice(b []byte, off int) []byte {
if off > len(b) {
return nil
}
return b[off:]
}
// tag reads a 4-byte OpenType tag as a string, recording errTruncated when the
// four bytes are not available.
func (r *reader) tag() string {
if r.pos+4 > len(r.b) {
r.err = errTruncated
return ""
}
s := string(r.b[r.pos : r.pos+4])
r.pos += 4
return s
}
// langSys is a decoded LangSys table: the (optional) required feature index and
// the list of feature indices enabled for a script/language.
type langSys struct {
required uint16 // 0xFFFF means "no required feature"
features []uint16
}
// langSysRecord binds a 4-byte language tag to its LangSys table.
type langSysRecord struct {
tag string
ls *langSys
}
// scriptRecord is a decoded Script table: its default LangSys (may be nil) and
// any language-specific LangSys tables, keyed by tag.
type scriptRecord struct {
tag string
defaultLang *langSys
langs []langSysRecord
}
// featureRecord is a decoded Feature table: the lookup indices it activates.
type featureRecord struct {
tag string
lookups []uint16
}
// layoutHeader holds the script and feature tables shared by a GSUB or GPOS
// table. It is embedded into gsub and gpos to provide selectLookups.
type layoutHeader struct {
scripts []scriptRecord
features []featureRecord
}
// parseLayoutCommon decodes the version header of a GSUB/GPOS table and its
// ScriptList, FeatureList and LookupList, returning the shared header plus the
// raw bytes of each Lookup table (decoded by the caller per lookup type).
func parseLayoutCommon(data []byte) (layoutHeader, [][]byte, error) {
r := reader{b: data}
r.skip(4) // majorVersion, minorVersion (1.1's featureVariations is ignored)
scriptOff := int(r.u16())
featureOff := int(r.u16())
lookupOff := int(r.u16())
if r.err != nil {
return layoutHeader{}, nil, fmt.Errorf("opentype: layout header: %w", r.err)
}
scripts, err := parseScriptList(subslice(data, scriptOff))
if err != nil {
return layoutHeader{}, nil, err
}
features, err := parseFeatureList(subslice(data, featureOff))
if err != nil {
return layoutHeader{}, nil, err
}
lookups, err := parseLookupList(subslice(data, lookupOff))
if err != nil {
return layoutHeader{}, nil, err
}
return layoutHeader{scripts: scripts, features: features}, lookups, nil
}
// parseScriptList decodes a ScriptList table.
func parseScriptList(b []byte) ([]scriptRecord, error) {
r := reader{b: b}
count := int(r.u16())
type rec struct {
tag string
off int
}
recs := make([]rec, count)
for i := 0; i < count; i++ {
recs[i].tag = r.tag()
recs[i].off = int(r.u16())
}
if r.err != nil {
return nil, fmt.Errorf("opentype: scriptList: %w", r.err)
}
out := make([]scriptRecord, count)
for i, rc := range recs {
sc, err := parseScript(subslice(b, rc.off))
if err != nil {
return nil, err
}
sc.tag = rc.tag
out[i] = sc
}
return out, nil
}
// parseScript decodes a Script table (its default and language LangSys tables).
func parseScript(b []byte) (scriptRecord, error) {
r := reader{b: b}
defOff := int(r.u16())
langCount := int(r.u16())
type lr struct {
tag string
off int
}
lrs := make([]lr, langCount)
for i := 0; i < langCount; i++ {
lrs[i].tag = r.tag()
lrs[i].off = int(r.u16())
}
if r.err != nil {
return scriptRecord{}, fmt.Errorf("opentype: script: %w", r.err)
}
var sc scriptRecord
if defOff != 0 {
ls, err := parseLangSys(subslice(b, defOff))
if err != nil {
return scriptRecord{}, err
}
sc.defaultLang = ls
}
sc.langs = make([]langSysRecord, langCount)
for i, l := range lrs {
ls, err := parseLangSys(subslice(b, l.off))
if err != nil {
return scriptRecord{}, err
}
sc.langs[i] = langSysRecord{tag: l.tag, ls: ls}
}
return sc, nil
}
// parseLangSys decodes a LangSys table.
func parseLangSys(b []byte) (*langSys, error) {
r := reader{b: b}
r.skip(2) // lookupOrderOffset (reserved, always 0)
required := r.u16()
n := int(r.u16())
feats := make([]uint16, n)
for i := 0; i < n; i++ {
feats[i] = r.u16()
}
if r.err != nil {
return nil, fmt.Errorf("opentype: langSys: %w", r.err)
}
return &langSys{required: required, features: feats}, nil
}
// parseFeatureList decodes a FeatureList table.
func parseFeatureList(b []byte) ([]featureRecord, error) {
r := reader{b: b}
count := int(r.u16())
type rec struct {
tag string
off int
}
recs := make([]rec, count)
for i := 0; i < count; i++ {
recs[i].tag = r.tag()
recs[i].off = int(r.u16())
}
if r.err != nil {
return nil, fmt.Errorf("opentype: featureList: %w", r.err)
}
out := make([]featureRecord, count)
for i, rc := range recs {
fr, err := parseFeature(subslice(b, rc.off))
if err != nil {
return nil, err
}
fr.tag = rc.tag
out[i] = fr
}
return out, nil
}
// parseFeature decodes a Feature table (its lookup index list).
func parseFeature(b []byte) (featureRecord, error) {
r := reader{b: b}
r.skip(2) // featureParamsOffset (unused here)
n := int(r.u16())
idx := make([]uint16, n)
for i := 0; i < n; i++ {
idx[i] = r.u16()
}
if r.err != nil {
return featureRecord{}, fmt.Errorf("opentype: feature: %w", r.err)
}
return featureRecord{lookups: idx}, nil
}
// parseLookupList decodes a LookupList table into the raw bytes of each Lookup
// table. Type-specific decoding happens in gsub.go and gpos.go.
func parseLookupList(b []byte) ([][]byte, error) {
r := reader{b: b}
count := int(r.u16())
offs := make([]int, count)
for i := 0; i < count; i++ {
offs[i] = int(r.u16())
}
if r.err != nil {
return nil, fmt.Errorf("opentype: lookupList: %w", r.err)
}
out := make([][]byte, count)
for i, off := range offs {
out[i] = subslice(b, off)
}
return out, nil
}
// parseCoverage decodes a Coverage table (format 1 or 2) into a map from glyph
// to its coverage index.
func parseCoverage(b []byte) (map[GlyphIndex]int, error) {
r := reader{b: b}
format := r.u16()
m := map[GlyphIndex]int{}
switch format {
case 1:
n := int(r.u16())
for i := 0; i < n; i++ {
m[GlyphIndex(r.u16())] = i
}
case 2:
n := int(r.u16())
for i := 0; i < n; i++ {
start := int(r.u16())
end := int(r.u16())
startCov := int(r.u16())
for g := start; g <= end; g++ {
m[GlyphIndex(g)] = startCov + (g - start)
}
}
default:
return nil, fmt.Errorf("opentype: coverage format %d", format)
}
if r.err != nil {
return nil, fmt.Errorf("opentype: coverage: %w", r.err)
}
return m, nil
}
// parseClassDef decodes a ClassDef table (format 1 or 2) into a map from glyph
// to class. Glyphs absent from the map belong to class 0.
func parseClassDef(b []byte) (map[GlyphIndex]int, error) {
r := reader{b: b}
format := r.u16()
m := map[GlyphIndex]int{}
switch format {
case 1:
start := int(r.u16())
n := int(r.u16())
for i := 0; i < n; i++ {
m[GlyphIndex(start+i)] = int(r.u16())
}
case 2:
n := int(r.u16())
for i := 0; i < n; i++ {
s := int(r.u16())
e := int(r.u16())
c := int(r.u16())
for g := s; g <= e; g++ {
m[GlyphIndex(g)] = c
}
}
default:
return nil, fmt.Errorf("opentype: classdef format %d", format)
}
if r.err != nil {
return nil, fmt.Errorf("opentype: classdef: %w", r.err)
}
return m, nil
}
// findScript resolves a script tag, falling back to "DFLT" and then to the
// first script present, matching the behaviour of common shapers.
func (h *layoutHeader) findScript(tag string) *scriptRecord {
for i := range h.scripts {
if h.scripts[i].tag == tag {
return &h.scripts[i]
}
}
for i := range h.scripts {
if h.scripts[i].tag == "DFLT" {
return &h.scripts[i]
}
}
if len(h.scripts) > 0 {
return &h.scripts[0]
}
return nil
}
// findLang resolves a language tag within a script, falling back to the script's
// default LangSys.
func (s *scriptRecord) findLang(tag string) *langSys {
for i := range s.langs {
if s.langs[i].tag == tag {
return s.langs[i].ls
}
}
return s.defaultLang
}
// selectLookups returns the lookup indices activated by the wanted feature tags
// for a (script, language) pair, in ascending lookup order and de-duplicated.
// The required feature (if any) is included when its tag is wanted.
func (h *layoutHeader) selectLookups(script, lang string, want map[string]bool) []int {
sr := h.findScript(script)
if sr == nil {
return nil
}
ls := sr.findLang(lang)
if ls == nil {
return nil
}
seen := map[int]bool{}
var out []int
add := func(featIdx uint16) {
if int(featIdx) >= len(h.features) {
return
}
fr := h.features[featIdx]
if !want[fr.tag] {
return
}
for _, li := range fr.lookups {
if !seen[int(li)] {
seen[int(li)] = true
out = append(out, int(li))
}
}
}
if ls.required != 0xFFFF {
add(ls.required)
}
for _, fi := range ls.features {
add(fi)
}
sort.Ints(out)
return out
}
+674
View File
@@ -0,0 +1,674 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import (
"fmt"
"sort"
)
// This file decodes the OpenType MATH table: the font-level metrics a math
// typesetter needs. It is the layer LuaTeX, unicode-math, MathJax and Word's
// equation editor build their math layout on. The MATH table has three parts:
//
// - MathConstants: ~56 global scalars (subscript/superscript shifts, fraction
// and radical gaps, rule thicknesses, the math axis height, …).
// - MathGlyphInfo: per-glyph italic correction, top-accent attachment points,
// an "extended shape" set and cut-in math kerning for the four glyph corners.
// - MathVariants: larger size variants of a glyph plus the recipe (a
// GlyphAssembly of repeatable parts) for building an arbitrarily tall or wide
// stretchy glyph (integrals, braces, radicals, arrows).
//
// This package exposes those metrics, pixel-scaled through a Face. The actual
// math LAYOUT — box building, TeX's math list rules, choosing a variant or
// assembling a stretchy glyph at a target size — is a higher-level engine that
// consumes these values; it is out of scope here.
//
// All offsets inside the table are unsigned 16-bit values measured from the
// start of the structure that contains them, so parsing re-slices with subslice
// and recurses, exactly as gsub.go/gpos.go/layout.go do.
// MathConstant identifies one scalar in the MATH table's MathConstants record.
// The constants are listed in their on-disk order. Face.MathConstant returns
// each pixel-scaled, except the three percentage constants
// (ScriptPercentScaleDown, ScriptScriptPercentScaleDown and
// RadicalDegreeBottomRaisePercent), which are pure ratios and returned verbatim.
type MathConstant int
// The MathConstants record fields, in table order. mathConstCount is the count
// sentinel (not a real constant).
const (
ScriptPercentScaleDown MathConstant = iota
ScriptScriptPercentScaleDown
DelimitedSubFormulaMinHeight
DisplayOperatorMinHeight
MathLeading
AxisHeight
AccentBaseHeight
FlattenedAccentBaseHeight
SubscriptShiftDown
SubscriptTopMax
SubscriptBaselineDropMin
SuperscriptShiftUp
SuperscriptShiftUpCramped
SuperscriptBottomMin
SuperscriptBaselineDropMax
SubSuperscriptGapMin
SuperscriptBottomMaxWithSubscript
SpaceAfterScript
UpperLimitGapMin
UpperLimitBaselineRiseMin
LowerLimitGapMin
LowerLimitBaselineDropMin
StackTopShiftUp
StackTopDisplayStyleShiftUp
StackBottomShiftDown
StackBottomDisplayStyleShiftDown
StackGapMin
StackDisplayStyleGapMin
StretchStackTopShiftUp
StretchStackBottomShiftDown
StretchStackGapAboveMin
StretchStackGapBelowMin
FractionNumeratorShiftUp
FractionNumeratorDisplayStyleShiftUp
FractionDenominatorShiftDown
FractionDenominatorDisplayStyleShiftDown
FractionNumeratorGapMin
FractionNumDisplayStyleGapMin
FractionRuleThickness
FractionDenominatorGapMin
FractionDenomDisplayStyleGapMin
SkewedFractionHorizontalGap
SkewedFractionVerticalGap
OverbarVerticalGap
OverbarRuleThickness
OverbarExtraAscender
UnderbarVerticalGap
UnderbarRuleThickness
UnderbarExtraDescender
RadicalVerticalGap
RadicalDisplayStyleVerticalGap
RadicalRuleThickness
RadicalExtraAscender
RadicalKernBeforeDegree
RadicalKernAfterDegree
RadicalDegreeBottomRaisePercent
mathConstCount
)
// isPercent reports whether c is a percentage ratio rather than a design-unit
// length; such a constant is returned unscaled.
func (c MathConstant) isPercent() bool {
return c == ScriptPercentScaleDown ||
c == ScriptScriptPercentScaleDown ||
c == RadicalDegreeBottomRaisePercent
}
// MathKernCorner selects one of a glyph's four math-kern corners, used for the
// staircase cut-in between a base glyph and an adjacent script.
type MathKernCorner int
// The four math-kern corners, in the on-disk order of a MathKernInfoRecord.
const (
MathKernTopRight MathKernCorner = iota
MathKernTopLeft
MathKernBottomRight
MathKernBottomLeft
)
// partFlagExtender marks a GlyphPart that may be repeated to lengthen a stretchy
// glyph assembly (PART_FLAG_EXTENDER).
const partFlagExtender = 0x0001
// MathVariant is one size variant of a stretchy glyph: the variant's glyph and
// its advance (height for a vertical variant, width for a horizontal one) in
// pixels at the Face's size.
type MathVariant struct {
Glyph GlyphIndex
Advance int
}
// MathAssembly is the recipe for building a stretchy glyph out of repeatable
// parts, with all measurements in pixels at the Face's size. A layout engine
// stacks the parts (vertically or horizontally), overlapping neighbours by at
// least MinConnectorOverlap and repeating the Extender parts until the assembly
// reaches the target size.
type MathAssembly struct {
ItalicsCorrection int
MinConnectorOverlap int
Parts []MathAssemblyPart
}
// MathAssemblyPart is one component of a MathAssembly. StartConnector and
// EndConnector are the maximum overlap lengths at the part's leading and
// trailing ends; FullAdvance is the part's full extent along the stretch axis.
type MathAssemblyPart struct {
Glyph GlyphIndex
StartConnector int
EndConnector int
FullAdvance int
Extender bool
}
// mathTable is a decoded MATH table. Every sub-part is optional; a font may
// carry constants only, glyph info only, variants only, or any combination.
type mathTable struct {
constants [mathConstCount]int32 // design-unit or percent scalars, indexed by MathConstant
hasConstants bool
italicCorr map[GlyphIndex]int32 // MathItalicsCorrectionInfo: per-glyph italic correction
topAccent map[GlyphIndex]int32 // MathTopAccentAttachment: per-glyph top-accent x
extendedShape map[GlyphIndex]int // ExtendedShapeCoverage, used as a set
kern map[GlyphIndex]*mathKernRecord
minConnectorOverlap int32
vertConstruct map[GlyphIndex]*mathGlyphConstruction // vertical (tall) stretch
horizConstruct map[GlyphIndex]*mathGlyphConstruction // horizontal (wide) stretch
}
// mathKernRecord holds a glyph's four corner MathKerns (any may be nil).
type mathKernRecord struct {
corners [4]*mathKern
}
// mathKern is a step function of correction height: heights has n entries and
// kerns has n+1, so a height selects the kern in the interval it falls into.
type mathKern struct {
heights []int32 // ascending correctionHeight values (design units)
kerns []int32 // kern values (design units), one more than heights
}
// valueAt returns the kern for correction height h (design units) by locating
// the interval h falls into: the first heights[i] strictly greater than h, or
// the final kern when h is at or above every height.
func (k *mathKern) valueAt(h int32) int32 {
i := sort.Search(len(k.heights), func(i int) bool { return h < k.heights[i] })
return k.kerns[i]
}
// mathGlyphConstruction lists a glyph's size variants and, optionally, the
// assembly recipe for an arbitrarily stretched glyph.
type mathGlyphConstruction struct {
variants []mathVariantRecord
assembly *mathAssemblyData
}
// mathVariantRecord is one MathGlyphVariantRecord (design units).
type mathVariantRecord struct {
glyph GlyphIndex
advance int32
}
// mathAssemblyData is a decoded GlyphAssembly (design units).
type mathAssemblyData struct {
italicsCorrection int32
parts []mathPart
}
// mathPart is one GlyphPart of an assembly (design units).
type mathPart struct {
glyph GlyphIndex
startConnector int32
endConnector int32
fullAdvance int32
extender bool
}
// mathValue reads a MathValueRecord: an int16 design-unit value followed by an
// Offset16 to an optional Device table, which this package tolerates and
// ignores (device deltas fine-tune a value at a specific ppem; a scalable
// renderer does not need them).
func (r *reader) mathValue() int32 {
v := r.i16()
r.skip(2) // deviceOffset, ignored
return int32(v)
}
// parseMath decodes the optional MATH table and stores it on the font. Absence
// is not an error; a malformed table fails the whole parse so a corrupt font is
// rejected cleanly, matching the other layout tables.
func (f *Font) parseMath(tables map[string][]byte) error {
b, ok := tables["MATH"]
if !ok {
return nil
}
m, err := parseMATH(b)
if err != nil {
return err
}
f.math = m
return nil
}
// parseMATH decodes a MATH table: a version header followed by up to three
// independent sub-offsets.
func parseMATH(b []byte) (*mathTable, error) {
r := reader{b: b}
r.skip(4) // majorVersion, minorVersion
constOff := int(r.u16())
glyphInfoOff := int(r.u16())
variantsOff := int(r.u16())
if r.err != nil {
return nil, fmt.Errorf("opentype: MATH header: %w", r.err)
}
m := &mathTable{}
if constOff != 0 {
if err := m.parseConstants(subslice(b, constOff)); err != nil {
return nil, err
}
}
if glyphInfoOff != 0 {
if err := m.parseGlyphInfo(subslice(b, glyphInfoOff)); err != nil {
return nil, err
}
}
if variantsOff != 0 {
if err := m.parseVariants(subslice(b, variantsOff)); err != nil {
return nil, err
}
}
return m, nil
}
// parseConstants decodes the MathConstants record into the constants array. The
// layout is two int16 percentages, two UFWORD minimum heights, 51 MathValue
// records and a trailing int16 percentage.
func (m *mathTable) parseConstants(b []byte) error {
r := reader{b: b}
var c [mathConstCount]int32
c[ScriptPercentScaleDown] = int32(r.i16())
c[ScriptScriptPercentScaleDown] = int32(r.i16())
c[DelimitedSubFormulaMinHeight] = int32(r.u16())
c[DisplayOperatorMinHeight] = int32(r.u16())
for i := MathLeading; i <= RadicalKernAfterDegree; i++ {
c[i] = r.mathValue()
}
c[RadicalDegreeBottomRaisePercent] = int32(r.i16())
if r.err != nil {
return fmt.Errorf("opentype: MATH constants: %w", r.err)
}
m.constants = c
m.hasConstants = true
return nil
}
// parseGlyphInfo decodes the MathGlyphInfo table: four optional sub-offsets.
func (m *mathTable) parseGlyphInfo(b []byte) error {
r := reader{b: b}
italicOff := int(r.u16())
topAccentOff := int(r.u16())
extShapeOff := int(r.u16())
kernOff := int(r.u16())
if r.err != nil {
return fmt.Errorf("opentype: MATH glyphInfo: %w", r.err)
}
if italicOff != 0 {
vals, err := parsePerGlyphValues(subslice(b, italicOff))
if err != nil {
return err
}
m.italicCorr = vals
}
if topAccentOff != 0 {
vals, err := parsePerGlyphValues(subslice(b, topAccentOff))
if err != nil {
return err
}
m.topAccent = vals
}
if extShapeOff != 0 {
cov, err := parseCoverage(subslice(b, extShapeOff))
if err != nil {
return err
}
m.extendedShape = cov
}
if kernOff != 0 {
k, err := parseMathKernInfo(subslice(b, kernOff))
if err != nil {
return err
}
m.kern = k
}
return nil
}
// parsePerGlyphValues decodes the shape shared by MathItalicsCorrectionInfo and
// MathTopAccentAttachment: a Coverage offset, a count and that many MathValue
// records, joined into a per-glyph map.
func parsePerGlyphValues(b []byte) (map[GlyphIndex]int32, error) {
r := reader{b: b}
covOff := int(r.u16())
count := int(r.u16())
vals := make([]int32, count)
for i := range vals {
vals[i] = r.mathValue()
}
if r.err != nil {
return nil, fmt.Errorf("opentype: MATH per-glyph values: %w", r.err)
}
cov, err := parseCoverage(subslice(b, covOff))
if err != nil {
return nil, err
}
return coverageValues(cov, vals), nil
}
// coverageValues joins a Coverage (glyph→index) with a parallel value slice into
// a glyph→value map.
func coverageValues(cov map[GlyphIndex]int, vals []int32) map[GlyphIndex]int32 {
out := make(map[GlyphIndex]int32, len(cov))
for g, i := range cov {
if i < len(vals) {
out[g] = vals[i]
}
}
return out
}
// parseMathKernInfo decodes the MathKernInfo table: a Coverage plus one
// MathKernInfoRecord (four corner offsets) per covered glyph.
func parseMathKernInfo(b []byte) (map[GlyphIndex]*mathKernRecord, error) {
r := reader{b: b}
covOff := int(r.u16())
count := int(r.u16())
offs := make([][4]int, count)
for i := range offs {
for c := 0; c < 4; c++ {
offs[i][c] = int(r.u16())
}
}
if r.err != nil {
return nil, fmt.Errorf("opentype: MATH kernInfo: %w", r.err)
}
recs := make([]*mathKernRecord, count)
for i := range offs {
rec := &mathKernRecord{}
for c := 0; c < 4; c++ {
if offs[i][c] == 0 {
continue // this corner has no math kern
}
k, err := parseMathKern(subslice(b, offs[i][c]))
if err != nil {
return nil, err
}
rec.corners[c] = k
}
recs[i] = rec
}
cov, err := parseCoverage(subslice(b, covOff))
if err != nil {
return nil, err
}
out := make(map[GlyphIndex]*mathKernRecord, len(cov))
for g, i := range cov {
if i < len(recs) {
out[g] = recs[i]
}
}
return out, nil
}
// parseMathKern decodes a MathKern subtable: heightCount correction heights and
// heightCount+1 kern values.
func parseMathKern(b []byte) (*mathKern, error) {
r := reader{b: b}
n := int(r.u16())
heights := make([]int32, n)
for i := range heights {
heights[i] = r.mathValue()
}
kerns := make([]int32, n+1)
for i := range kerns {
kerns[i] = r.mathValue()
}
if r.err != nil {
return nil, fmt.Errorf("opentype: MATH kern: %w", r.err)
}
return &mathKern{heights: heights, kerns: kerns}, nil
}
// parseVariants decodes the MathVariants table: the minimum connector overlap,
// two Coverage offsets and the vertical and horizontal glyph constructions.
func (m *mathTable) parseVariants(b []byte) error {
r := reader{b: b}
m.minConnectorOverlap = int32(r.u16())
vertCovOff := int(r.u16())
horizCovOff := int(r.u16())
vertCount := int(r.u16())
horizCount := int(r.u16())
vertOffs := make([]int, vertCount)
for i := range vertOffs {
vertOffs[i] = int(r.u16())
}
horizOffs := make([]int, horizCount)
for i := range horizOffs {
horizOffs[i] = int(r.u16())
}
if r.err != nil {
return fmt.Errorf("opentype: MATH variants: %w", r.err)
}
vert, err := buildConstructions(b, vertCovOff, vertOffs)
if err != nil {
return err
}
horiz, err := buildConstructions(b, horizCovOff, horizOffs)
if err != nil {
return err
}
m.vertConstruct = vert
m.horizConstruct = horiz
return nil
}
// buildConstructions decodes each MathGlyphConstruction at offs and keys them by
// the glyphs in the Coverage at covOff. A zero covOff (an empty axis) yields an
// empty map without touching the coverage bytes.
func buildConstructions(b []byte, covOff int, offs []int) (map[GlyphIndex]*mathGlyphConstruction, error) {
out := map[GlyphIndex]*mathGlyphConstruction{}
if covOff == 0 {
return out, nil
}
constructs := make([]*mathGlyphConstruction, len(offs))
for i, off := range offs {
gc, err := parseGlyphConstruction(subslice(b, off))
if err != nil {
return nil, err
}
constructs[i] = gc
}
cov, err := parseCoverage(subslice(b, covOff))
if err != nil {
return nil, err
}
for g, i := range cov {
if i < len(constructs) {
out[g] = constructs[i]
}
}
return out, nil
}
// parseGlyphConstruction decodes a MathGlyphConstruction: an optional
// GlyphAssembly offset and a list of MathGlyphVariantRecords.
func parseGlyphConstruction(b []byte) (*mathGlyphConstruction, error) {
r := reader{b: b}
asmOff := int(r.u16())
variantCount := int(r.u16())
variants := make([]mathVariantRecord, variantCount)
for i := range variants {
variants[i].glyph = GlyphIndex(r.u16())
variants[i].advance = int32(r.u16())
}
if r.err != nil {
return nil, fmt.Errorf("opentype: MATH glyph construction: %w", r.err)
}
gc := &mathGlyphConstruction{variants: variants}
if asmOff != 0 {
asm, err := parseGlyphAssembly(subslice(b, asmOff))
if err != nil {
return nil, err
}
gc.assembly = asm
}
return gc, nil
}
// parseGlyphAssembly decodes a GlyphAssembly: an italics-correction MathValue
// and a list of GlyphParts.
func parseGlyphAssembly(b []byte) (*mathAssemblyData, error) {
r := reader{b: b}
italic := r.mathValue()
partCount := int(r.u16())
parts := make([]mathPart, partCount)
for i := range parts {
parts[i].glyph = GlyphIndex(r.u16())
parts[i].startConnector = int32(r.u16())
parts[i].endConnector = int32(r.u16())
parts[i].fullAdvance = int32(r.u16())
parts[i].extender = r.u16()&partFlagExtender != 0
}
if r.err != nil {
return nil, fmt.Errorf("opentype: MATH glyph assembly: %w", r.err)
}
return &mathAssemblyData{italicsCorrection: italic, parts: parts}, nil
}
// --- public API -------------------------------------------------------------
// HasMath reports whether the font carries an OpenType MATH table (the metrics
// a math-typesetting engine needs). When false, all Face math accessors return
// their zero result.
func (f *Font) HasMath() bool { return f.math != nil }
// IsExtendedShapeGlyph reports whether gid is in the MATH table's extended-shape
// coverage: a tall glyph (such as a large integral) whose superscript is
// positioned as if the glyph were a stretched, rather than a fixed-size, shape.
func (f *Font) IsExtendedShapeGlyph(gid GlyphIndex) bool {
if f.math == nil {
return false
}
_, ok := f.math.extendedShape[gid]
return ok
}
// scaleUnit converts a design-unit value to whole pixels at the face's size.
func (fc *Face) scaleUnit(v int32) int { return roundInt(float64(v) * fc.scale) }
// MathConstant returns the MathConstants value which, pixel-scaled at the face's
// size — except the three percentage constants, which are pure ratios and
// returned verbatim. It returns 0 when the font has no MATH constants or which
// is out of range.
func (fc *Face) MathConstant(which MathConstant) int {
m := fc.font.math
if m == nil || !m.hasConstants || which < 0 || which >= mathConstCount {
return 0
}
v := m.constants[which]
if which.isPercent() {
return int(v)
}
return fc.scaleUnit(v)
}
// ItalicCorrection returns glyph gid's math italic correction in pixels, or 0
// when the glyph has none (or the font carries no MATH table). It is the space
// to insert after a slanted glyph before a following upright one, and the
// horizontal offset for a subscript that follows a superscript.
func (fc *Face) ItalicCorrection(gid GlyphIndex) int {
m := fc.font.math
if m == nil {
return 0
}
return fc.scaleUnit(m.italicCorr[gid])
}
// TopAccentAttachment returns the x position (in pixels from the glyph origin)
// at which a top accent should be centred over glyph gid. ok is false when the
// glyph has no entry, in which case a layout engine centres on the glyph's
// advance instead.
func (fc *Face) TopAccentAttachment(gid GlyphIndex) (int, bool) {
m := fc.font.math
if m == nil {
return 0, false
}
v, ok := m.topAccent[gid]
if !ok {
return 0, false
}
return fc.scaleUnit(v), true
}
// MathKern returns the math cut-in kern (in pixels) for glyph gid at the given
// corner and correction height, the height (in pixels above the baseline) at
// which the adjacent script attaches. It returns 0 when the glyph has no kern
// for that corner or the font carries no MATH table.
func (fc *Face) MathKern(gid GlyphIndex, corner MathKernCorner, correctionHeight int) int {
m := fc.font.math
if m == nil {
return 0
}
rec := m.kern[gid]
if rec == nil {
return 0
}
if corner < 0 || corner > MathKernBottomLeft {
return 0
}
k := rec.corners[corner]
if k == nil {
return 0
}
hUnits := int32(roundInt(float64(correctionHeight) / fc.scale))
return fc.scaleUnit(k.valueAt(hUnits))
}
// MathVariants returns the size variants and the stretchy-glyph assembly for
// glyph gid along the requested axis (vertical when vertical is true, otherwise
// horizontal), with every measurement pixel-scaled at the face's size. Variants
// are ordered smallest-first as stored in the font; the assembly is nil when the
// glyph has no assembly recipe. Both results are nil when the glyph has no
// construction on that axis (or the font carries no MATH table). A math layout
// engine picks the first variant whose advance meets the target size, or builds
// the assembly when even the largest variant is too small.
func (fc *Face) MathVariants(gid GlyphIndex, vertical bool) ([]MathVariant, *MathAssembly) {
m := fc.font.math
if m == nil {
return nil, nil
}
table := m.horizConstruct
if vertical {
table = m.vertConstruct
}
gc := table[gid]
if gc == nil {
return nil, nil
}
var variants []MathVariant
for _, v := range gc.variants {
variants = append(variants, MathVariant{Glyph: v.glyph, Advance: fc.scaleUnit(v.advance)})
}
var asm *MathAssembly
if gc.assembly != nil {
a := gc.assembly
asm = &MathAssembly{
ItalicsCorrection: fc.scaleUnit(a.italicsCorrection),
MinConnectorOverlap: fc.scaleUnit(m.minConnectorOverlap),
Parts: make([]MathAssemblyPart, len(a.parts)),
}
for i, p := range a.parts {
asm.Parts[i] = MathAssemblyPart{
Glyph: p.glyph,
StartConnector: fc.scaleUnit(p.startConnector),
EndConnector: fc.scaleUnit(p.endConnector),
FullAdvance: fc.scaleUnit(p.fullAdvance),
Extender: p.extender,
}
}
}
return variants, asm
}
+465
View File
@@ -0,0 +1,465 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import "fmt"
// This file implements the optional metric-variation tables HVAR, VVAR and
// MVAR, so a variable font's advance widths, advance heights and global font
// metrics track the axis coordinate rather than staying at the default master.
//
// - HVAR (Horizontal Metrics Variations): per-glyph horizontal-advance deltas
// (plus optional left/right side-bearing maps), applied by Face.Advance,
// Face.Measure and the GlyphMask family.
// - VVAR (Vertical Metrics Variations): the vertical analogue, per-glyph
// advance-height deltas, applied by Face.VerticalAdvance.
// - MVAR (Metrics Variations): global font-metric deltas keyed by four-byte
// value tags (hasc/hdsc/hlgp/...), folded into Face.Metrics.
//
// All three share one machinery: an ItemVariationStore (the same format 1 store
// CFF2 references, but here the delta sets are fully decoded because the deltas
// come from the store rather than from a charstring) plus, per metric, an
// optional DeltaSetIndexMap translating a glyph index into an (outer, inner)
// pair addressing the store. The tent-function region evaluator supportScalar
// (gvar.go) is reused unchanged.
//
// When a variable glyf font ships no HVAR table, horizontal advances fall back
// to the per-glyph gvar phantom-point deltas the specification prescribes (see
// gvarAdvanceWidthDelta); this fallback is implemented for simple glyphs.
// Composite-glyph advance fallback is out of scope (it returns no delta).
// metricVarData is one ItemVariationData subtable: the ordered region indices
// its columns correspond to, and one fully-decoded delta row per item.
type metricVarData struct {
regionIndices []int
deltaSets [][]int32 // itemCount rows, each len(regionIndices) deltas
}
// metricVarStore is a format-1 ItemVariationStore with its delta sets decoded.
// Each region is kept as parallel per-axis lower/peak/upper coordinate slices so
// the shared supportScalar evaluator can be called without repacking.
type metricVarStore struct {
axisCount int
regLower [][]float64
regPeak [][]float64
regUpper [][]float64
data []metricVarData
}
// delta returns the interpolated delta for item (outer, inner) at the normalized
// coordinate norm (F2Dot14 int16 per axis, as produced by NormalizeCoords): the
// sum over the subtable's regions of the region's tent scalar times the stored
// delta. An out-of-range outer or inner index yields zero.
func (s *metricVarStore) delta(outer, inner int, norm []int16) float64 {
if outer < 0 || outer >= len(s.data) {
return 0
}
d := s.data[outer]
if inner < 0 || inner >= len(d.deltaSets) {
return 0
}
coords := make([]float64, s.axisCount)
for i := range coords {
if i < len(norm) {
coords[i] = float64(norm[i]) / 16384.0
}
}
row := d.deltaSets[inner]
sum := 0.0
for j, ri := range d.regionIndices {
sum += supportScalar(coords, s.regLower[ri], s.regPeak[ri], s.regUpper[ri]) * float64(row[j])
}
return sum
}
// parseMetricVarStore decodes the ItemVariationStore at offset off in b, whose
// internal region-list and subtable offsets are relative to off. Only format 1
// is supported (the sole format defined for OpenType variations).
func parseMetricVarStore(b []byte, off int) (*metricVarStore, error) {
r := reader{b: b, pos: off}
if format := r.u16(); format != 1 {
return nil, fmt.Errorf("opentype: ItemVariationStore: unsupported format %d", format)
}
regionListOff := off + int(r.u32())
ivdCount := int(r.u16())
ivdOffsets := make([]int, ivdCount)
for i := range ivdOffsets {
ivdOffsets[i] = off + int(r.u32())
}
if r.err != nil {
return nil, fmt.Errorf("opentype: ItemVariationStore header: %w", r.err)
}
rr := reader{b: b, pos: regionListOff}
axisCount := int(rr.u16())
regionCount := int(rr.u16())
lower := make([][]float64, regionCount)
peak := make([][]float64, regionCount)
upper := make([][]float64, regionCount)
for i := 0; i < regionCount; i++ {
lo := make([]float64, axisCount)
pk := make([]float64, axisCount)
up := make([]float64, axisCount)
for a := 0; a < axisCount; a++ {
lo[a] = rr.f2dot14()
pk[a] = rr.f2dot14()
up[a] = rr.f2dot14()
}
lower[i], peak[i], upper[i] = lo, pk, up
}
if rr.err != nil {
return nil, fmt.Errorf("opentype: ItemVariationStore regions: %w", rr.err)
}
data := make([]metricVarData, ivdCount)
for i, o := range ivdOffsets {
d, err := parseItemVarData(b, o, regionCount)
if err != nil {
return nil, err
}
data[i] = d
}
return &metricVarStore{axisCount: axisCount, regLower: lower, regPeak: peak, regUpper: upper, data: data}, nil
}
// parseItemVarData decodes one ItemVariationData subtable at offset off, reading
// its region ordering and the full grid of item deltas. regionCount bounds the
// region indices. The wordDeltaCount field's high bit (LONG_WORDS) and its low
// 15 bits (the count of wide leading columns) select each delta's byte width.
func parseItemVarData(b []byte, off, regionCount int) (metricVarData, error) {
r := reader{b: b, pos: off}
itemCount := int(r.u16())
wordDeltaCount := int(r.u16())
regionIndexCount := int(r.u16())
idx := make([]int, regionIndexCount)
for k := range idx {
ri := int(r.u16())
if ri >= regionCount {
return metricVarData{}, fmt.Errorf("opentype: ItemVariationData: region index %d out of range", ri)
}
idx[k] = ri
}
if r.err != nil {
return metricVarData{}, fmt.Errorf("opentype: ItemVariationData header: %w", r.err)
}
longWords := wordDeltaCount&0x8000 != 0
wordCount := wordDeltaCount & 0x7FFF
if wordCount > regionIndexCount {
return metricVarData{}, fmt.Errorf("opentype: ItemVariationData: word count %d exceeds region count %d", wordCount, regionIndexCount)
}
sets := make([][]int32, itemCount)
for i := 0; i < itemCount; i++ {
row := make([]int32, regionIndexCount)
for j := 0; j < regionIndexCount; j++ {
row[j] = readVarDelta(&r, longWords, j < wordCount)
}
sets[i] = row
}
if r.err != nil {
return metricVarData{}, fmt.Errorf("opentype: ItemVariationData deltas: %w", r.err)
}
return metricVarData{regionIndices: idx, deltaSets: sets}, nil
}
// readVarDelta reads one ItemVariationData delta. With LONG_WORDS the wide
// columns are 32-bit and the narrow ones 16-bit; without it the wide columns are
// 16-bit and the narrow ones 8-bit. wide selects the wide width for this column.
func readVarDelta(r *reader, longWords, wide bool) int32 {
if longWords && wide {
return int32(r.u32())
}
if longWords || wide {
return int32(r.i16())
}
return int32(int8(r.u8()))
}
// deltaSetMap is a decoded DeltaSetIndexMap: per glyph index, the (outer, inner)
// pair addressing the ItemVariationStore. A nil or empty map maps a glyph index
// to itself (outer 0, inner = glyph index).
type deltaSetMap struct {
outer []uint32
inner []uint32
}
// lookup returns the (outer, inner) store indices for glyph gid. A nil or empty
// map is the identity mapping (outer 0, inner gid); a gid past the map's last
// entry clamps to that entry (as the specification prescribes).
func (m *deltaSetMap) lookup(gid int) (int, int) {
if m == nil || len(m.outer) == 0 {
return 0, gid
}
if gid >= len(m.outer) {
gid = len(m.outer) - 1
}
return int(m.outer[gid]), int(m.inner[gid])
}
// parseDeltaSetMap decodes the DeltaSetIndexMap at offset off in b (format 0 or
// 1). A zero offset means the map is absent and yields a nil map (the identity
// mapping). entryFormat's low nibble gives the inner-index bit count minus one
// and bits 4-5 give the per-entry byte size minus one.
func parseDeltaSetMap(b []byte, off int) (*deltaSetMap, error) {
if off == 0 {
return nil, nil
}
r := reader{b: b, pos: off}
format := r.u8()
entryFormat := r.u8()
var count int
switch format {
case 0:
count = int(r.u16())
case 1:
count = int(r.u32())
default:
return nil, fmt.Errorf("opentype: DeltaSetIndexMap: unsupported format %d", format)
}
innerBits := uint(entryFormat&0x0F) + 1
entrySize := int((entryFormat&0x30)>>4) + 1
innerMask := uint32(1)<<innerBits - 1
m := &deltaSetMap{outer: make([]uint32, count), inner: make([]uint32, count)}
for i := 0; i < count; i++ {
var entry uint32
for k := 0; k < entrySize; k++ {
entry = entry<<8 | uint32(r.u8())
}
m.inner[i] = entry & innerMask
m.outer[i] = entry >> innerBits
}
if r.err != nil {
return nil, fmt.Errorf("opentype: DeltaSetIndexMap: %w", r.err)
}
return m, nil
}
// hvarTable is the parsed HVAR table: the shared store plus the advance-width
// and optional left/right side-bearing DeltaSetIndexMaps. Only the advance-width
// map drives a metric here; the side-bearing maps are decoded for completeness.
type hvarTable struct {
store *metricVarStore
advMap *deltaSetMap
lsbMap *deltaSetMap
rsbMap *deltaSetMap
}
// vvarTable is the parsed VVAR table: the shared store plus the advance-height
// and optional top/bottom side-bearing and vertical-origin maps.
type vvarTable struct {
store *metricVarStore
advMap *deltaSetMap
tsbMap *deltaSetMap
bsbMap *deltaSetMap
vorgMap *deltaSetMap
}
// mvarTable is the parsed MVAR table: the shared store plus a map from each
// four-byte value tag to its (outer, inner) store indices.
type mvarTable struct {
store *metricVarStore
values map[string][2]int
}
// delta returns the MVAR delta for value tag at norm, or zero when the tag is
// absent or the table carries no store.
func (m *mvarTable) delta(tag string, norm []int16) float64 {
v, ok := m.values[tag]
if !ok || m.store == nil {
return 0
}
return m.store.delta(v[0], v[1], norm)
}
// parseMetricVarTable decodes the header common to HVAR and VVAR: a version, a
// required ItemVariationStore offset, and mapCount DeltaSetIndexMap offsets (3
// for HVAR, 4 for VVAR), all uint32 and relative to the table start.
func parseMetricVarTable(b []byte, mapCount int) (*metricVarStore, []*deltaSetMap, error) {
r := reader{b: b}
r.u16() // majorVersion
r.u16() // minorVersion
storeOff := int(r.u32())
offs := make([]int, mapCount)
for i := range offs {
offs[i] = int(r.u32())
}
if r.err != nil {
return nil, nil, fmt.Errorf("opentype: metric variations header: %w", r.err)
}
store, err := parseMetricVarStore(b, storeOff)
if err != nil {
return nil, nil, err
}
maps := make([]*deltaSetMap, mapCount)
for i, o := range offs {
m, err := parseDeltaSetMap(b, o)
if err != nil {
return nil, nil, err
}
maps[i] = m
}
return store, maps, nil
}
// parseHVAR decodes the HVAR table into f.hvar.
func (f *Font) parseHVAR(b []byte) error {
store, maps, err := parseMetricVarTable(b, 3)
if err != nil {
return err
}
f.hvar = &hvarTable{store: store, advMap: maps[0], lsbMap: maps[1], rsbMap: maps[2]}
return nil
}
// parseVVAR decodes the VVAR table into f.vvar.
func (f *Font) parseVVAR(b []byte) error {
store, maps, err := parseMetricVarTable(b, 4)
if err != nil {
return err
}
f.vvar = &vvarTable{store: store, advMap: maps[0], tsbMap: maps[1], bsbMap: maps[2], vorgMap: maps[3]}
return nil
}
// parseMVAR decodes the MVAR table into f.mvar: a header, a run of value records
// (each a four-byte tag and an (outer, inner) index pair) and, unless there are
// none, a shared ItemVariationStore. The store offset here is a uint16.
func (f *Font) parseMVAR(b []byte) error {
r := reader{b: b}
r.u16() // majorVersion
r.u16() // minorVersion
r.u16() // reserved
valueRecordSize := int(r.u16())
valueRecordCount := int(r.u16())
storeOff := int(r.u16())
if r.err != nil {
return fmt.Errorf("opentype: MVAR header: %w", r.err)
}
base := r.pos
values := make(map[string][2]int, valueRecordCount)
for i := 0; i < valueRecordCount; i++ {
vr := reader{b: b, pos: base + i*valueRecordSize}
tag := readTag(&vr)
outer := int(vr.u16())
inner := int(vr.u16())
if vr.err != nil {
return fmt.Errorf("opentype: MVAR value record %d: %w", i, vr.err)
}
values[tag] = [2]int{outer, inner}
}
var store *metricVarStore
if storeOff != 0 {
s, err := parseMetricVarStore(b, storeOff)
if err != nil {
return err
}
store = s
}
f.mvar = &mvarTable{store: store, values: values}
return nil
}
// parseMetricVariations parses the optional HVAR, VVAR and MVAR tables. Each is
// independent; absence is not an error. A present-but-malformed table is
// reported so a corrupt font fails cleanly.
func (f *Font) parseMetricVariations(tables map[string][]byte) error {
if b, ok := tables["HVAR"]; ok {
if err := f.parseHVAR(b); err != nil {
return err
}
}
if b, ok := tables["VVAR"]; ok {
if err := f.parseVVAR(b); err != nil {
return err
}
}
if b, ok := tables["MVAR"]; ok {
if err := f.parseMVAR(b); err != nil {
return err
}
}
return nil
}
// horizontalAdvanceDelta returns glyph gid's horizontal-advance delta in font
// units at normalized coordinate norm: the HVAR advance-width delta when the
// font carries HVAR, otherwise the gvar phantom-point fallback for a variable
// glyf font. It is zero for a static font (nil norm) or when nothing applies.
func (f *Font) horizontalAdvanceDelta(gid int, norm []int16) float64 {
if norm == nil {
return 0
}
if f.hvar != nil {
outer, inner := f.hvar.advMap.lookup(gid)
return f.hvar.store.delta(outer, inner, norm)
}
return f.gvarAdvanceWidthDelta(gid, norm)
}
// verticalAdvanceDelta returns glyph gid's advance-height delta in font units at
// normalized coordinate norm from the VVAR table, or zero when the font has no
// VVAR table or norm is nil.
func (f *Font) verticalAdvanceDelta(gid int, norm []int16) float64 {
if norm == nil || f.vvar == nil {
return 0
}
outer, inner := f.vvar.advMap.lookup(gid)
return f.vvar.store.delta(outer, inner, norm)
}
// gvarAdvanceWidthDelta computes glyph gid's horizontal-advance delta from its
// 'gvar' phantom points — the fallback the specification prescribes when a
// variable font varies advances through gvar but ships no HVAR table. It decodes
// the simple glyph's default points, runs the same gvar delta accumulation used
// for outlines (gvar.go) over a buffer that includes the four TrueType phantom
// points, and returns the advance-width phantom's x delta (pp2) minus the left
// phantom's (pp1). It is zero for an empty glyph, a composite glyph, a
// missing/short gvar, or malformed data.
func (f *Font) gvarAdvanceWidthDelta(gid int, norm []int16) float64 {
gv := f.gvar
if gv == nil || gid < 0 || gid+1 >= len(gv.dataOffsets) {
return 0
}
start, end := f.loca[gid], f.loca[gid+1]
if start >= end || int(end) > len(f.glyf) {
return 0 // empty or out-of-range glyph
}
r := reader{b: f.glyf[start:end]}
n := int(r.i16())
r.skip(8) // xMin, yMin, xMax, yMax
if n < 0 {
return 0 // composite-glyph advance fallback is out of scope
}
contours, err := f.simpleGlyph(&r, n)
if err != nil {
return 0
}
var pts []outlinePoint
var ends []int
for _, c := range contours {
pts = append(pts, c...)
ends = append(ends, len(pts)-1)
}
numReal := len(pts)
gstart := gv.dataArrayOff + int(gv.dataOffsets[gid])
gend := gv.dataArrayOff + int(gv.dataOffsets[gid+1])
if gstart >= gend || gend > len(gv.data) {
return 0
}
coords := make([]float64, gv.axisCount)
for i := range coords {
if i < len(norm) {
coords[i] = float64(norm[i]) / 16384.0
}
}
accX := make([]float64, numReal+4)
accY := make([]float64, numReal+4)
if err := gv.applyGlyph(gv.data[gstart:gend], coords, ends, pts, accX, accY, false); err != nil {
return 0
}
return accX[numReal+1] - accX[numReal]
}
+185
View File
@@ -0,0 +1,185 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
// This file exposes the parsed GSUB and GPOS tables to external callers and
// adds positional GSUB application: applying a feature's lookups only at
// selected glyph positions. That per-glyph masking is the mechanism a
// complex-text shaper needs for Arabic cursive joining, where the isol, init,
// medi and fina single-substitution features must each be applied only at the
// glyphs that take that joining form. Whole-run application (as done by Apply)
// remains available by passing a nil mask.
// GSUB is the exported handle to a font's parsed GSUB (glyph substitution)
// table, obtained via [Font.GSUB]. It exposes [GSUB.Apply] (whole-run) and
// [GSUB.ApplyMasked] (positional) so a shaper outside this package can drive
// substitution lookups directly.
type GSUB = gsub
// GPOS is the exported handle to a font's parsed GPOS (glyph positioning)
// table, obtained via [Font.GPOS]. Its [GPOS.Position] method runs positioning
// lookups (pair kerning, mark attachment, cursive attachment, ...) over a glyph
// run a caller has already substituted.
type GPOS = gpos
// GSUB returns the font's parsed GSUB table, or nil when the font carries no
// GSUB table (in which case no substitution is available).
func (f *Font) GSUB() *GSUB { return f.gsub }
// GPOS returns the font's parsed GPOS table, or nil when the font carries no
// GPOS table (in which case no positioning is available).
func (f *Font) GPOS() *GPOS { return f.gpos }
// GlyphAdvance returns the horizontal advance of glyph g in font units, or 0
// when g is out of range. A shaper multiplies it by [Face.Scale] to obtain
// pixels, and feeds the per-glyph advances to [GPOS.Position] so mark
// attachment can pull a diacritic back onto its base.
func (f *Font) GlyphAdvance(g GlyphIndex) int {
if int(g) < len(f.advances) {
return f.advances[g]
}
return 0
}
// FeatureApp requests one GSUB feature applied over a subset of a glyph run.
// Tag is the 4-byte feature tag (for example "init" or "liga"). Positions
// selects, by run index, the glyphs the feature's lookups may start at: a
// lookup is attempted at glyph i only when Positions[i] is true. A nil
// Positions applies the feature over the whole run (identical to Apply); a
// non-nil Positions restricts it, with indices at or past its end treated as
// false.
//
// Masking is intended for the length-preserving single substitutions OpenType
// files the Arabic positional-form features (isol/init/medi/fina) as, so the
// mask indices stay aligned to the run as it is rewritten.
type FeatureApp struct {
Tag string
Positions []bool
}
// ApplyMasked applies each feature in feats, in order, to the glyph run and
// returns the substituted run; the input slice is not modified. For a feature
// with a nil Positions mask the feature's lookups run over the whole run (as
// [GSUB.Apply] does); with a non-nil mask they are attempted only at the glyphs
// the mask selects — the mechanism Arabic cursive shaping needs to apply
// init/medi/fina/isol each at just the glyphs in that joining position.
//
// Features are resolved with the script-agnostic default fallback (the DFLT
// script, then the union across every script's default LangSys), so features
// filed only under a real script such as "arab" or "latn" still resolve. An
// empty feature list or an empty run leaves the input unchanged.
//
// Use [GSUB.ApplyMaskedTracked] instead when the caller must follow which
// source glyph each output glyph derives from across a decomposition or
// ligature (as a shaper does to re-derive Arabic joining masks after ccmp
// decomposes letters, and to map output glyphs back to text clusters).
func (g *gsub) ApplyMasked(glyphs []GlyphIndex, feats []FeatureApp) []GlyphIndex {
out, _ := g.applyMasked(glyphs, nil, feats)
return out
}
// ApplyMaskedTracked is [GSUB.ApplyMasked] that also propagates a per-glyph
// cluster (source-position) slice through the substitution. clusters gives the
// starting cluster of each input glyph; nil starts them at 0, 1, 2, ... The
// returned cluster slice is the same length as the returned run: a glyph
// produced by decomposing input glyph k carries cluster[k], and a ligature
// carries the cluster of its first component. Neither input slice is modified.
//
// This is the primitive a complex-text shaper drives: apply ccmp with tracking
// to learn where each decomposed glyph came from, rebuild the init/medi/fina
// masks against the decomposed run, apply those, and read the final clusters to
// map glyphs back to the source text.
func (g *gsub) ApplyMaskedTracked(glyphs []GlyphIndex, clusters []int, feats []FeatureApp) ([]GlyphIndex, []int) {
if clusters == nil {
clusters = make([]int, len(glyphs))
for i := range clusters {
clusters[i] = i
}
}
return g.applyMasked(glyphs, clusters, feats)
}
// applyMasked is the shared implementation of ApplyMasked and
// ApplyMaskedTracked. A nil clusters disables cluster tracking (the plain
// ApplyMasked fast path); a non-nil clusters is threaded through the applier
// and returned alongside the substituted run.
func (g *gsub) applyMasked(glyphs []GlyphIndex, clusters []int, feats []FeatureApp) ([]GlyphIndex, []int) {
if len(feats) == 0 || len(glyphs) == 0 {
return glyphs, clusters
}
out := append([]GlyphIndex(nil), glyphs...)
a := &gsubApplier{lookups: g.lookups, gdef: g.gdef}
if clusters != nil {
a.clusters = append([]int(nil), clusters...)
}
for _, fa := range feats {
idxs := g.selectLookupsDefault(map[string]bool{fa.Tag: true})
for _, li := range idxs {
if li >= len(g.lookups) {
continue
}
out = a.applyLookupMasked(g.lookups[li], out, fa.Positions)
}
}
return out, a.clusters
}
// applyLookupMasked applies one lookup like applyLookup, but restricted to the
// positions the mask selects. A nil mask means the whole run (delegating to
// applyLookup). With a mask, a substitution is attempted at glyph i only when
// mask[i] is true (indices past the mask are treated as false); this is meant
// for length-preserving single substitutions so the mask stays aligned.
func (a *gsubApplier) applyLookupMasked(lk gsubLookup, glyphs []GlyphIndex, mask []bool) []GlyphIndex {
if mask == nil {
return a.applyLookup(lk, glyphs)
}
a.skip = a.skipperFor(lk)
masked := func(i int) bool { return i < len(mask) && mask[i] && !a.skip.skip(glyphs[i]) }
if lk.reverse {
for i := len(glyphs) - 1; i >= 0; i-- {
if !masked(i) {
continue
}
for _, st := range lk.subtables {
if out, _, ok := st.sub(a, glyphs, i); ok {
glyphs = out
break
}
}
}
return glyphs
}
i := 0
for i < len(glyphs) {
if !masked(i) {
i++
continue
}
applied := false
for _, st := range lk.subtables {
if out, next, ok := st.sub(a, glyphs, i); ok {
glyphs = out
i = next
applied = true
break
}
}
if !applied {
i++
}
}
return glyphs
}
// Position runs the GPOS positioning lookups activated by the given feature
// tags over the glyph run and returns one adjustment per glyph, in font units.
// advances gives each glyph's base horizontal advance (used to pull marks back
// onto their base) and may be nil (treated as all-zero). Features are resolved
// with the same script-agnostic default fallback as kerning, so kern/mark/mkmk
// filed under a real script resolve. It is the exported entry point a shaper
// uses to position a run it substituted itself.
func (p *gpos) Position(glyphs []GlyphIndex, advances []int, features ...string) []GlyphPosition {
return p.position(glyphs, advances, features...)
}
+171
View File
@@ -0,0 +1,171 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import (
"image"
"image/color"
"math"
)
// fpoint is a point in device (pixel) space.
type fpoint struct{ x, y float64 }
// supersample is the per-axis oversampling factor; the rasteriser takes
// ss*ss coverage samples per output pixel for anti-aliasing.
const supersample = 4
// flattenContour scales a font-unit contour to device space (flipping the
// y axis so font "up" becomes image "down") and flattens its quadratic
// segments into a closed polygon. It returns nil for a degenerate contour of
// fewer than two points.
func flattenContour(c contour, scale float64) []fpoint {
n := len(c)
if n < 2 {
return nil
}
type dp struct {
x, y float64
on bool
}
pts := make([]dp, n)
for i, p := range c {
pts[i] = dp{x: p.x * scale, y: -p.y * scale, on: p.on}
}
// Insert an implied on-curve midpoint between any two consecutive
// off-curve points (walking the contour cyclically).
q := make([]dp, 0, n*2)
for i := 0; i < n; i++ {
a := pts[i]
q = append(q, a)
b := pts[(i+1)%n]
if !a.on && !b.on {
q = append(q, dp{x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, on: true})
}
}
// Rotate so the walk starts on an on-curve point (one is guaranteed to
// exist for n >= 2 after midpoint insertion).
s := 0
for !q[s].on {
s++
}
ordered := append(append([]dp{}, q[s:]...), q[:s]...)
m := len(ordered)
poly := []fpoint{{x: ordered[0].x, y: ordered[0].y}}
i := 0
for i < m {
next := ordered[(i+1)%m]
if next.on {
poly = append(poly, fpoint{x: next.x, y: next.y})
i++
continue
}
cur := ordered[i]
end := ordered[(i+2)%m]
flattenQuad(&poly, cur.x, cur.y, next.x, next.y, end.x, end.y)
i += 2
}
return poly
}
// flattenQuad appends a flattened quadratic Bézier (from the start point,
// already the last element of *poly, through control point (cx,cy) to end
// point (x1,y1)) using a step count adapted to the curve's device-space size.
func flattenQuad(poly *[]fpoint, x0, y0, cx, cy, x1, y1 float64) {
d := math.Hypot(cx-x0, cy-y0) + math.Hypot(x1-cx, y1-cy)
steps := int(d / 3.0)
if steps < 1 {
steps = 1
}
for i := 1; i <= steps; i++ {
t := float64(i) / float64(steps)
mt := 1 - t
x := mt*mt*x0 + 2*mt*t*cx + t*t*x1
y := mt*mt*y0 + 2*mt*t*cy + t*t*y1
*poly = append(*poly, fpoint{x: x, y: y})
}
}
// isLeft returns a positive value if point (x,y) is left of the directed edge
// a→b, negative if right, and zero if collinear.
func isLeft(a, b fpoint, x, y float64) float64 {
return (b.x-a.x)*(y-a.y) - (x-a.x)*(b.y-a.y)
}
// insideNonZero reports whether the point (x,y) is inside the polygon set
// under the non-zero winding rule.
func insideNonZero(polys [][]fpoint, x, y float64) bool {
wn := 0
for _, poly := range polys {
n := len(poly)
for i := 0; i < n; i++ {
a := poly[i]
b := poly[(i+1)%n]
if a.y <= y {
if b.y > y && isLeft(a, b, x, y) > 0 {
wn++
}
} else if b.y <= y && isLeft(a, b, x, y) < 0 {
wn--
}
}
}
return wn != 0
}
// rasterize scan-converts the device-space polygons into an 8-bit alpha
// coverage mask using supersampled non-zero winding. It returns the mask's
// device-space bounds (relative to the glyph origin) and an *image.Alpha whose
// own rectangle starts at (0,0). An empty polygon set yields an empty rect and
// a nil mask.
func rasterize(polys [][]fpoint) (image.Rectangle, *image.Alpha) {
if len(polys) == 0 {
return image.Rectangle{}, nil
}
minX, minY := math.Inf(1), math.Inf(1)
maxX, maxY := math.Inf(-1), math.Inf(-1)
for _, poly := range polys {
for _, p := range poly {
if p.x < minX {
minX = p.x
}
if p.x > maxX {
maxX = p.x
}
if p.y < minY {
minY = p.y
}
if p.y > maxY {
maxY = p.y
}
}
}
x0 := int(math.Floor(minX))
y0 := int(math.Floor(minY))
x1 := int(math.Ceil(maxX))
y1 := int(math.Ceil(maxY))
w, h := x1-x0, y1-y0
alpha := image.NewAlpha(image.Rect(0, 0, w, h))
for py := 0; py < h; py++ {
for px := 0; px < w; px++ {
count := 0
for sy := 0; sy < supersample; sy++ {
for sx := 0; sx < supersample; sx++ {
fx := float64(x0+px) + (float64(sx)+0.5)/supersample
fy := float64(y0+py) + (float64(sy)+0.5)/supersample
if insideNonZero(polys, fx, fy) {
count++
}
}
}
alpha.SetAlpha(px, py, color.Alpha{A: uint8(count * 255 / (supersample * supersample))})
}
}
return image.Rect(x0, y0, x1, y1), alpha
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import "sort"
// This file exposes read-only access to the raw, undecoded sfnt table bytes the
// font was parsed from. A consumer that embeds a font (a PDF or EPUB writer) or
// inspects a table this package does not decode can read the exact table data
// without re-parsing the container itself.
// TableTags returns the four-byte tags of every table in the font, sorted
// lexicographically. The returned slice is freshly allocated and owned by the
// caller. Tags retain their sfnt spelling, including any trailing space (for
// example "cvt " and "OS/2").
func (f *Font) TableTags() []string {
tags := make([]string, 0, len(f.tables))
for t := range f.tables {
tags = append(tags, t)
}
sort.Strings(tags)
return tags
}
// Table returns a copy of the raw bytes of the table named by its four-byte tag,
// and ok reporting whether the font carries that table. The tag must be the exact
// sfnt spelling including any trailing space (for example "cvt " or "OS/2"). The
// returned slice is a fresh copy the caller may retain or mutate; it never aliases
// the font's backing data.
func (f *Font) Table(tag string) (data []byte, ok bool) {
b, ok := f.tables[tag]
if !ok {
return nil, false
}
return append([]byte(nil), b...), true
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import "encoding/binary"
// reader is a big-endian sequential reader over a byte slice with sticky
// error tracking: any read past the end records errTruncated and returns zero,
// so a parser can read a run of fields and check err once at the end.
type reader struct {
b []byte
pos int
err error
}
// u8 reads one unsigned byte.
func (r *reader) u8() uint8 {
if r.pos+1 > len(r.b) {
r.err = errTruncated
return 0
}
v := r.b[r.pos]
r.pos++
return v
}
// u16 reads a big-endian uint16.
func (r *reader) u16() uint16 {
if r.pos+2 > len(r.b) {
r.err = errTruncated
return 0
}
v := binary.BigEndian.Uint16(r.b[r.pos:])
r.pos += 2
return v
}
// u32 reads a big-endian uint32.
func (r *reader) u32() uint32 {
if r.pos+4 > len(r.b) {
r.err = errTruncated
return 0
}
v := binary.BigEndian.Uint32(r.b[r.pos:])
r.pos += 4
return v
}
// i16 reads a big-endian signed int16.
func (r *reader) i16() int16 { return int16(r.u16()) }
// u24 reads a big-endian unsigned 24-bit integer (used by cmap format 14's
// variation-selector and Unicode-value fields).
func (r *reader) u24() uint32 {
if r.pos+3 > len(r.b) {
r.err = errTruncated
return 0
}
v := uint32(r.b[r.pos])<<16 | uint32(r.b[r.pos+1])<<8 | uint32(r.b[r.pos+2])
r.pos += 3
return v
}
// f2dot14 reads an F2Dot14 fixed-point value as a float64.
func (r *reader) f2dot14() float64 { return float64(r.i16()) / 16384.0 }
// skip advances the position by n bytes, recording an error if that runs past
// the end of the slice.
func (r *reader) skip(n int) {
if r.pos+n > len(r.b) {
r.err = errTruncated
return
}
r.pos += n
}
+125
View File
@@ -0,0 +1,125 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import (
"encoding/binary"
"sort"
)
// This file holds the shared machinery for writing a fresh sfnt container: the
// table directory, per-table checksums and the head checkSumAdjustment. Both the
// TrueType subsetter (subset.go) and the variable-instance serialiser
// (instance.go) rebuild a font from a set of tables and emit it through
// buildSFNT, so a single, tested writer covers every produced font.
// buildSFNT packs tables into an sfnt container with the given sfnt version tag,
// laying out the table directory (records sorted by tag, 4-byte-aligned bodies),
// filling in each record's checksum, offset and length, and patching head's
// checkSumAdjustment so the emitted font is a valid, self-consistent sfnt. A
// "head" table, when present, must be at least 12 bytes (its checkSumAdjustment
// field lives at offset 8); the callers always supply the full 54-byte table.
func buildSFNT(version uint32, tables map[string][]byte) []byte {
tags := make([]string, 0, len(tables))
for t := range tables {
tags = append(tags, t)
}
sort.Strings(tags)
n := len(tags)
// searchRange/entrySelector/rangeShift describe the largest power of two not
// exceeding n, per the sfnt spec's binary-search hint fields.
entrySelector := 0
for (1 << (entrySelector + 1)) <= n {
entrySelector++
}
searchRange := (1 << entrySelector) * 16
rangeShift := n*16 - searchRange
headerLen := 12 + 16*n
offsets := make(map[string]int, n)
var body []byte
for _, t := range tags {
offsets[t] = headerLen + len(body)
body = append(body, tables[t]...)
for len(body)%4 != 0 {
body = append(body, 0)
}
}
out := make([]byte, headerLen)
binary.BigEndian.PutUint32(out[0:], version)
binary.BigEndian.PutUint16(out[4:], uint16(n))
binary.BigEndian.PutUint16(out[6:], uint16(searchRange))
binary.BigEndian.PutUint16(out[8:], uint16(entrySelector))
binary.BigEndian.PutUint16(out[10:], uint16(rangeShift))
for i, t := range tags {
rec := 12 + i*16
copy(out[rec:], t)
binary.BigEndian.PutUint32(out[rec+4:], sfntChecksum(tables[t]))
binary.BigEndian.PutUint32(out[rec+8:], uint32(offsets[t]))
binary.BigEndian.PutUint32(out[rec+12:], uint32(len(tables[t])))
}
out = append(out, body...)
// head.checkSumAdjustment = 0xB1B0AFBA - checksum(whole file), computed with
// the field itself read as zero (it is still zero in out at this point).
if headOff, ok := offsets["head"]; ok {
adj := 0xB1B0AFBA - sfntChecksum(out)
binary.BigEndian.PutUint32(out[headOff+8:], adj)
}
return out
}
// sfntChecksum is the sfnt table checksum: the sum modulo 2^32 of the data read
// as big-endian uint32 words, the final word zero-padded when the length is not a
// multiple of four.
func sfntChecksum(b []byte) uint32 {
var sum uint32
var i int
for ; i+4 <= len(b); i += 4 {
sum += binary.BigEndian.Uint32(b[i:])
}
if rem := len(b) - i; rem > 0 {
var tail [4]byte
copy(tail[:], b[i:])
sum += binary.BigEndian.Uint32(tail[:])
}
return sum
}
// minimalCmap returns a structurally valid, essentially empty cmap table: a
// single format-4 (3,1 Unicode BMP) subtable whose only segment is the mandatory
// 0xFFFF sentinel, so it maps no character to a real glyph. A subset embedded in
// a PDF as a CIDFontType2 is addressed by glyph id through the CIDToGIDMap and
// never consults a cmap, but opentype.Parse (and other consumers) require a cmap
// table to be present and decodable; this satisfies that without carrying the
// original, now-misindexed mappings.
func minimalCmap() []byte {
var sub []byte
put16 := func(v uint16) { sub = binary.BigEndian.AppendUint16(sub, v) }
put16(4) // format
put16(24) // length (4-header + 8 fixed + one segment of 8)
put16(0) // language
put16(2) // segCountX2 (segCount 1)
put16(2) // searchRange
put16(0) // entrySelector
put16(0) // rangeShift
put16(0xFFFF) // endCode[0] (sentinel)
put16(0) // reservedPad
put16(0xFFFF) // startCode[0]
put16(1) // idDelta[0]
put16(0) // idRangeOffset[0]
var t []byte
putT16 := func(v uint16) { t = binary.BigEndian.AppendUint16(t, v) }
putT32 := func(v uint32) { t = binary.BigEndian.AppendUint32(t, v) }
putT16(0) // version
putT16(1) // numTables
putT16(3) // platformID (Windows)
putT16(1) // encodingID (Unicode BMP)
putT32(12)
return append(t, sub...)
}
+161
View File
@@ -0,0 +1,161 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
// This file wires the OpenType Layout tables (GSUB, GPOS and the legacy kern
// table, parsed in gsub.go/gpos.go/kern.go) into the public Face API: Shape
// runs glyph substitution over a text run, while Kern and MeasureKerned apply
// pair-adjustment kerning. A font without the relevant tables behaves as a
// plain unshaped, unkerned font.
// Shape maps text to its glyph run and applies the GSUB lookups activated by
// the given feature tags (for example "liga" for standard ligatures, "smcp"
// for small caps). Each rune maps through the font's cmap; an unmapped rune
// becomes glyph 0 (.notdef). With no GSUB table, no features, or features that
// match no lookups, the run is the plain cmap mapping.
func (fc *Face) Shape(text string, features ...string) []GlyphIndex {
var glyphs []GlyphIndex
for _, r := range text {
gid, _ := fc.font.GlyphIndex(r) // unmapped -> 0 (.notdef)
glyphs = append(glyphs, gid)
}
if fc.font.gsub != nil {
glyphs = fc.font.gsub.Apply(glyphs, features...)
}
return glyphs
}
// PositionedGlyph is one glyph of a positioned run produced by ShapePositioned:
// the glyph to draw, the pen-relative placement offsets (XOffset, YOffset) GPOS
// assigned it (a non-zero YOffset lifts an attached diacritic onto its base),
// and the advance to move the pen by afterwards, all in whole pixels at the
// face's size.
type PositionedGlyph struct {
Glyph GlyphIndex
XOffset int
YOffset int
XAdvance int
}
// defaultPosFeatures are the GPOS features ShapePositioned always activates in
// addition to any the caller requests: pair/positional kerning ("kern"),
// mark-to-base/ligature attachment ("mark") and mark-to-mark stacking ("mkmk").
var defaultPosFeatures = []string{"kern", "mark", "mkmk"}
// ShapePositioned maps text to a fully positioned glyph run: each rune maps
// through the cmap (an unmapped rune becomes glyph 0), the requested GSUB
// features are applied (ligatures, contextual substitution, ...), then GPOS
// positions the resulting glyphs — pair kerning, single/cursive adjustment and
// mark attachment (types 4/5/6 pull diacritics onto their base). The positioning
// features are the caller's features plus the always-on kern/mark/mkmk set.
//
// The result has one PositionedGlyph per output glyph, with placement and
// advance in whole pixels at the face's size. A font lacking GSUB or GPOS simply
// skips that stage; with neither the run is the plain cmap mapping at its
// unadjusted advances.
func (fc *Face) ShapePositioned(text string, features ...string) []PositionedGlyph {
var glyphs []GlyphIndex
for _, r := range text {
gid, _ := fc.font.GlyphIndex(r) // unmapped -> 0 (.notdef)
glyphs = append(glyphs, gid)
}
if fc.font.gsub != nil {
glyphs = fc.font.gsub.Apply(glyphs, features...)
}
// Base horizontal advances in font units, indexed by glyph, feed both the
// mark-attachment pull-back and the final per-glyph advance.
advances := make([]int, len(glyphs))
for i, g := range glyphs {
advances[i] = fc.font.advances[g]
}
var positions []GlyphPosition
if fc.font.gpos != nil {
posFeatures := append(append([]string(nil), features...), defaultPosFeatures...)
positions = fc.font.gpos.position(glyphs, advances, posFeatures...)
}
out := make([]PositionedGlyph, len(glyphs))
for i, g := range glyphs {
adv := advances[i]
var xOff, yOff int
if positions != nil {
xOff = positions[i].XOffset
yOff = positions[i].YOffset
adv += positions[i].XAdvance
}
out[i] = PositionedGlyph{
Glyph: g,
XOffset: roundInt(float64(xOff) * fc.scale),
YOffset: roundInt(float64(yOff) * fc.scale),
XAdvance: roundInt(float64(adv) * fc.scale),
}
}
return out
}
// Kern returns the horizontal kerning adjustment between the consecutive runes
// prev and r, in whole pixels at the face's size. GPOS pair positioning is
// preferred; the legacy kern table is used as a fallback. It is zero when either
// rune is unmapped or the font carries no kerning for the pair.
//
// The result is rounded to a whole pixel. A text layer that sets type at (or
// near) the font's own em — where one pixel spans a large design quantum and a
// single-pixel kern would quantise away most of the sub-pixel pair adjustment —
// should measure with KernUnits (or KernIndexUnits) instead, which return the
// unrounded font-unit value, exactly as AdvanceIndexUnits complements
// AdvanceIndex.
func (fc *Face) Kern(prev, r rune) int {
return roundInt(fc.KernUnits(prev, r) * fc.scale)
}
// KernIndexUnits returns the horizontal kerning adjustment between the
// consecutive glyphs left and right in font units (unrounded) — the value Kern
// scales and rounds to pixels. GPOS pair positioning is preferred; the legacy
// kern table is the fallback. It is the by-glyph-index, font-unit counterpart of
// Kern, the exact primitive a shaper or a text engine wants: kerning applies
// between shaped glyphs (which may be ligatures), and a caller typesetting at
// the design em keeps the sub-pixel adjustment that Kern's whole-pixel rounding
// would discard. A caller multiplies the result by Face.Scale to obtain pixels
// (mirroring AdvanceIndexUnits). The adjustment is zero when the font carries no
// kerning for the pair.
func (fc *Face) KernIndexUnits(left, right GlyphIndex) float64 {
kn := Kerner{gpos: fc.font.gpos, kern: fc.font.kern}
return float64(kn.Kerning(left, right))
}
// KernUnits returns the horizontal kerning adjustment between the consecutive
// runes prev and r in font units (unrounded) — the rune-keyed, font-unit
// counterpart of Kern. Each rune maps through the cmap; the result is zero when
// either rune is unmapped or the font carries no kerning for the pair. A caller
// multiplies by Face.Scale to obtain pixels; see KernIndexUnits for the
// by-glyph-index form a shaper drives on an already-substituted run.
func (fc *Face) KernUnits(prev, r rune) float64 {
lg, ok := fc.font.GlyphIndex(prev)
if !ok {
return 0
}
rg, ok := fc.font.GlyphIndex(r)
if !ok {
return 0
}
return fc.KernIndexUnits(lg, rg)
}
// MeasureKerned returns the total advance width of s in pixels like Measure,
// but additionally applies the font's kerning between each pair of consecutive
// runes. For a font with no kerning it equals Measure.
func (fc *Face) MeasureKerned(s string) int {
total := 0
var prev rune
havePrev := false
for _, r := range s {
if havePrev {
total += fc.Kern(prev, r)
}
total += fc.Advance(r)
prev = r
havePrev = true
}
return total
}
+224
View File
@@ -0,0 +1,224 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import (
"encoding/binary"
"errors"
"fmt"
"sort"
)
// This file implements TrueType ('glyf') glyph subsetting: rebuilding a minimal,
// self-contained sfnt that carries only a requested set of glyphs (and every
// glyph a composite among them references). It is the primitive a PDF or EPUB
// embedder needs so it can ship a subset font program instead of the whole face,
// and so it does not have to re-parse and re-serialise the sfnt container itself.
// CFF ('CFF ') charstring subsetting lives in subsetcff.go.
// componentRef locates one component-glyph reference inside a composite glyph's
// raw bytes: the byte offset of its two-byte glyph-id field and the glyph id
// stored there. Subsetting reads the ids to compute the closure and rewrites the
// field at the offset to the component's new id.
type componentRef struct {
offset int
gid GlyphIndex
}
// componentRefs returns the component-glyph references of a raw glyph. It returns
// nil for an empty or simple glyph (numberOfContours >= 0) and, for a composite,
// one entry per component in stream order. The walk is bounds-checked: a record
// that would read past the end of data ends the scan, so a truncated composite
// yields the components decodable so far rather than a panic.
func componentRefs(data []byte) []componentRef {
if len(data) < 10 || sbe16(data) >= 0 {
return nil
}
var refs []componentRef
p := 10
for p+4 <= len(data) {
flags := be16(data[p:])
refs = append(refs, componentRef{offset: p + 2, gid: GlyphIndex(be16(data[p+2:]))})
p += 4
if flags&compArgsAreWords != 0 {
p += 4
} else {
p += 2
}
switch {
case flags&compWeHaveScale != 0:
p += 2
case flags&compXAndYScale != 0:
p += 4
case flags&compTwoByTwo != 0:
p += 8
}
if flags&compMoreComponents == 0 {
break
}
}
return refs
}
// rawGlyph returns glyph gid's raw bytes from the glyf table (the loca-delimited
// slice), or nil for an empty glyph. The caller must not mutate the result; it
// aliases the font's backing data.
func (f *Font) rawGlyph(gid GlyphIndex) []byte {
start, end := f.loca[gid], f.loca[gid+1]
if start >= end || int(end) > len(f.glyf) {
return nil
}
return f.glyf[start:end]
}
// subsetClosure returns the sorted set of glyph ids to keep: the requested gids,
// glyph 0 (.notdef, always retained so the subset has a valid missing glyph), and
// every glyph transitively referenced by a composite among them. A requested id
// outside the font's glyph range is an error. Cycles are terminated by the
// membership set, so a self- or mutually-referential composite is handled.
func (f *Font) subsetClosure(gids []GlyphIndex) ([]GlyphIndex, error) {
needed := map[GlyphIndex]bool{}
var walk func(gid GlyphIndex) error
walk = func(gid GlyphIndex) error {
if int(gid) >= f.numGlyphs {
return fmt.Errorf("opentype: subset: glyph index %d out of range", int(gid))
}
if needed[gid] {
return nil
}
needed[gid] = true
for _, ref := range componentRefs(f.rawGlyph(gid)) {
if err := walk(ref.gid); err != nil {
return err
}
}
return nil
}
if err := walk(0); err != nil {
return nil, err
}
for _, g := range gids {
if err := walk(g); err != nil {
return nil, err
}
}
order := make([]GlyphIndex, 0, len(needed))
for g := range needed {
order = append(order, g)
}
sort.Slice(order, func(i, j int) bool { return order[i] < order[j] })
return order, nil
}
// SubsetTrueType builds a minimal, valid TrueType sfnt carrying only the glyphs
// in gids plus glyph 0 and every component a composite among them references. The
// kept glyphs are renumbered compactly (glyph 0 stays 0); the returned oldToNew
// map gives each retained original glyph id its id in the subset, which a PDF
// CIDFontType2 embedder turns into a /CIDToGIDMap (CID = original id -> subset id)
// and a content-stream writer uses to re-address glyphs.
//
// The rebuilt font contains glyf, loca (long form), head, hhea, maxp, hmtx and a
// minimal cmap (enough to re-parse; a PDF addresses the subset by glyph id, not
// through the cmap), plus the TrueType instruction tables (cvt/fpgm/prep) when the
// original font carried them so hinted glyphs still render as designed. Composite
// component references are rewritten to the new glyph numbering.
//
// It returns an error for a font with no glyf table (a CFF/OpenType font — use
// SubsetCFF) or when a requested glyph id is out of range.
func (f *Font) SubsetTrueType(gids []GlyphIndex) (data []byte, oldToNew map[GlyphIndex]GlyphIndex, err error) {
if f.glyf == nil {
return nil, nil, errors.New("opentype: SubsetTrueType: font has no glyf table (not a TrueType font)")
}
kept, err := f.subsetClosure(gids)
if err != nil {
return nil, nil, err
}
oldToNew = make(map[GlyphIndex]GlyphIndex, len(kept))
for newGID, oldGID := range kept {
oldToNew[oldGID] = GlyphIndex(newGID)
}
// Rebuild glyf and a long-format loca in the new, compact glyph order,
// rewriting each composite's component ids to their subset ids.
blobs := make([][]byte, len(kept))
for newGID, oldGID := range kept {
blobs[newGID] = remapGlyph(f.rawGlyph(oldGID), oldToNew)
}
locaBytes, glyf := packGlyf(blobs)
tables := map[string][]byte{
"glyf": glyf,
"loca": locaBytes,
"head": f.subsetHead(),
"hhea": f.subsetHhea(len(kept)),
"maxp": f.subsetMaxp(len(kept)),
"hmtx": f.subsetHmtx(kept),
"cmap": minimalCmap(),
}
for _, opt := range []string{"cvt ", "fpgm", "prep"} {
if b, ok := f.tables[opt]; ok {
tables[opt] = append([]byte(nil), b...)
}
}
return buildSFNT(versionTrueType, tables), oldToNew, nil
}
// remapGlyph returns a copy of a raw glyph with every composite component id
// rewritten to its subset id via oldToNew. A simple or empty glyph is copied
// unchanged. A component id absent from oldToNew (which cannot happen for a glyph
// produced by subsetClosure, whose closure includes every component) is left as
// is, so the function never panics on unexpected input.
func remapGlyph(data []byte, oldToNew map[GlyphIndex]GlyphIndex) []byte {
refs := componentRefs(data)
out := append([]byte(nil), data...)
for _, ref := range refs {
if nb, ok := oldToNew[ref.gid]; ok {
binary.BigEndian.PutUint16(out[ref.offset:], uint16(nb))
}
}
return out
}
// subsetHead clones the head table with checkSumAdjustment zeroed (buildSFNT
// recomputes it) and indexToLocFormat set to long, matching the long-format loca
// the subsetter always emits.
func (f *Font) subsetHead() []byte {
head := append([]byte(nil), f.tables["head"]...)
binary.BigEndian.PutUint32(head[8:], 0) // checkSumAdjustment: recomputed by buildSFNT
binary.BigEndian.PutUint16(head[50:], 1) // indexToLocFormat: long
return head
}
// subsetHhea clones the hhea table with numberOfHMetrics set to the subset glyph
// count, matching the full-width hmtx subsetHmtx emits.
func (f *Font) subsetHhea(n int) []byte {
hhea := append([]byte(nil), f.tables["hhea"]...)
binary.BigEndian.PutUint16(hhea[34:], uint16(n))
return hhea
}
// subsetMaxp clones the maxp table with numGlyphs set to the subset glyph count.
// The other maxp limits (max points, max component depth, ...) remain valid upper
// bounds for a subset of the original glyphs, so they are carried over unchanged.
func (f *Font) subsetMaxp(n int) []byte {
maxp := append([]byte(nil), f.tables["maxp"]...)
binary.BigEndian.PutUint16(maxp[4:], uint16(n))
return maxp
}
// subsetHmtx builds an hmtx table for the subset glyph order: one (advanceWidth,
// leftSideBearing) pair per kept glyph, taken from the original per-glyph metrics.
// Emitting a full entry for every glyph (numberOfHMetrics == glyph count, set in
// subsetHhea) keeps the table trivially correct without the trailing shared-advance
// optimisation.
func (f *Font) subsetHmtx(kept []GlyphIndex) []byte {
out := make([]byte, 4*len(kept))
for i, oldGID := range kept {
binary.BigEndian.PutUint16(out[i*4:], uint16(f.advances[oldGID]))
binary.BigEndian.PutUint16(out[i*4+2:], uint16(int16(f.lsbs[oldGID])))
}
return out
}
+406
View File
@@ -0,0 +1,406 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import "fmt"
// This file implements CFF (Type2 charstring) subsetting: rebuilding a 'CFF '
// table that keeps the outlines of a requested set of glyphs and replaces every
// other glyph's charstring with an empty one (a lone endchar). It is a real
// charstring subset — the CharStrings INDEX, the bulk of a CFF program, no longer
// carries the dropped glyphs' outlines — while glyph numbering is preserved, so a
// PDF embeds the result with an Identity CIDToGIDMap and the font's charset,
// encoding, Private DICT and subroutines all stay valid unchanged.
//
// Scope (documented, deliberate): the global and local subroutine INDEXes are
// retained whole rather than tree-shaken; CID-keyed CFF (a ROS/FDArray/FDSelect
// font) and CFF2 are rejected with an error rather than subset; and endchar-seac
// accent composition (a deprecated CFF feature) is not followed, so a caller
// embedding a seac-built accented glyph should also request its base and accent
// components. These edges keep the subsetter tractable and exact for the common
// name-keyed OTF, which is what PDF and EPUB embedders overwhelmingly handle.
// dictRawEntry is one operator of a CFF DICT together with the raw bytes of its
// operands and their decoded values. The raw bytes let a DICT be re-emitted
// verbatim except for the operators whose operands the subsetter rewrites (the
// offset operators of the Top DICT); copying them avoids decoding and re-encoding
// real-number operands (such as FontMatrix), which would not round-trip exactly.
// The decoded values let the subsetter read the offset and flag operators it acts
// on without a second parse pass.
type dictRawEntry struct {
operands []byte
values []float64
op int
}
// parseDictRaw splits a CFF DICT into its operator entries, each carrying the raw
// operand bytes that preceded the operator and their decoded values. It reuses
// parseDictOperand for the per-operand byte lengths and values, so it accepts
// exactly the operands parseDict does.
func parseDictRaw(b []byte) ([]dictRawEntry, error) {
var entries []dictRawEntry
opStart := 0
var values []float64
i := 0
for i < len(b) {
b0 := b[i]
if b0 <= 21 {
operands := b[opStart:i]
op := int(b0)
i++
if b0 == 12 {
if i >= len(b) {
return nil, fmt.Errorf("opentype: cff dict: truncated operator")
}
op = 1200 + int(b[i])
i++
}
entries = append(entries, dictRawEntry{operands: operands, values: values, op: op})
opStart = i
values = nil
continue
}
v, n, err := parseDictOperand(b, i)
if err != nil {
return nil, err
}
values = append(values, v)
i += n
}
return entries, nil
}
// findEntry returns the DICT entry for operator op, or nil when the operator is
// absent.
func findEntry(entries []dictRawEntry, op int) *dictRawEntry {
for i := range entries {
if entries[i].op == op {
return &entries[i]
}
}
return nil
}
// encodeCFFInt5 encodes v as a five-byte CFF DICT integer operand (marker 29 and
// a 32-bit big-endian value). A fixed five-byte width keeps the Top DICT the same
// size no matter what offset value it holds, which lets the layout be computed in
// one pass: the offsets an operand carries do not change the size of the DICT that
// carries them.
func encodeCFFInt5(v int) []byte {
return []byte{29, byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
}
// encodeCFFOp encodes a DICT operator (a one-byte operator, or the escape byte 12
// followed by the second byte for a two-byte operator keyed as 1200+second).
func encodeCFFOp(op int) []byte {
if op >= 1200 {
return []byte{12, byte(op - 1200)}
}
return []byte{byte(op)}
}
// topDictOffsets carries the rebuilt Top DICT's offset operands. charset is -1
// when the original charset is predefined (its operand is copied unchanged);
// otherwise it is the new absolute offset of the relocated charset. hasPrivate is
// false when the font has no Private DICT (no op-18 entry is emitted then).
type topDictOffsets struct {
charset int
charStrings int
privateSize int
privateOff int
hasPrivate bool
charsetIsPre bool
}
// encodeTopDict re-emits the Top DICT, copying every operator verbatim except the
// ones that carry absolute offsets: CharStrings (17) and Private (18) get the new
// offsets, charset (15) gets its new offset unless it is predefined, and Encoding
// (16) is forced to the predefined Standard encoding (0) since a custom encoding
// is neither relocated nor needed for embedding. All rewritten offset operands use
// the fixed five-byte form, so the encoded size does not depend on the offsets.
func encodeTopDict(entries []dictRawEntry, off topDictOffsets) []byte {
var out []byte
for _, e := range entries {
switch e.op {
case 15: // charset
if off.charsetIsPre {
out = append(out, e.operands...)
} else {
out = append(out, encodeCFFInt5(off.charset)...)
}
out = append(out, encodeCFFOp(15)...)
case 16: // Encoding -> Standard
out = append(out, encodeCFFInt5(0)...)
out = append(out, encodeCFFOp(16)...)
case 17: // CharStrings
out = append(out, encodeCFFInt5(off.charStrings)...)
out = append(out, encodeCFFOp(17)...)
case 18: // Private [size offset]
out = append(out, encodeCFFInt5(off.privateSize)...)
out = append(out, encodeCFFInt5(off.privateOff)...)
out = append(out, encodeCFFOp(18)...)
default:
out = append(out, e.operands...)
out = append(out, encodeCFFOp(e.op)...)
}
}
return out
}
// writeCFFIndex encodes items as a CFF INDEX (a count, an offset size, count+1
// relative offsets, then the concatenated items). An empty item list encodes as a
// count of zero, the shortest valid INDEX.
func writeCFFIndex(items [][]byte) []byte {
count := len(items)
out := []byte{byte(count >> 8), byte(count)}
if count == 0 {
return out
}
offs := make([]int, count+1)
offs[0] = 1
for i, it := range items {
offs[i+1] = offs[i] + len(it)
}
offSize := 1
for v := offs[count]; v > 0xFF; v >>= 8 {
offSize++
}
out = append(out, byte(offSize))
for _, o := range offs {
for b := offSize - 1; b >= 0; b-- {
out = append(out, byte(o>>(8*b)))
}
}
for _, it := range items {
out = append(out, it...)
}
return out
}
// cffIndexEnd returns the byte offset just past the CFF INDEX beginning at pos.
func cffIndexEnd(data []byte, pos int) (int, error) {
r := &reader{b: data, pos: pos}
if _, err := parseIndex(r); err != nil {
return 0, err
}
return r.pos, nil
}
// cffCharsetLength returns the byte length of the (non-predefined) charset at off
// for a font of nGlyphs glyphs, decoding format 0, 1 or 2 exactly as parseCharset
// walks them.
func cffCharsetLength(data []byte, off, nGlyphs int) (int, error) {
if off > len(data) {
return 0, fmt.Errorf("opentype: cff charset offset out of range")
}
r := &reader{b: data, pos: off}
format := r.u8()
gid := 1
switch format {
case 0:
for gid < nGlyphs {
r.u16()
gid++
}
case 1:
for gid < nGlyphs {
r.u16()
gid += int(r.u8()) + 1
}
case 2:
for gid < nGlyphs {
r.u16()
gid += int(r.u16()) + 1
}
default:
return 0, fmt.Errorf("opentype: cff charset: unsupported format %d", format)
}
if r.err != nil {
return 0, fmt.Errorf("opentype: cff charset: %w", r.err)
}
return r.pos - off, nil
}
// cffPrivateBlob returns a copy of the Private DICT at [privOff, privOff+privSize)
// together with its Local Subr INDEX when it declares one, as a single contiguous
// blob. The blob preserves the byte distance between the Private DICT and its
// local subrs, so the DICT's (relative) Subrs offset stays valid after the blob is
// relocated as a whole.
func cffPrivateBlob(data []byte, privOff, privSize int) ([]byte, error) {
if privOff < 0 || privSize < 0 || privOff+privSize > len(data) {
return nil, fmt.Errorf("opentype: cff: Private DICT out of range")
}
priv, err := parseDict(data[privOff : privOff+privSize])
if err != nil {
return nil, err
}
soff, ok := dictInt(priv, 19, 0)
if !ok {
return append([]byte(nil), data[privOff:privOff+privSize]...), nil
}
subrAbs := privOff + soff
if subrAbs < 0 || subrAbs > len(data) {
return nil, fmt.Errorf("opentype: cff: Local Subrs offset out of range")
}
end, err := cffIndexEnd(data, subrAbs)
if err != nil {
return nil, err
}
return append([]byte(nil), data[privOff:end]...), nil
}
// SubsetCFF builds a subset 'CFF ' table (the raw font program, as a PDF /FontFile3
// CIDFontType0C stream embeds it) that keeps the charstrings of the glyphs in gids
// plus glyph 0, replacing every other glyph's charstring with an empty one. Glyph
// numbering is preserved, so the font's charset, encoding, Private DICT and
// subroutines remain valid and a PDF embeds the result with an Identity CIDToGIDMap
// (no glyph-id remap is needed, unlike SubsetTrueType).
//
// It returns an error for a font with no CFF table (a TrueType font — use
// SubsetTrueType), for a CFF2 (variable) font, for a CID-keyed CFF (one with a ROS,
// FDArray or FDSelect operator), or when a requested glyph id is out of range. See
// the file-level comment for the retained-subroutine and endchar-seac scope notes.
func (f *Font) SubsetCFF(gids []GlyphIndex) ([]byte, error) {
if f.cff2 != nil {
return nil, fmt.Errorf("opentype: SubsetCFF: CFF2 (variable) fonts are not supported; bake an instance first")
}
if f.cff == nil {
return nil, fmt.Errorf("opentype: SubsetCFF: font has no CFF table (not a CFF/OpenType font)")
}
// f.cff being non-nil means Parse decoded a 'CFF ' table, which it also
// retained in f.tables, so the lookup always succeeds here.
return subsetCFFTable(f.tables["CFF "], f.cff, gids)
}
// subsetCFFTable rebuilds the CFF table bytes: header and the Name, String and
// Global Subr INDEXes are copied verbatim; the Top DICT is re-emitted with new
// offsets; the CharStrings INDEX is rebuilt keeping the requested glyphs; and the
// (relocated) charset and Private+Local-Subr blobs are appended. The layout is
// header, Name INDEX, Top DICT INDEX, String INDEX, Global Subr INDEX, charset,
// CharStrings INDEX, Private+Local Subrs.
func subsetCFFTable(data []byte, cff *cffTable, gids []GlyphIndex) ([]byte, error) {
if len(data) < 4 {
return nil, fmt.Errorf("opentype: cff header: %w", errTruncated)
}
hdrSize := int(data[2])
if hdrSize < 4 || hdrSize > len(data) {
return nil, fmt.Errorf("opentype: cff: bad hdrSize %d", hdrSize)
}
// Re-walk the top-level INDEXes to recover their raw byte spans (Name, Top
// DICT, String, Global Subr), which are copied unchanged into the subset.
r := &reader{b: data, pos: hdrSize}
nameStart := r.pos
if _, err := parseIndex(r); err != nil {
return nil, err
}
topStart := r.pos
topDicts, err := parseIndex(r)
if err != nil {
return nil, err
}
if len(topDicts) < 1 {
return nil, fmt.Errorf("opentype: cff: empty Top DICT INDEX")
}
strStart := r.pos
if _, err := parseIndex(r); err != nil {
return nil, err
}
gsubrStart := r.pos
if _, err := parseIndex(r); err != nil {
return nil, err
}
gsubrEnd := r.pos
nameRaw := data[nameStart:topStart]
strRaw := data[strStart:gsubrStart]
gsubrRaw := data[gsubrStart:gsubrEnd]
entries, err := parseDictRaw(topDicts[0])
if err != nil {
return nil, err
}
// CID-keyed CFF (ROS/FDArray/FDSelect) is out of scope: its charset and
// FDSelect index the same glyph ids, and its Font DICTs carry their own
// Private offsets, none of which this preserve-numbering subsetter rewrites.
if findEntry(entries, 1230) != nil {
return nil, fmt.Errorf("opentype: SubsetCFF: CID-keyed CFF is not supported")
}
if findEntry(entries, 1236) != nil {
return nil, fmt.Errorf("opentype: SubsetCFF: CID-keyed CFF (FDArray) is not supported")
}
n := len(cff.charStrings)
kept := map[GlyphIndex]bool{0: true}
for _, g := range gids {
if int(g) >= n {
return nil, fmt.Errorf("opentype: SubsetCFF: glyph index %d out of range", int(g))
}
kept[g] = true
}
items := make([][]byte, n)
for gid := 0; gid < n; gid++ {
if kept[GlyphIndex(gid)] {
items[gid] = cff.charStrings[gid]
} else {
items[gid] = []byte{14} // endchar: an empty glyph
}
}
newCharStrings := writeCFFIndex(items)
// Resolve the charset: predefined (offset <= 2) charsets are copied by operand
// and need no blob; a custom charset is relocated and its length measured.
var charsetBlob []byte
offsets := topDictOffsets{charset: -1, charsetIsPre: true}
if e := findEntry(entries, 15); e != nil && len(e.values) > 0 && int(e.values[0]) > 2 {
csOff := int(e.values[0])
csLen, err := cffCharsetLength(data, csOff, n)
if err != nil {
return nil, err
}
charsetBlob = append([]byte(nil), data[csOff:csOff+csLen]...)
offsets.charsetIsPre = false
}
// Resolve the Private DICT and its Local Subrs into one relocatable blob.
var privBlob []byte
if pv := findEntry(entries, 18); pv != nil {
if len(pv.values) < 2 {
return nil, fmt.Errorf("opentype: cff: bad Private DICT entry")
}
privSize, privOff := int(pv.values[0]), int(pv.values[1])
privBlob, err = cffPrivateBlob(data, privOff, privSize)
if err != nil {
return nil, err
}
offsets.hasPrivate = true
offsets.privateSize = privSize
}
// Measure the Top DICT INDEX (fixed size thanks to five-byte offset operands),
// then compute every downstream offset and assemble.
topDictSize := len(writeCFFIndex([][]byte{encodeTopDict(entries, offsets)}))
pos := hdrSize + len(nameRaw) + topDictSize + len(strRaw) + len(gsubrRaw)
if !offsets.charsetIsPre {
offsets.charset = pos
pos += len(charsetBlob)
}
offsets.charStrings = pos
pos += len(newCharStrings)
if offsets.hasPrivate {
offsets.privateOff = pos
}
topDict := encodeTopDict(entries, offsets)
out := append([]byte(nil), data[:hdrSize]...)
out = append(out, nameRaw...)
out = append(out, writeCFFIndex([][]byte{topDict})...)
out = append(out, strRaw...)
out = append(out, gsubrRaw...)
out = append(out, charsetBlob...)
out = append(out, newCharStrings...)
out = append(out, privBlob...)
return out, nil
}
+122
View File
@@ -0,0 +1,122 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import "fmt"
// parseHead reads unitsPerEm and indexToLocFormat from the head table.
// The full head table is 54 bytes; indexToLocFormat sits at offset 50.
func (f *Font) parseHead(b []byte) error {
if len(b) < 54 {
return fmt.Errorf("opentype: head table: %w", errTruncated)
}
f.unitsPerEm = int(be16(b[18:]))
if f.unitsPerEm == 0 {
return fmt.Errorf("opentype: head table: unitsPerEm is zero")
}
f.macStyle = be16(b[44:])
f.xMin = int(sbe16(b[36:]))
f.yMin = int(sbe16(b[38:]))
f.xMax = int(sbe16(b[40:]))
f.yMax = int(sbe16(b[42:]))
f.indexToLocFormat = int(sbe16(b[50:]))
if f.indexToLocFormat != 0 && f.indexToLocFormat != 1 {
return fmt.Errorf("opentype: head table: bad indexToLocFormat %d", f.indexToLocFormat)
}
return nil
}
// parseMaxp reads numGlyphs from the maxp table (offset 4).
func (f *Font) parseMaxp(b []byte) error {
if len(b) < 6 {
return fmt.Errorf("opentype: maxp table: %w", errTruncated)
}
f.numGlyphs = int(be16(b[4:]))
if f.numGlyphs == 0 {
return fmt.Errorf("opentype: maxp table: zero glyphs")
}
// Version-1.0 maxp additionally carries the interpreter limits used by the
// hinting VM. A shorter (version-0.5) table leaves conservative defaults.
f.maxTwilightPts = 16
f.maxStorage = 64
f.maxFunctionDefs = 64
f.maxStackElements = 256
if len(b) >= 26 {
f.maxTwilightPts = int(be16(b[16:]))
f.maxStorage = int(be16(b[18:]))
f.maxFunctionDefs = int(be16(b[20:]))
f.maxStackElements = int(be16(b[24:]))
}
return nil
}
// parseHhea reads the vertical metrics and numberOfHMetrics from hhea.
// ascender/descender/lineGap are int16 at offsets 4/6/8; numberOfHMetrics is
// the uint16 at offset 34.
func (f *Font) parseHhea(b []byte) error {
if len(b) < 36 {
return fmt.Errorf("opentype: hhea table: %w", errTruncated)
}
f.ascender = int(sbe16(b[4:]))
f.descender = int(sbe16(b[6:]))
f.lineGap = int(sbe16(b[8:]))
f.numberOfHMetrics = int(be16(b[34:]))
if f.numberOfHMetrics == 0 || f.numberOfHMetrics > f.numGlyphs {
return fmt.Errorf("opentype: hhea table: bad numberOfHMetrics %d", f.numberOfHMetrics)
}
return nil
}
// numberOfHMetrics is kept only for the span of parseHhea→parseHmtx.
// It is declared on Font to pass the value between the two parsers.
// parseHmtx expands the hmtx table into per-glyph advanceWidth and left side
// bearing slices. The first numberOfHMetrics entries are (advance, lsb) pairs;
// the trailing glyphs share the last entry's advance and carry their own lsb.
func (f *Font) parseHmtx(b []byte) error {
n := f.numberOfHMetrics
need := 4*n + 2*(f.numGlyphs-n)
if len(b) < need {
return fmt.Errorf("opentype: hmtx table: %w", errTruncated)
}
f.advances = make([]int, f.numGlyphs)
f.lsbs = make([]int, f.numGlyphs)
lastAdvance := 0
for i := 0; i < f.numGlyphs; i++ {
if i < n {
lastAdvance = int(be16(b[i*4:]))
f.advances[i] = lastAdvance
f.lsbs[i] = int(sbe16(b[i*4+2:]))
} else {
f.advances[i] = lastAdvance
f.lsbs[i] = int(sbe16(b[4*n+2*(i-n):]))
}
}
return nil
}
// parseLoca decodes the loca table into numGlyphs+1 glyph offsets into glyf.
// Short format stores uint16 half-offsets (doubled); long format stores
// uint32 offsets directly, selected by head.indexToLocFormat.
func (f *Font) parseLoca(b []byte) error {
count := f.numGlyphs + 1
f.loca = make([]uint32, count)
if f.indexToLocFormat == 0 {
if len(b) < count*2 {
return fmt.Errorf("opentype: loca table (short): %w", errTruncated)
}
for i := 0; i < count; i++ {
f.loca[i] = uint32(be16(b[i*2:])) * 2
}
return nil
}
if len(b) < count*4 {
return fmt.Errorf("opentype: loca table (long): %w", errTruncated)
}
for i := 0; i < count; i++ {
f.loca[i] = be32(b[i*4:])
}
return nil
}
+467
View File
@@ -0,0 +1,467 @@
// Copyright (c) 2026, the go-opentype authors
// All rights reserved.
//
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import (
"bytes"
"fmt"
"math"
"strconv"
)
// type1Font is a PostScript Type 1 font program: a dictionary of charstrings
// addressed by name, the subroutines they call, and the encoding the program
// was written with. It is the format PDF calls FontFile, and the one every
// PostScript printer of the nineteen-nineties spoke.
type type1Font struct {
// names is every glyph, in the order they were read, which is the order
// their indices are handed out in. .notdef is put first when the font has
// one, so that glyph zero means what it means everywhere else.
names []string
charstrings map[string][]byte
byName map[string]GlyphIndex
subrs [][]byte
// encoding is what the program itself says a byte stands for; nil means
// it said to use the standard one.
encoding map[byte]string
fontMatrix [6]float64
}
// The three constants the eexec cipher is built on, and the two keys: one for
// the program's private half, one for each charstring inside it.
const (
eexecC1 = 52845
eexecC2 = 22719
eexecKey = 55665
charstringR = 4330
defaultLenIV = 4
)
// ParseType1 decodes a PostScript Type 1 font program — what a PDF carries as
// a FontFile, and what a .pfb or .pfa file holds. Such a program addresses its
// glyphs by name and carries an encoding of its own, so the result answers
// [Font.GlyphIndexByName] and [Font.GlyphIndexByCode] and reports no character
// map.
//
// The bytes are not retained: a Type 1 program is decrypted on the way in, so
// what is kept is the plaintext.
func ParseType1(b []byte) (*Font, error) {
clear, enc, err := type1Halves(b)
if err != nil {
return nil, err
}
private := eexecDecrypt(enc, eexecKey, 4)
if len(private) == 0 {
return nil, fmt.Errorf("opentype: type1: the private half is empty")
}
t1 := &type1Font{
charstrings: map[string][]byte{},
byName: map[string]GlyphIndex{},
fontMatrix: defaultFontMatrix,
}
t1.readClear(clear)
if err := t1.readPrivate(private); err != nil {
return nil, err
}
if len(t1.names) == 0 {
return nil, fmt.Errorf("opentype: type1: the font has no glyphs")
}
f := &Font{
t1: t1,
numGlyphs: len(t1.names),
unitsPerEm: unitsPerEmOf(t1.fontMatrix),
numberOfHMetrics: len(t1.names),
glyphNames: t1.byName,
}
f.ascender = f.unitsPerEm
f.advances, f.lsbs = t1.metrics()
return f, nil
}
// type1Halves separates the program's readable half from its encrypted one,
// whichever container they arrived in: the segments of a .pfb file, the hex of
// a .pfa one, or the plain concatenation a PDF stream holds.
func type1Halves(b []byte) (clear, enc []byte, err error) {
if len(b) >= 2 && b[0] == 0x80 {
if b, err = joinPFB(b); err != nil {
return nil, nil, err
}
}
i := bytes.Index(b, []byte("eexec"))
if i < 0 {
return nil, nil, fmt.Errorf("opentype: type1: the program has no eexec section")
}
clear = b[:i]
rest := b[i+len("eexec"):]
for len(rest) > 0 && isPSSpace(rest[0]) {
rest = rest[1:]
}
if isHexSection(rest) {
return clear, unhex(rest), nil
}
return clear, rest, nil
}
// joinPFB concatenates the segments of a .pfb file, which wraps each half in a
// six-byte header rather than running them together.
func joinPFB(b []byte) ([]byte, error) {
var out []byte
for len(b) >= 2 && b[0] == 0x80 {
switch b[1] {
case 3:
return out, nil
case 1, 2:
if len(b) < 6 {
return nil, fmt.Errorf("opentype: type1: a pfb segment header is cut short")
}
n := int(b[2]) | int(b[3])<<8 | int(b[4])<<16 | int(b[5])<<24
if n < 0 || 6+n > len(b) {
return nil, fmt.Errorf("opentype: type1: a pfb segment runs past the end")
}
out = append(out, b[6:6+n]...)
b = b[6+n:]
default:
return nil, fmt.Errorf("opentype: type1: unknown pfb segment %d", b[1])
}
}
return out, nil
}
// hexLookahead is how many bytes must all be hexadecimal digits before the
// section is read as hexadecimal. The specification says four, which is not
// enough: a binary section begins with four random bytes, and four random
// bytes are all hexadecimal digits about once in twenty thousand times — often
// enough to happen across a corpus, and when it does the whole font is lost.
// Sixteen makes that impossible while still holding for every real one, whose
// hexadecimal runs are lines long.
const hexLookahead = 16
// isHexSection reports whether the encrypted half was written as hexadecimal,
// which is what a .pfa file does.
func isHexSection(b []byte) bool {
n := 0
for _, c := range b {
if isPSSpace(c) {
continue
}
if !isHexDigit(c) {
return false
}
if n++; n == hexLookahead {
return true
}
}
return false
}
func isHexDigit(c byte) bool {
return c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F'
}
func isPSSpace(c byte) bool {
return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f' || c == 0
}
// unhex reads a hexadecimal section, ignoring whatever whitespace it was laid
// out with.
func unhex(b []byte) []byte {
out := make([]byte, 0, len(b)/2)
var hi byte
have := false
for _, c := range b {
if !isHexDigit(c) {
continue
}
v := hexVal(c)
if !have {
hi, have = v, true
continue
}
out = append(out, hi<<4|v)
have = false
}
return out
}
func hexVal(c byte) byte {
switch {
case c <= '9':
return c - '0'
case c <= 'F':
return c - 'A' + 10
default:
return c - 'a' + 10
}
}
// eexecDecrypt undoes the cipher a Type 1 program is written in, throwing away
// the leading bytes that exist only to randomise it.
func eexecDecrypt(data []byte, key uint16, skip int) []byte {
r := key
out := make([]byte, len(data))
for i, c := range data {
out[i] = c ^ byte(r>>8)
r = (uint16(c)+r)*eexecC1 + eexecC2
}
if len(out) < skip {
return nil
}
return out[skip:]
}
// readClear takes what the readable half says: the font's matrix, and the
// encoding it was written with.
func (t *type1Font) readClear(b []byte) {
if m, ok := readMatrix(b); ok {
t.fontMatrix = m
}
t.encoding = readType1Encoding(b)
}
// readMatrix reads /FontMatrix, which says how big the program's own units are.
func readMatrix(b []byte) ([6]float64, bool) {
var m [6]float64
i := bytes.Index(b, []byte("/FontMatrix"))
if i < 0 {
return m, false
}
open := bytes.IndexByte(b[i:], '[')
if open < 0 {
return m, false
}
close := bytes.IndexByte(b[i+open:], ']')
if close < 0 {
return m, false
}
fields := bytes.Fields(b[i+open+1 : i+open+close])
if len(fields) < 6 {
return m, false
}
for k := 0; k < 6; k++ {
v, err := strconv.ParseFloat(string(fields[k]), 64)
if err != nil {
return m, false
}
m[k] = v
}
if m[0] == 0 {
return m, false
}
return m, true
}
// readType1Encoding reads the /Encoding the program was written with: either a
// name saying to use the standard one, or an array built up by putting a name
// at a code at a time.
func readType1Encoding(b []byte) map[byte]string {
i := bytes.Index(b, []byte("/Encoding"))
if i < 0 {
return nil
}
rest := b[i+len("/Encoding"):]
// Whatever comes first decides: a name, or an array being built.
if j := bytes.Index(rest, []byte("StandardEncoding")); j >= 0 && j < 40 {
return nil
}
out := map[byte]string{}
for {
j := bytes.Index(rest, []byte("dup "))
if j < 0 {
break
}
rest = rest[j+4:]
fields := bytes.Fields(rest)
if len(fields) < 3 || len(fields[1]) < 2 || fields[1][0] != '/' || string(fields[2]) != "put" {
continue
}
code, err := strconv.Atoi(string(fields[0]))
if err != nil || code < 0 || code > 255 {
continue
}
out[byte(code)] = string(fields[1][1:])
}
if len(out) == 0 {
return nil
}
return out
}
// readPrivate takes what the encrypted half holds: how much of each charstring
// is padding, the subroutines, and the charstrings themselves.
func (t *type1Font) readPrivate(b []byte) error {
lenIV := defaultLenIV
if i := bytes.Index(b, []byte("/lenIV")); i >= 0 {
if fields := bytes.Fields(b[i+len("/lenIV"):]); len(fields) > 0 {
if v, err := strconv.Atoi(string(fields[0])); err == nil && v >= 0 && v < 16 {
lenIV = v
}
}
}
t.readSubrs(b, lenIV)
return t.readCharstrings(b, lenIV)
}
// readSubrs reads the /Subrs array: a numbered list of shared charstrings.
func (t *type1Font) readSubrs(b []byte, lenIV int) {
i := bytes.Index(b, []byte("/Subrs"))
if i < 0 {
return
}
rest := b[i+len("/Subrs"):]
fields := bytes.Fields(rest)
if len(fields) == 0 {
return
}
n, err := strconv.Atoi(string(fields[0]))
if err != nil || n < 0 || n > 65535 {
return
}
t.subrs = make([][]byte, n)
for k := 0; k < n; k++ {
j := bytes.Index(rest, []byte("dup "))
if j < 0 {
return
}
rest = rest[j+4:]
idx, data, next, ok := readBinaryEntry(rest, lenIV)
if !ok {
return
}
if num, err := strconv.Atoi(string(idx)); err == nil && num >= 0 && num < n {
t.subrs[num] = data
}
rest = next
}
}
// readCharstrings reads the /CharStrings dictionary: a glyph name and its
// charstring, over and over.
func (t *type1Font) readCharstrings(b []byte, lenIV int) error {
i := bytes.Index(b, []byte("/CharStrings"))
if i < 0 {
return fmt.Errorf("opentype: type1: the program has no CharStrings")
}
rest := b[i+len("/CharStrings"):]
// Past the dictionary's own preamble, every entry begins with a name.
if j := bytes.Index(rest, []byte("begin")); j >= 0 {
rest = rest[j+len("begin"):]
}
for {
j := bytes.IndexByte(rest, '/')
if j < 0 {
break
}
rest = rest[j+1:]
raw, data, next, ok := readBinaryEntry(rest, lenIV)
if !ok {
break
}
name := string(raw)
if _, seen := t.charstrings[name]; !seen && name != "" {
t.charstrings[name] = data
t.names = append(t.names, name)
}
rest = next
}
t.order()
return nil
}
// readBinaryEntry reads "<key> <length> RD <length bytes> ..." — the shape
// both a subroutine and a charstring arrive in. The operator that introduces
// the bytes is spelt RD by some programs and -| by others, and neither is a
// keyword worth insisting on: what matters is that a number is followed by
// something that is not a number, and then that many bytes.
func readBinaryEntry(b []byte, lenIV int) (key, data []byte, rest []byte, ok bool) {
name, after := psToken(b)
if name == nil {
return nil, nil, nil, false
}
lengthTok, after := psToken(after)
n, err := strconv.Atoi(string(lengthTok))
if err != nil || n < 0 || n > len(after) {
return nil, nil, nil, false
}
_, after = psToken(after) // RD, -|, or whatever this program calls it
if len(after) < 1+n {
return nil, nil, nil, false
}
// Exactly one space separates the operator from the bytes.
raw := after[1 : 1+n]
return name, eexecDecrypt(raw, charstringR, lenIV), after[1+n:], true
}
// psToken reads one whitespace-delimited token.
func psToken(b []byte) (tok, rest []byte) {
i := 0
for i < len(b) && isPSSpace(b[i]) {
i++
}
start := i
for i < len(b) && !isPSSpace(b[i]) {
i++
}
if start == i {
return nil, b[i:]
}
return b[start:i], b[i:]
}
// order puts .notdef first, so that glyph zero means what it means in every
// other format, and indexes the rest where they fell.
func (t *type1Font) order() {
for i, n := range t.names {
if n == ".notdef" && i != 0 {
t.names = append([]string{n}, append(t.names[:i:i], t.names[i+1:]...)...)
break
}
}
for i, n := range t.names {
t.byName[n] = GlyphIndex(i)
}
}
// metrics is what each glyph's charstring says it is wide, which for a Type 1
// program is the operand of its very first operator.
func (t *type1Font) metrics() (advances, lsbs []int) {
advances = make([]int, len(t.names))
lsbs = make([]int, len(t.names))
for i, name := range t.names {
sb, w, ok := type1Width(t.charstrings[name])
if !ok {
continue
}
advances[i] = int(math.Round(w))
lsbs[i] = int(math.Round(sb))
}
return advances, lsbs
}
// glyphByCode maps a byte through the encoding the program was written with.
func (t *type1Font) glyphByCode(code byte) (GlyphIndex, bool) {
name, ok := t.encoding[code]
if !ok {
if t.encoding != nil {
return 0, false
}
sid, in := standardEncodingSID(int(code))
if !in || int(sid) >= nStdStrings {
return 0, false
}
name = cffStandardStrings[sid]
}
gid, ok := t.byName[name]
return gid, ok
}
// glyphName is what the program calls a glyph.
func (t *type1Font) glyphName(gid int) (string, bool) {
if gid < 0 || gid >= len(t.names) {
return "", false
}
return t.names[gid], true
}
+493
View File
@@ -0,0 +1,493 @@
// Copyright (c) 2026, the go-opentype authors
// All rights reserved.
//
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import "fmt"
// The Type 1 charstring operators. They are not the Type 2 ones: the two
// formats share a lineage and very little else.
const (
t1HStem = 1
t1VStem = 3
t1VMoveTo = 4
t1RLineTo = 5
t1HLineTo = 6
t1VLineTo = 7
t1RRCurveTo = 8
t1ClosePath = 9
t1CallSubr = 10
t1Return = 11
t1Escape = 12
t1HSbw = 13
t1EndChar = 14
t1RMoveTo = 21
t1HMoveTo = 22
t1VHCurveTo = 30
t1HVCurveTo = 31
)
// The escaped operators, reached through operator 12.
const (
t1DotSection = 0
t1VStem3 = 1
t1HStem3 = 2
t1Seac = 6
t1Sbw = 7
t1Div = 12
t1CallOtherSubr = 16
t1Pop = 17
t1SetCurrentPoint = 33
maxT1Depth = 30
maxT1Stack = 48
maxT1PostScriptSize = 32
)
// t1machine runs one Type 1 charstring.
type t1machine struct {
f *type1Font
stack []float64
ps []float64 // the interpreter's other stack, which othersubrs use
contours []contour
cur contour
x, y float64
sbx, sby float64
width float64
haveWidth bool
// flex collects the seven points an othersubr-driven curve is built from;
// while it is running, a move records a point instead of starting a
// contour.
inFlex bool
flexPts []outlinePoint
seacDepth int
done bool
}
// outline draws a glyph of a Type 1 program.
func (t *type1Font) outline(gid int) ([]contour, error) {
name, ok := t.glyphName(gid)
if !ok {
return nil, fmt.Errorf("opentype: type1: glyph index %d out of range", gid)
}
return t.outlineByName(name, 0)
}
// outlineByName draws the glyph a name stands for.
func (t *type1Font) outlineByName(name string, seacDepth int) ([]contour, error) {
cs, ok := t.charstrings[name]
if !ok {
return nil, fmt.Errorf("opentype: type1: no glyph called %q", name)
}
m := &t1machine{f: t, seacDepth: seacDepth}
if err := m.run(cs, 0); err != nil {
return nil, err
}
m.finishContour()
return m.contours, nil
}
// type1Width reads what a charstring says its glyph is wide, which is the
// operand of hsbw or sbw — always the first operator of a Type 1 glyph.
func type1Width(cs []byte) (sb, w float64, ok bool) {
var stack []float64
for i := 0; i < len(cs); {
v, next, isNum, valid := t1Operand(cs, i)
if !valid {
return 0, 0, false
}
if isNum {
stack = append(stack, v)
i = next
continue
}
switch cs[i] {
case t1HSbw:
if len(stack) < 2 {
return 0, 0, false
}
return stack[0], stack[1], true
case t1Escape:
if i+1 < len(cs) && cs[i+1] == t1Sbw {
if len(stack) < 4 {
return 0, 0, false
}
return stack[0], stack[2], true
}
return 0, 0, false
default:
return 0, 0, false
}
}
return 0, 0, false
}
// t1Operand reads one charstring byte, saying whether it was a number and
// where the next one is.
func t1Operand(cs []byte, i int) (v float64, next int, isNum, ok bool) {
b := cs[i]
switch {
case b >= 32 && b <= 246:
return float64(int(b) - 139), i + 1, true, true
case b >= 247 && b <= 250:
if i+1 >= len(cs) {
return 0, 0, false, false
}
return float64((int(b)-247)*256 + int(cs[i+1]) + 108), i + 2, true, true
case b >= 251 && b <= 254:
if i+1 >= len(cs) {
return 0, 0, false, false
}
return float64(-(int(b)-251)*256 - int(cs[i+1]) - 108), i + 2, true, true
case b == 255:
if i+4 >= len(cs) {
return 0, 0, false, false
}
n := int32(uint32(cs[i+1])<<24 | uint32(cs[i+2])<<16 | uint32(cs[i+3])<<8 | uint32(cs[i+4]))
return float64(n), i + 5, true, true
}
return 0, i, false, true
}
// run executes a charstring or a subroutine.
func (m *t1machine) run(cs []byte, depth int) error {
if depth > maxT1Depth {
return fmt.Errorf("opentype: type1: subroutine nesting too deep")
}
for i := 0; i < len(cs); {
v, next, isNum, ok := t1Operand(cs, i)
if !ok {
return fmt.Errorf("opentype: type1: a charstring operand runs past the end")
}
if isNum {
if len(m.stack) >= maxT1Stack {
return fmt.Errorf("opentype: type1: too many operands")
}
m.stack = append(m.stack, v)
i = next
continue
}
op := cs[i]
i++
if op == t1Escape {
if i >= len(cs) {
return fmt.Errorf("opentype: type1: a charstring ends in an escape")
}
esc := cs[i]
i++
stop, err := m.escaped(esc, depth)
if err != nil {
return err
}
if stop {
return nil
}
continue
}
stop, err := m.operator(op, depth)
if err != nil {
return err
}
if stop {
return nil
}
}
return nil
}
// operator runs one plain operator, reporting whether the charstring is over.
func (m *t1machine) operator(op byte, depth int) (bool, error) {
s := m.stack
switch op {
case t1HStem, t1VStem:
m.clear()
case t1HSbw:
if len(s) >= 2 {
m.sbx, m.width, m.haveWidth = s[0], s[1], true
m.x, m.y = m.sbx, 0
}
m.clear()
case t1RMoveTo:
if len(s) >= 2 {
m.moveTo(s[len(s)-2], s[len(s)-1])
}
m.clear()
case t1HMoveTo:
if len(s) >= 1 {
m.moveTo(s[len(s)-1], 0)
}
m.clear()
case t1VMoveTo:
if len(s) >= 1 {
m.moveTo(0, s[len(s)-1])
}
m.clear()
case t1RLineTo:
if len(s) >= 2 {
m.lineTo(s[0], s[1])
}
m.clear()
case t1HLineTo:
if len(s) >= 1 {
m.lineTo(s[0], 0)
}
m.clear()
case t1VLineTo:
if len(s) >= 1 {
m.lineTo(0, s[0])
}
m.clear()
case t1RRCurveTo:
if len(s) >= 6 {
m.rrcurveTo(s[0], s[1], s[2], s[3], s[4], s[5])
}
m.clear()
case t1HVCurveTo:
if len(s) >= 4 {
m.rrcurveTo(s[0], 0, s[1], s[2], 0, s[3])
}
m.clear()
case t1VHCurveTo:
if len(s) >= 4 {
m.rrcurveTo(0, s[0], s[1], s[2], s[3], 0)
}
m.clear()
case t1ClosePath:
m.closePath()
m.clear()
case t1CallSubr:
return false, m.callSubr(depth)
case t1Return:
return true, nil
case t1EndChar:
m.done = true
return true, nil
default:
// An operator this format has not got: the rest of the charstring
// cannot be trusted, but what has been drawn so far can.
return true, nil
}
return false, nil
}
// escaped runs one of the operators reached through the escape byte.
func (m *t1machine) escaped(esc byte, depth int) (bool, error) {
s := m.stack
switch esc {
case t1DotSection, t1VStem3, t1HStem3:
m.clear()
case t1Sbw:
if len(s) >= 4 {
m.sbx, m.sby, m.width, m.haveWidth = s[0], s[1], s[2], true
m.x, m.y = m.sbx, m.sby
}
m.clear()
case t1Div:
if len(s) >= 2 && s[len(s)-1] != 0 {
m.stack = append(s[:len(s)-2], s[len(s)-2]/s[len(s)-1])
}
case t1Seac:
return true, m.seac()
case t1CallOtherSubr:
return false, m.callOtherSubr()
case t1Pop:
if len(m.ps) > 0 {
m.stack = append(m.stack, m.ps[len(m.ps)-1])
m.ps = m.ps[:len(m.ps)-1]
} else {
m.stack = append(m.stack, 0)
}
case t1SetCurrentPoint:
if len(s) >= 2 {
m.x, m.y = s[0], s[1]
}
m.clear()
default:
return true, nil
}
return false, nil
}
// callSubr runs a numbered subroutine.
func (m *t1machine) callSubr(depth int) error {
if len(m.stack) == 0 {
return nil
}
n := int(m.stack[len(m.stack)-1])
m.stack = m.stack[:len(m.stack)-1]
if n < 0 || n >= len(m.f.subrs) || m.f.subrs[n] == nil {
return nil
}
return m.run(m.f.subrs[n], depth+1)
}
// callOtherSubr handles the four subroutines a Type 1 program calls out to the
// interpreter for. Numbers 0 to 2 build a flex — a curve so shallow it is
// drawn as one — and number 3 asks which hints to use, which nothing here
// needs. Anything else is answered by handing its arguments straight back, so
// that a program using an othersubr of its own carries on rather than stops.
func (m *t1machine) callOtherSubr() error {
if len(m.stack) < 2 {
m.clear()
return nil
}
which := int(m.stack[len(m.stack)-1])
n := int(m.stack[len(m.stack)-2])
m.stack = m.stack[:len(m.stack)-2]
if n < 0 || n > len(m.stack) {
m.clear()
return nil
}
args := m.stack[len(m.stack)-n:]
m.stack = m.stack[:len(m.stack)-n]
switch which {
case 0: // the flex is over: draw the two curves its seven points describe
m.endFlex()
// The convention is that the two coordinates it ends at are read back
// with two pops.
m.ps = append(m.ps[:0], m.y, m.x)
case 1: // a flex begins
m.inFlex = true
m.flexPts = m.flexPts[:0]
case 2: // a flex point has been collected by the move that followed
case 3:
// Hint replacement. The program pushes the number of the subroutine
// holding the hints it wants, and expects to read it straight back
// with a pop and then call it. Handing back anything else — the
// othersubr's own number, say — calls the wrong subroutine, and the
// glyph comes out empty with nothing to say why.
if len(args) > 0 {
m.ps = append(m.ps[:0], args[0])
} else {
m.ps = append(m.ps[:0], 3)
}
default:
// Hand the arguments back in the order the pops will take them.
m.ps = m.ps[:0]
for i := len(args) - 1; i >= 0; i-- {
m.ps = append(m.ps, args[i])
}
if len(m.ps) > maxT1PostScriptSize {
m.ps = m.ps[:maxT1PostScriptSize]
}
}
return nil
}
// endFlex turns the seven collected points into the two curves they stand for.
func (m *t1machine) endFlex() {
m.inFlex = false
if len(m.flexPts) < 7 {
return
}
p := m.flexPts[len(m.flexPts)-7:]
// The first of the seven is the reference point the shape is measured
// against and is not drawn.
m.curveTo(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y)
m.curveTo(p[4].x, p[4].y, p[5].x, p[5].y, p[6].x, p[6].y)
}
// seac composes an accented glyph out of two others, named by their Standard
// Encoding codes.
func (m *t1machine) seac() error {
if m.seacDepth >= maxSeacDepth {
return fmt.Errorf("opentype: type1: accent composition nesting too deep")
}
s := m.stack
if len(s) < 5 {
m.clear()
return nil
}
asb, adx, ady, bchar, achar := s[0], s[1], s[2], int(s[3]), int(s[4])
m.clear()
base, err := m.component(bchar)
if err != nil {
return err
}
accent, err := m.component(achar)
if err != nil {
return err
}
m.finishContour()
m.contours = append(m.contours, base...)
// The accent is placed relative to the base's own left side bearing.
dx, dy := m.sbx-asb+adx, ady
for _, c := range accent {
nc := make(contour, len(c))
for i, p := range c {
nc[i] = outlinePoint{x: p.x + dx, y: p.y + dy, on: p.on}
}
m.contours = append(m.contours, nc)
}
m.done = true
return nil
}
// component draws one half of an accented glyph, named by a Standard Encoding
// code.
func (m *t1machine) component(code int) ([]contour, error) {
sid, ok := standardEncodingSID(code)
if !ok || int(sid) >= nStdStrings {
return nil, fmt.Errorf("opentype: type1: code %d is not in the standard encoding", code)
}
return m.f.outlineByName(cffStandardStrings[sid], m.seacDepth+1)
}
// clear empties the operand stack, which every operator that consumes one does.
func (m *t1machine) clear() { m.stack = m.stack[:0] }
// moveTo begins a contour, or collects a flex point while one is being built.
func (m *t1machine) moveTo(dx, dy float64) {
m.x += dx
m.y += dy
if m.inFlex {
m.flexPts = append(m.flexPts, outlinePoint{x: m.x, y: m.y, on: true})
return
}
m.finishContour()
m.cur = contour{{x: m.x, y: m.y, on: true}}
}
// lineTo adds a straight segment.
func (m *t1machine) lineTo(dx, dy float64) {
m.x += dx
m.y += dy
m.cur = append(m.cur, outlinePoint{x: m.x, y: m.y, on: true})
}
// rrcurveTo adds a curve given by three relative steps.
func (m *t1machine) rrcurveTo(dx1, dy1, dx2, dy2, dx3, dy3 float64) {
x1, y1 := m.x+dx1, m.y+dy1
x2, y2 := x1+dx2, y1+dy2
m.curveTo(x1, y1, x2, y2, x2+dx3, y2+dy3)
}
// curveTo flattens a cubic curve into the on-curve points a contour is made of.
func (m *t1machine) curveTo(x1, y1, x2, y2, x3, y3 float64) {
x0, y0 := m.x, m.y
for k := 1; k <= cffCurveSteps; k++ {
t := float64(k) / float64(cffCurveSteps)
u := 1 - t
bx := u*u*u*x0 + 3*u*u*t*x1 + 3*u*t*t*x2 + t*t*t*x3
by := u*u*u*y0 + 3*u*u*t*y1 + 3*u*t*t*y2 + t*t*t*y3
m.cur = append(m.cur, outlinePoint{x: bx, y: by, on: true})
}
m.x, m.y = x3, y3
}
// closePath ends the contour in progress. A Type 1 program says so out loud,
// where later formats leave it implied.
func (m *t1machine) closePath() {
m.finishContour()
}
// finishContour puts the contour under construction with the others.
func (m *t1machine) finishContour() {
if len(m.cur) > 0 {
m.contours = append(m.contours, m.cur)
m.cur = nil
}
}
+193
View File
@@ -0,0 +1,193 @@
// Copyright (c) 2026 the go-opentype/opentype authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package opentype
import "fmt"
// This file adds vertical writing-mode metrics: the data needed to lay glyphs
// out top-to-bottom for CJK tategaki and other vertical text. Three optional
// tables carry it:
//
// - vhea: the vertical header (ascender/descender/lineGap for vertical layout
// plus numOfLongVerMetrics, the horizontal analogue of hhea);
// - vmtx: per-glyph advanceHeight and topSideBearing (analogous to hmtx);
// - VORG: the Vertical Origin table, a default and per-glyph y coordinate of
// each glyph's vertical origin.
//
// Absence of these tables is not an error; it simply means vertical metrics are
// unavailable (HasVerticalMetrics reports false) and the caller falls back to
// the em square.
//
// Selecting vertical GLYPH FORMS (the OpenType 'vert'/'vrt2' features that swap
// horizontal glyphs for their upright vertical variants) is a shaping concern
// handled through GSUB (see gsub.go and Face.Shape). This file supplies the
// vertical METRICS and origin those laid-out glyphs are positioned with; it does
// not itself perform substitution.
// parseVertical decodes the optional vhea, vmtx and VORG tables. Each is
// independent, but vmtx is meaningful only alongside vhea (which supplies
// numOfLongVerMetrics), so a vmtx without a vhea is ignored. A malformed table
// that is present is reported as an error so a corrupt font fails cleanly.
func (f *Font) parseVertical(tables map[string][]byte) error {
if b, ok := tables["vhea"]; ok {
if err := f.parseVhea(b); err != nil {
return err
}
}
if b, ok := tables["vmtx"]; ok && f.hasVhea {
if err := f.parseVmtx(b); err != nil {
return err
}
}
if b, ok := tables["VORG"]; ok {
if err := f.parseVORG(b); err != nil {
return err
}
}
return nil
}
// parseVhea reads the vertical metrics and numOfLongVerMetrics from vhea. Its
// layout mirrors hhea: vertTypoAscender/Descender/LineGap are int16 at offsets
// 4/6/8 and numOfLongVerMetrics is the uint16 at offset 34.
func (f *Font) parseVhea(b []byte) error {
if len(b) < 36 {
return fmt.Errorf("opentype: vhea table: %w", errTruncated)
}
f.vertAscender = int(sbe16(b[4:]))
f.vertDescender = int(sbe16(b[6:]))
f.vertLineGap = int(sbe16(b[8:]))
f.numOfLongVerMetrics = int(be16(b[34:]))
if f.numOfLongVerMetrics == 0 || f.numOfLongVerMetrics > f.numGlyphs {
return fmt.Errorf("opentype: vhea table: bad numOfLongVerMetrics %d", f.numOfLongVerMetrics)
}
f.hasVhea = true
return nil
}
// parseVmtx expands the vmtx table into per-glyph advanceHeight and top side
// bearing slices, mirroring parseHmtx. The first numOfLongVerMetrics entries are
// (advanceHeight, topSideBearing) pairs; the trailing glyphs share the last
// entry's advance and carry their own top side bearing.
func (f *Font) parseVmtx(b []byte) error {
n := f.numOfLongVerMetrics
need := 4*n + 2*(f.numGlyphs-n)
if len(b) < need {
return fmt.Errorf("opentype: vmtx table: %w", errTruncated)
}
f.vertAdvances = make([]int, f.numGlyphs)
f.tsbs = make([]int, f.numGlyphs)
lastAdvance := 0
for i := 0; i < f.numGlyphs; i++ {
if i < n {
lastAdvance = int(be16(b[i*4:]))
f.vertAdvances[i] = lastAdvance
f.tsbs[i] = int(sbe16(b[i*4+2:]))
} else {
f.vertAdvances[i] = lastAdvance
f.tsbs[i] = int(sbe16(b[4*n+2*(i-n):]))
}
}
f.hasVmtx = true
return nil
}
// parseVORG decodes the Vertical Origin table: an 8-byte header carrying the
// defaultVertOriginY (int16 at offset 4) and numVertOriginYMetrics (uint16 at
// offset 6), followed by that many 4-byte {glyphIndex uint16, vertOriginY int16}
// records overriding the default for specific glyphs.
func (f *Font) parseVORG(b []byte) error {
if len(b) < 8 {
return fmt.Errorf("opentype: VORG table: %w", errTruncated)
}
f.vorgDefault = int(sbe16(b[4:]))
count := int(be16(b[6:]))
if len(b) < 8+4*count {
return fmt.Errorf("opentype: VORG table: %w", errTruncated)
}
f.vorg = make(map[GlyphIndex]int, count)
for i := 0; i < count; i++ {
rec := b[8+4*i:]
f.vorg[GlyphIndex(be16(rec))] = int(sbe16(rec[2:]))
}
f.hasVORG = true
return nil
}
// HasVerticalMetrics reports whether the font carries the vertical header and
// per-glyph vertical advances (vhea and vmtx) required to lay text out
// top-to-bottom. When it returns false, VerticalAdvance falls back to the em
// square and VerticalMetrics returns zeroes.
func (f *Font) HasVerticalMetrics() bool {
return f.hasVhea && f.hasVmtx
}
// verticalAdvance returns glyph gid's advance height in font units. When the
// font has no vmtx table it falls back to one em (unitsPerEm), the conventional
// full-width advance for vertical CJK layout.
func (f *Font) verticalAdvance(gid GlyphIndex) int {
if !f.hasVmtx {
return f.unitsPerEm
}
return f.vertAdvances[gid]
}
// verticalOriginY returns glyph gid's vertical origin y in font units and
// whether the font supplies one (a VORG table). A glyph without its own record
// takes the table's default vertical origin.
func (f *Font) verticalOriginY(gid GlyphIndex) (int, bool) {
if !f.hasVORG {
return 0, false
}
if y, ok := f.vorg[gid]; ok {
return y, true
}
return f.vorgDefault, true
}
// VerticalAdvance returns the vertical advance (advance height) of r in pixels,
// or 0 if the rune is not mapped by the font's cmap. When the font has no vmtx
// table the advance is one em, scaled to the face's size.
//
// This is the top-to-bottom analogue of Advance. Choosing the upright vertical
// glyph form for r (the 'vert'/'vrt2' features) is a separate shaping step done
// through GSUB; VerticalAdvance measures whichever glyph the cmap maps.
func (fc *Face) VerticalAdvance(r rune) int {
gid, ok := fc.font.GlyphIndex(r)
if !ok {
return 0
}
adv := float64(fc.font.verticalAdvance(gid))
if fc.varCoords != nil {
adv += fc.font.verticalAdvanceDelta(int(gid), fc.varNorm)
}
return roundInt(adv * fc.scale)
}
// VerticalMetrics returns the face's vertical-layout line metrics in pixels: the
// vhea vertTypoAscender, vertTypoDescender and vertTypoLineGap scaled to the
// face's size. All three are zero when the font has no vhea table.
func (fc *Face) VerticalMetrics() (ascent, descent, lineGap int) {
s := fc.scale
return roundInt(float64(fc.font.vertAscender) * s),
roundInt(float64(fc.font.vertDescender) * s),
roundInt(float64(fc.font.vertLineGap) * s)
}
// VerticalOrigin returns the y coordinate of r's vertical origin in pixels and
// whether the font supplies one (a VORG table). The origin locates the glyph
// relative to the pen when advancing top-to-bottom; ok is false when r is
// unmapped or the font has no VORG table.
func (fc *Face) VerticalOrigin(r rune) (int, bool) {
gid, mapped := fc.font.GlyphIndex(r)
if !mapped {
return 0, false
}
y, ok := fc.font.verticalOriginY(gid)
if !ok {
return 0, false
}
return roundInt(float64(y) * fc.scale), true
}
+1
View File
@@ -0,0 +1 @@
coverage.out
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2026 the go-pdfkit/render authors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
+91
View File
@@ -0,0 +1,91 @@
# extract
[![CI](https://github.com/go-pdfkit/extract/actions/workflows/ci.yml/badge.svg)](https://github.com/go-pdfkit/extract/actions/workflows/ci.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/go-pdfkit/extract.svg)](https://pkg.go.dev/github.com/go-pdfkit/extract)
[![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](LICENSE)
[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](#how-it-is-checked)
Reads a PDF page back: the text on it, with where each piece of it sits, and
the images it places.
```go
text, err := extract.Text(doc, 1) // the page as words, lines put back together
runs, err := extract.Runs(doc, 1) // every piece, with its place and its size
imgs, err := extract.Images(doc, 1) // every picture, and where it lands
```
Pure Go, no C. It builds for `GOOS=js/wasm` and every architecture the fleet
targets.
## What a page actually holds
Not text. A page holds instructions for drawing glyphs, and the text is what
those glyphs were meant to say — which the document says only if it was asked
to. Where it does not, this says so rather than guessing: a run whose codes
could not be worked out comes back marked `Unreadable`, so a caller can tell
*this page says nothing* from *this page could not be read*.
Three things are tried, in order:
1. the font's `/ToUnicode` map, which is the document's own word on what its
text says;
2. the name its encoding gives the code — but only a name the document chose
itself, since a **symbolic** font read through an assumed encoding gives
the wrong letter with nothing to say so;
3. what the **embedded font program** calls the code, read with
[`go-opentype`](https://github.com/go-opentype/opentype) — which is the
honest answer for a symbolic font, and the reason a page of mathematics
comes back as mathematics.
Text drawn in the mode that puts no ink on the page — how a scanner writes
what it read underneath the picture it read it from — is returned and marked
`Invisible`.
## Images
A picture comes back as the file holds it, with the box it covers on the page.
A JPEG comes out as a JPEG: re-encoding it would lose something for nothing.
Turning the other kinds into pixels means reading their colour space, which is
[`render`](https://github.com/go-pdfkit/render)'s work rather than this
package's.
## How it is checked
100% of statements, including every branch that handles a page saying
something it should not.
Then on the corpus — 118 833 real PDFs, 121 946 pages:
```
pages read 121946
with text 94865 (77.8%)
saying nothing 27081, of which 4812 did draw glyphs
characters 33976414 (17272226 of them letters)
runs 10797045
unreadable 203607 (1.89%)
invisible 29917
images 300685
panics 0
```
The 22% of pages that say nothing are figures — this corpus is arXiv's, and
most of it is plots. Of the pages that *did* draw glyphs and still said
nothing, nearly all are Type 3 fonts with no `/ToUnicode`: dvips bitmap fonts
whose glyphs are called `a1` and `s32`, which no reader can turn back into
letters.
## The rest of the family
[**reader**](https://github.com/go-pdfkit/reader) reads and writes the format ·
[**ops**](https://github.com/go-pdfkit/ops) is the verbs and the `pdfops` command ·
[**render**](https://github.com/go-pdfkit/render) turns a page into pixels ·
[**pdffont**](https://github.com/go-pdfkit/pdffont) is what a document says about a font ·
[**extract**](https://github.com/go-pdfkit/extract) reads a page back ·
[**coedit**](https://github.com/go-pdfkit/coedit) is a PDF several people edit at once ·
[**app**](https://github.com/go-pdfkit/app) is the browser workbench.
Documentation for all of it: <https://go-pdfkit.github.io/docs/>
## Licence
BSD-3-Clause.
+176
View File
@@ -0,0 +1,176 @@
// Package extract reads a PDF page back: the text on it, with where each piece
// of it sits, and the images it places.
//
// A page does not hold text. It holds instructions for drawing glyphs, and the
// text is what those glyphs were meant to say — which the document says only
// if it was asked to. Where it does not, this says so rather than guessing:
// a run whose characters could not be worked out comes back marked, so that a
// caller can tell "this page says nothing" from "this page could not be read".
package extract
import (
"sort"
"strings"
"github.com/go-pdfkit/pdffont"
"github.com/go-pdfkit/reader"
)
// A Run is a piece of text drawn in one go: one font, one size, one place.
type Run struct {
// Text is what the run says.
Text string
// X and Y are where it starts, in the page's own coordinates — up from
// the bottom left corner, in points.
X, Y float64
// Width is how far the pen moved drawing it, and Size how tall the text
// is, both in points.
Width, Size float64
// Font is the name the page's resources give the font.
Font string
// Space is how wide a space would be here, which is what says whether a
// gap between two runs is a word break or only kerning.
Space float64
// Invisible reports text drawn in the mode that puts no ink on the page.
// It is how a scanner puts what it read underneath the picture it read it
// from, so it is text worth having — and worth knowing about.
Invisible bool
// Unreadable reports a run some of whose codes said nothing about which
// characters they were: the font carried no map and named its glyphs
// after numbers. What Text holds is then only the part that could be read.
Unreadable bool
}
// Text is everything the page says, in reading order, with the lines put back
// together.
func Text(d *reader.Document, page int) (string, error) {
runs, err := Runs(d, page)
if err != nil {
return "", err
}
return Assemble(runs), nil
}
// Runs is every piece of text on the page, in the order it was drawn.
func Runs(d *reader.Document, page int) ([]Run, error) {
e, err := walk(d, page, false)
if err != nil {
return nil, err
}
return e.runs, nil
}
// walk reads a page's content stream once, keeping what it finds.
//
// A page whose content decoded only part of the way is read as far as it went:
// 263 streams in 212 of the 1 633 real forms in the corpus cannot be decoded
// cleanly, and half a page of text is worth more to somebody searching than
// none of it. A page that decoded no bytes at all is a different answer and
// stays an error — a conversion that silently produces an empty document from
// an unreadable page is worse than one that says it could not read it, and
// go-pdfkit/latex relies on being told.
//
// wantPictures says whether the caller is going to look at the pictures. A
// page's images have nothing to do with its text, and undoing their filters is
// the most expensive thing on the page: one arXiv figure holds 378 MB of image
// once decompressed, so asking that page what it *said* used to cost 1 094 MB
// and 1.3 seconds spent inflating pictures the answer throws away. Where they
// land is still noted either way, since that costs a matrix multiply; only the
// bytes are left alone.
func walk(d *reader.Document, page int, wantPictures bool) (*extractor, error) {
dec, err := d.PageContentDecoded(page)
if err != nil {
return nil, err
}
if len(dec.Data) == 0 && dec.Cause != nil {
return nil, dec.Cause
}
content := dec.Data
p, _ := d.Page(page)
resources, _ := d.GetDict(p, "Resources")
e := &extractor{doc: d, fonts: map[int]*pdffont.Font{}, wantPictures: wantPictures}
e.run(content, resources, initialState(d, p), 0)
return e, nil
}
// Assemble puts runs back together as lines of text: everything on one
// baseline in the order it sits across the page, with a space wherever the
// gap between two pieces is wide enough to be one.
func Assemble(runs []Run) string {
if len(runs) == 0 {
return ""
}
lines := groupIntoLines(runs)
var out strings.Builder
for i, line := range lines {
if i > 0 {
out.WriteByte('\n')
}
out.WriteString(joinLine(line))
}
return out.String()
}
// groupIntoLines gathers runs that sit on the same baseline, in the order they
// appear down the page.
func groupIntoLines(runs []Run) [][]Run {
sorted := append([]Run{}, runs...)
sort.SliceStable(sorted, func(i, j int) bool {
if !sameLine(sorted[i], sorted[j]) {
return sorted[i].Y > sorted[j].Y
}
return sorted[i].X < sorted[j].X
})
var lines [][]Run
for _, r := range sorted {
if n := len(lines); n > 0 && sameLine(lines[n-1][0], r) {
lines[n-1] = append(lines[n-1], r)
continue
}
lines = append(lines, []Run{r})
}
return lines
}
// sameLine reports whether two runs sit on the same baseline. A line is
// allowed to wobble by a fraction of its own height, which is what a
// superscript or a slightly different font does.
func sameLine(a, b Run) bool {
tol := a.Size * 0.3
if tol <= 0 {
tol = 1
}
d := a.Y - b.Y
return d < tol && d > -tol
}
// joinLine writes one line out, putting a space wherever the gap between two
// pieces is wide enough to be one.
func joinLine(line []Run) string {
var out strings.Builder
for i, r := range line {
if i > 0 {
prev := line[i-1]
gap := r.X - (prev.X + prev.Width)
if wantsSpace(prev, r, gap) {
out.WriteByte(' ')
}
}
out.WriteString(r.Text)
}
return strings.TrimRight(out.String(), " ")
}
// wantsSpace decides whether a gap between two pieces of a line is a word
// break. A space of its own is never doubled, and a gap of about a third of a
// space is where kerning ends and words begin.
func wantsSpace(prev, next Run, gap float64) bool {
if strings.HasSuffix(prev.Text, " ") || strings.HasPrefix(next.Text, " ") {
return false
}
space := prev.Space
if space <= 0 {
space = prev.Size * 0.25
}
return gap > space*0.3
}
+188
View File
@@ -0,0 +1,188 @@
package extract
import (
"github.com/go-pdfkit/reader"
)
// An Image is a picture the page places, and where it puts it.
//
// What comes back is the picture as the file holds it — the bytes and the
// dictionary describing them — rather than pixels. A JPEG comes out as a JPEG,
// which is what anyone pulling pictures out of a document wants: re-encoding
// it would lose something for nothing. Turning the other kinds into pixels
// means reading their colour space, which is the renderer's work rather than
// this package's.
type Image struct {
// Name is what the page's resources call it, or empty for one written
// into the content stream.
Name string
// Width and Height are the picture's own, in samples.
Width, Height int
// X and Y are where its bottom left corner lands on the page, and
// DrawnWidth and DrawnHeight how large it is drawn, in points. A picture
// turned or skewed by the page still reports the box it covers.
X, Y float64
DrawnWidth, DrawnHeight float64
// Filter is the last filter still on the data: DCTDecode for a JPEG that
// comes out as one, JPXDecode for a JPEG 2000, and empty for data this
// has unfiltered into plain samples.
Filter reader.Name
// Data is the picture's bytes, with every filter this understands undone.
Data []byte
// Dict is the image's own dictionary, which says how to read the data:
// its colour space, how many bits a sample takes, and whether it is a
// mask.
Dict reader.Dict
// Inline reports a picture written into the content stream rather than
// held as a resource of its own.
Inline bool
}
// Images is every picture the page places, in the order it places them. The
// same picture placed twice comes back twice, since where it lands is part of
// what is being asked for.
func Images(d *reader.Document, page int) ([]Image, error) {
e, err := walk(d, page, true)
if err != nil {
return nil, err
}
return e.images, nil
}
// doXObject handles the Do operator: a form is walked into, and a picture is
// noted where it lands.
func (e *extractor) doXObject(g *state, operands []reader.Object, resources reader.Dict, depth int) {
if len(operands) == 0 {
return
}
name, ok := reader.ToName(operands[0])
if !ok {
return
}
xobjects, ok := e.doc.GetDict(resources, "XObject")
if !ok {
return
}
stream, ok := reader.ToStream(resolve(e.doc, xobjects.Get(name)))
if !ok {
return
}
switch sub, _ := reader.ToName(resolve(e.doc, stream.Dict.Get("Subtype"))); sub {
case "Form":
e.doForm(g, stream, resources, depth)
case "Image":
e.noteImage(g, string(name), stream.Dict, stream.Raw, false)
}
}
// doForm walks into a form, which is a piece of a page held apart and drawn
// where the page says.
func (e *extractor) doForm(g *state, stream *reader.Stream, resources reader.Dict, depth int) {
data, img, err := e.doc.DecodeStream(stream)
if err != nil || img != "" {
return
}
inner := *g
if m := floatArray(e.doc, stream.Dict.Get("Matrix")); len(m) >= 6 {
inner.ctm = matrix{m[0], m[1], m[2], m[3], m[4], m[5]}.mul(g.ctm)
}
own, ok := e.doc.GetDict(stream.Dict, "Resources")
if !ok {
own = resources
}
e.run(data, own, inner, depth+1)
}
// inlineImage notes a picture written into the content stream.
func (e *extractor) inlineImage(g *state, im *reader.InlineImage) {
if im == nil {
return
}
e.noteImage(g, "", im.Expanded(), im.Raw, true)
}
// noteImage works out where a picture lands and records it. A picture occupies
// the unit square of the transform in force, so its corners are where that
// square's are.
func (e *extractor) noteImage(g *state, name string, dict reader.Dict, raw []byte, inline bool) {
minX, minY, maxX, maxY := unitSquareBox(g.ctm)
img := Image{
Name: name,
Width: int(intOf(resolve(e.doc, dict.Get("Width")), 0)),
Height: int(intOf(resolve(e.doc, dict.Get("Height")), 0)),
X: minX,
Y: minY,
DrawnWidth: maxX - minX,
DrawnHeight: maxY - minY,
Dict: dict,
Inline: inline,
}
// A caller after the text is not after the pixels, and undoing a
// picture's filters to hand it nothing is the most expensive thing a page
// can be made to do.
if e.wantPictures {
data, filter, err := reader.Decode(dict, raw, e.doc.Resolver())
if err == nil {
img.Data, img.Filter = data, filter
} else {
img.Data = raw
}
}
e.images = append(e.images, img)
}
// unitSquareBox is the box the unit square covers once the transform has had
// its way with it.
func unitSquareBox(m matrix) (minX, minY, maxX, maxY float64) {
first := true
for _, c := range [][2]float64{{0, 0}, {1, 0}, {1, 1}, {0, 1}} {
x, y := m.apply(c[0], c[1])
if first {
minX, minY, maxX, maxY = x, y, x, y
first = false
continue
}
minX, maxX = smaller(minX, x), larger(maxX, x)
minY, maxY = smaller(minY, y), larger(maxY, y)
}
return minX, minY, maxX, maxY
}
func smaller(a, b float64) float64 {
if a < b {
return a
}
return b
}
func larger(a, b float64) float64 {
if a > b {
return a
}
return b
}
// intOf reads a whole number, or gives a default.
func intOf(o reader.Object, def int64) int64 {
if v, ok := reader.ToInt(o); ok {
return v
}
return def
}
// floatArray reads an array of numbers, or nothing.
func floatArray(d *reader.Document, o reader.Object) []float64 {
arr, ok := reader.ToArray(resolve(d, o))
if !ok {
return nil
}
out := make([]float64, 0, len(arr))
for _, e := range arr {
v, ok := reader.ToFloat(resolve(d, e))
if !ok {
return nil
}
out = append(out, v)
}
return out
}
+175
View File
@@ -0,0 +1,175 @@
package extract
import (
"github.com/go-opentype/opentype"
"github.com/go-pdfkit/pdffont"
"github.com/go-pdfkit/reader"
)
// attachProgram gives a font a last resort for the codes the document says
// nothing about: what its own embedded program calls them.
//
// A font that says it is symbolic is addressed through its own character map,
// and an assumed encoding is a bad guess at it — a mathematical font puts a
// capital gamma where the standard encoding puts an inverted exclamation mark.
// The program knows, and this is how it is asked.
//
// There are two ways to ask, and a program answers at most one of them. A
// PostScript program names its glyphs and carries an encoding from code to
// name, so the name is the answer. A TrueType program usually does neither —
// a subset of one names nothing worth reading and has no code-to-name
// encoding at all — but it does carry character maps, and walking through one
// to the glyph and back out of another says which character the glyph is for.
func attachProgram(f *pdffont.Font) {
if f.Kind() == pdffont.Composite {
return
}
key, data, ok := f.Program()
if !ok {
return
}
program, err := readProgram(key, data)
if err != nil {
return
}
maps := chooseCharacterMaps(program)
// Only a simple font reaches here, and its codes are bytes.
f.SetFallback(func(code int) (string, bool) {
if r, ok := runeByName(program, code); ok {
return string(r), true
}
if r, ok := runeByCharacterMap(program, maps, code); ok {
return string(r), true
}
return "", false
})
}
// runeByName asks the program's own encoding what it calls a code, and reads
// the character out of that name. Only a PostScript program — a Type 1 one, or
// the CFF outlines of an OpenType font — carries such an encoding.
func runeByName(program *opentype.Font, code int) (rune, bool) {
gid, ok := program.GlyphIndexByCode(byte(code))
if !ok {
return 0, false
}
// A glyph the program does not name comes back as no name at all,
// which names no character either.
name, _ := program.GlyphName(gid)
return pdffont.RuneOfGlyphName(name)
}
// characterMaps says which of a program's cmap subtables are worth addressing,
// by index, or -1 for one the program does not carry.
type characterMaps struct {
// symbol is the Microsoft Symbol subtable, platform 3 encoding 0: a font's
// own codes, conventionally written at 0xF000 + code.
symbol int
// mac is the Macintosh Roman subtable, platform 1 encoding 0, indexed by
// single bytes.
mac int
// unicode is a Unicode subtable — Microsoft Unicode, platform 3 encoding 1
// or 10, or anything on platform 0 — indexed by codepoint. This is the one
// that is inverted, because it is the only one whose codes are characters.
unicode int
}
// chooseCharacterMaps picks out the subtables a font is addressed through and
// the one that says which character a glyph is for.
//
// A font can carry several subtables of a kind; the first of each is taken,
// which is what a font that repeats one means by repeating it.
func chooseCharacterMaps(program *opentype.Font) characterMaps {
m := characterMaps{symbol: -1, mac: -1, unicode: -1}
for i := range program.NumCharacterMaps() {
platform, encoding, _, _ := program.CharacterMap(i)
switch {
case platform == 3 && encoding == 0:
if m.symbol < 0 {
m.symbol = i
}
case platform == 1 && encoding == 0:
if m.mac < 0 {
m.mac = i
}
case platform == 0, platform == 3 && (encoding == 1 || encoding == 10):
if m.unicode < 0 {
m.unicode = i
}
}
}
return m
}
// runeByCharacterMap walks a code through the font's own character map to a
// glyph, and back out of its Unicode character map to the character that glyph
// stands for.
//
// The way in is the one poppler uses for a font the document gave no encoding:
// the Microsoft Symbol subtable if there is one, else the Macintosh Roman one,
// addressed by the raw code and then, failing that, by 0xF000 + code, which is
// where such subtables are conventionally written.
//
// The way out is the Unicode subtable, inverted. Without one there is no way
// out: a glyph on its own says nothing about which character it is, and a
// guess would be exactly the wrong letter the caller's guard exists to refuse.
func runeByCharacterMap(program *opentype.Font, m characterMaps, code int) (rune, bool) {
if m.unicode < 0 {
return 0, false
}
in := m.symbol
if in < 0 {
in = m.mac
}
if in < 0 {
return 0, false
}
gid, ok := program.GlyphIndexInMap(in, rune(code))
if !ok {
gid, ok = program.GlyphIndexInMap(in, rune(0xF000|code))
}
if !ok {
return 0, false
}
r, ok := program.RuneOfGlyphInMap(m.unicode, gid)
if !ok || !readableRune(r) {
return 0, false
}
return r, true
}
// readableRune reports whether a character recovered this way is worth
// reporting as text.
//
// A private-use codepoint is not: it means whatever the font decided it means
// and nothing outside the font can read it, so a page full of them searches no
// better than a page of nothing and looks, wrongly, like it was read. Control
// characters are refused for the same reason — a page does not say them.
func readableRune(r rune) bool {
switch {
case r < 0x20, r >= 0x7F && r <= 0x9F:
return false
case r >= 0xE000 && r <= 0xF8FF:
return false
case r >= 0xF0000:
return false
}
return true
}
// readProgram decodes an embedded font program. Which key it arrived under
// says what it is: FontFile2 is TrueType, FontFile a PostScript Type 1
// program, and FontFile3 either a bare CFF one or a whole OpenType font — the
// key alone does not settle that one, so both are tried.
func readProgram(key reader.Name, data []byte) (*opentype.Font, error) {
switch key {
case "FontFile":
return opentype.ParseType1(data)
case "FontFile3":
if f, err := opentype.Parse(data); err == nil {
return f, nil
}
return opentype.ParseCFF(data)
}
return opentype.Parse(data)
}
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>go-pdfkit/.github"]
}
+273
View File
@@ -0,0 +1,273 @@
package extract
import (
"strings"
"github.com/go-pdfkit/pdffont"
"github.com/go-pdfkit/reader"
)
// maxFormDepth is how deeply one form may draw another before a page is taken
// to be drawing itself.
const maxFormDepth = 12
// An extractor walks a content stream keeping just enough state to say where
// each piece of text was drawn.
type extractor struct {
doc *reader.Document
runs []Run
fonts map[int]*pdffont.Font
// tm is where the next glyph goes and tlm where the line began; both
// belong to the page rather than to the graphics state.
tm, tlm matrix
images []Image
// wantPictures says whether anybody is going to read Image.Data. When
// nobody is, the filters are left undone.
wantPictures bool
}
// run walks a content stream.
func (e *extractor) run(content []byte, resources reader.Dict, g state, depth int) {
if depth > maxFormDepth {
return
}
// The operations are taken one at a time rather than all at once. The
// walk only ever goes forwards, so nothing needs the list; keeping it
// costs a struct and a slice header for every operator on the page, and
// pages are not always small. One arXiv figure holds a single page of
// 84.8 MB of content stream and 4 579 973 operations: holding them cost
// 2 166 MB where reading them one by one costs 935 MB.
scan := reader.NewContentScanner(content)
var stack []state
for {
op, more := scan.Next()
if !more {
break
}
n := numbers(op.Operands)
switch op.Operator {
case "q":
stack = append(stack, g)
case "Q":
if len(stack) > 0 {
g = stack[len(stack)-1]
stack = stack[:len(stack)-1]
}
case "cm":
if len(n) >= 6 {
g.ctm = matrix{n[0], n[1], n[2], n[3], n[4], n[5]}.mul(g.ctm)
}
case "BT":
e.tm, e.tlm = identity, identity
case "ET":
e.tm, e.tlm = identity, identity
case "Tf":
e.setFont(&g, op.Operands, n, resources)
case "Tc":
if len(n) >= 1 {
g.text.charSpace = n[0]
}
case "Tw":
if len(n) >= 1 {
g.text.wordSpace = n[0]
}
case "Tz":
if len(n) >= 1 {
g.text.scale = n[0] / 100
}
case "TL":
if len(n) >= 1 {
g.text.leading = n[0]
}
case "Ts":
if len(n) >= 1 {
g.text.rise = n[0]
}
case "Tr":
if len(n) >= 1 {
g.text.mode = int(n[0])
}
case "Td":
if len(n) >= 2 {
e.tlm = matrix{1, 0, 0, 1, n[0], n[1]}.mul(e.tlm)
e.tm = e.tlm
}
case "TD":
if len(n) >= 2 {
g.text.leading = -n[1]
e.tlm = matrix{1, 0, 0, 1, n[0], n[1]}.mul(e.tlm)
e.tm = e.tlm
}
case "Tm":
if len(n) >= 6 {
e.tlm = matrix{n[0], n[1], n[2], n[3], n[4], n[5]}
e.tm = e.tlm
}
case "T*":
e.nextLine(&g)
case "Tj":
e.showOperands(&g, op.Operands)
case "'":
e.nextLine(&g)
e.showOperands(&g, op.Operands)
case "\"":
if len(n) >= 2 {
g.text.wordSpace, g.text.charSpace = n[0], n[1]
}
e.nextLine(&g)
e.showOperands(&g, op.Operands)
case "TJ":
e.showArray(&g, op.Operands)
case "Do":
e.doXObject(&g, op.Operands, resources, depth)
case "BI":
e.inlineImage(&g, op.Image)
}
}
}
// setFont reads the font a Tf operator names.
func (e *extractor) setFont(g *state, operands []reader.Object, n []float64, resources reader.Dict) {
if len(operands) < 2 {
return
}
name, ok := reader.ToName(operands[0])
if !ok {
return
}
if len(n) >= 1 {
g.text.size = n[len(n)-1]
}
g.text.fontName = string(name)
g.text.font = e.font(name, resources)
}
// font reads a font out of the page's resources, remembering the ones already
// read so that a page setting the same font a thousand times reads it once.
func (e *extractor) font(name reader.Name, resources reader.Dict) *pdffont.Font {
fonts, ok := e.doc.GetDict(resources, "Font")
if !ok {
return nil
}
if ref, ok := fonts.Get(name).(reader.Ref); ok {
if f, seen := e.fonts[ref.Num]; seen {
return f
}
dict, ok := e.doc.GetDict(fonts, name)
if !ok {
return nil
}
f := pdffont.Read(e.doc, dict)
attachProgram(f)
e.fonts[ref.Num] = f
return f
}
dict, ok := e.doc.GetDict(fonts, name)
if !ok {
return nil
}
f := pdffont.Read(e.doc, dict)
attachProgram(f)
return f
}
// nextLine moves down by the leading, which is what T* and the quote
// operators do.
func (e *extractor) nextLine(g *state) {
e.tlm = matrix{1, 0, 0, 1, 0, -g.text.leading}.mul(e.tlm)
e.tm = e.tlm
}
// showOperands shows the string a Tj-like operator carries.
func (e *extractor) showOperands(g *state, operands []reader.Object) {
for _, o := range operands {
if s, ok := reader.ToString(o); ok {
e.show(g, s)
return
}
}
}
// showArray shows the pieces a TJ operator carries, moving the pen by the
// numbers between them.
func (e *extractor) showArray(g *state, operands []reader.Object) {
for _, o := range operands {
arr, ok := reader.ToArray(o)
if !ok {
continue
}
for _, item := range arr {
if s, ok := reader.ToString(item); ok {
e.show(g, s)
continue
}
if v, ok := reader.ToFloat(item); ok {
e.advance(g, -v/1000*g.text.size*g.text.scale)
}
}
}
}
// show reads one string as text and records where it was drawn.
func (e *extractor) show(g *state, s []byte) {
f := g.text.font
if f == nil || len(s) == 0 {
return
}
start := e.tm.mul(g.ctm)
x, y := start.apply(0, g.text.rise)
var text strings.Builder
unreadable := false
width := 0.0
for _, code := range f.Codes(s) {
if t, ok := f.Text(code); ok {
text.WriteString(pdffont.TrimText(t))
} else {
unreadable = true
}
adv := (f.Width(code)*g.text.size + g.text.charSpace) * g.text.scale
if f.Kind() != pdffont.Composite && code == ' ' {
adv += g.text.wordSpace * g.text.scale
}
width += adv
}
e.advance(g, width)
if text.Len() == 0 && !unreadable {
return
}
// How far a length in text space reaches on the page: the text matrix and
// the page's transform together, which is the same matrix the run's own
// position came through.
//
// Only the page's transform used to be counted here, and the text matrix
// was left out. A document is free to put the whole of its scale there —
// "/F1 1 Tf" and then a text matrix of ten is how TeX writes every PDF it
// has ever produced — and such a document reported its text as one point
// tall with letters half a point wide, while saying quite correctly where
// on the page each run began.
//
// Those are the two numbers a word break is decided by. A gap measured in
// points was being weighed against a space width ten times too small, so
// every ordinary kern between two letters looked like a space: "Original
// Domain" came back as "Or ig inal D omain". Across four thousand arXiv
// figures, 57% of the words a reader got back were one or two letters
// long. Nothing failed, and nothing was slow; the text was simply no
// longer text.
scale := start.scale()
e.runs = append(e.runs, Run{
Text: text.String(),
X: x,
Y: y,
Width: width * scale,
Size: g.text.size * scale,
Font: g.text.fontName,
Space: f.Width(' ') * g.text.size * g.text.scale * scale,
Invisible: g.text.mode == 3,
Unreadable: unreadable,
})
}
// advance moves the pen along the line by a distance in text space.
func (e *extractor) advance(g *state, by float64) {
e.tm = matrix{1, 0, 0, 1, by, 0}.mul(e.tm)
}
+134
View File
@@ -0,0 +1,134 @@
package extract
import (
"github.com/go-pdfkit/pdffont"
"github.com/go-pdfkit/reader"
)
// A matrix is the six numbers a PDF transform is written as. Extraction needs
// only to multiply them and to move a point, so it carries its own rather than
// depending on a drawing library it has no other use for.
type matrix struct{ a, b, c, d, e, f float64 }
// identity is the transform that changes nothing.
var identity = matrix{1, 0, 0, 1, 0, 0}
// mul is the transform that applies m first and then n, which is the order a
// PDF composes them in.
func (m matrix) mul(n matrix) matrix {
return matrix{
a: m.a*n.a + m.b*n.c,
b: m.a*n.b + m.b*n.d,
c: m.c*n.a + m.d*n.c,
d: m.c*n.b + m.d*n.d,
e: m.e*n.a + m.f*n.c + n.e,
f: m.e*n.b + m.f*n.d + n.f,
}
}
// apply moves a point.
func (m matrix) apply(x, y float64) (float64, float64) {
return m.a*x + m.c*y + m.e, m.b*x + m.d*y + m.f
}
// scale is how much the transform stretches a length, taken as the geometric
// mean of its two axes so that a rotation counts as no stretch at all.
func (m matrix) scale() float64 {
x := m.a*m.a + m.b*m.b
y := m.c*m.c + m.d*m.d
return sqrt(sqrt(x * y))
}
// sqrt without pulling in the whole of math for one use.
func sqrt(v float64) float64 {
if v <= 0 {
return 0
}
x := v
for i := 0; i < 24; i++ {
x = (x + v/x) / 2
}
return x
}
// A textState is everything the showing operators read besides the string.
type textState struct {
font *pdffont.Font
fontName string
size float64
charSpace float64
wordSpace float64
scale float64 // horizontal scaling, as a fraction
leading float64
rise float64
mode int
}
// A state is the graphics state, of which extraction needs only the transform
// and the text parameters.
type state struct {
ctm matrix
text textState
}
// initialState is the transform a page starts in, which for extraction is the
// one that undoes the page's own box: text comes back in the page's
// coordinates, counting up from the bottom left of what is visible.
func initialState(d *reader.Document, page reader.Dict) state {
box := pageBox(d, page)
return state{
ctm: matrix{1, 0, 0, 1, -box[0], -box[1]},
text: textState{scale: 1},
}
}
// pageBox is the area of the page that is visible.
func pageBox(d *reader.Document, page reader.Dict) [4]float64 {
for _, key := range []reader.Name{"CropBox", "MediaBox"} {
if b, ok := rectangle(d, page.Get(key)); ok {
return b
}
}
return [4]float64{0, 0, 612, 792}
}
// rectangle reads a PDF rectangle, put the right way round.
func rectangle(d *reader.Document, o reader.Object) ([4]float64, bool) {
var out [4]float64
arr, ok := reader.ToArray(resolve(d, o))
if !ok || len(arr) < 4 {
return out, false
}
for i := 0; i < 4; i++ {
v, ok := reader.ToFloat(resolve(d, arr[i]))
if !ok {
return out, false
}
out[i] = v
}
if out[0] > out[2] {
out[0], out[2] = out[2], out[0]
}
if out[1] > out[3] {
out[1], out[3] = out[3], out[1]
}
return out, true
}
// resolve follows an indirect reference. A document that opened cannot fail to
// resolve one.
func resolve(d *reader.Document, o reader.Object) reader.Object {
out, _ := d.Resolve(o)
return out
}
// numbers reads the operands that are numbers.
func numbers(operands []reader.Object) []float64 {
out := make([]float64, 0, len(operands))
for _, o := range operands {
if v, ok := reader.ToFloat(o); ok {
out = append(out, v)
}
}
return out
}
+1
View File
@@ -0,0 +1 @@
coverage.out
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2026 the go-pdfkit/render authors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
+75
View File
@@ -0,0 +1,75 @@
# pdffont
[![CI](https://github.com/go-pdfkit/pdffont/actions/workflows/ci.yml/badge.svg)](https://github.com/go-pdfkit/pdffont/actions/workflows/ci.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/go-pdfkit/pdffont.svg)](https://pkg.go.dev/github.com/go-pdfkit/pdffont)
[![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](LICENSE)
[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](#how-it-is-checked)
The PDF side of a font: not the outlines, which are the font program's
business, but everything the document itself says about how a string of
bytes is to be read.
Which codes a string holds, how wide each one is, what each is called, and
what text each stands for. It is what a renderer needs before it can ask a
font program for a glyph, and all an extractor needs to read a page back as
words — so neither has to say it twice.
Pure Go, no C, nothing outside [`reader`](https://github.com/go-pdfkit/reader)
and the standard library.
## What it reads
Simple fonts, addressed one byte at a time through a base encoding and
whatever differences the document names on top of it. Composite fonts,
addressed by character identifier, with their widths and their map from
identifier to glyph. Type 3 fonts, whose glyphs are little drawings and
whose widths are in a space of their own.
And `/ToUnicode`, which is the one thing in a PDF that says what its text
actually **says**. Without it a page can be drawn but not read.
## What it refuses to guess
A font that says it is **symbolic** is addressed through its own character
map, and a base encoding is a guess at it — a bad one. A mathematical font
puts a capital gamma where the standard encoding puts an inverted
exclamation mark, so reading the name gives the wrong letter with nothing
to say so.
Such a code is left unread unless the document named it itself, or unless a
caller installs [`Font.SetFallback`](https://pkg.go.dev/github.com/go-pdfkit/pdffont#Font.SetFallback)
to say what the embedded program calls it. Reading the program is another
package's work; `extract` does it with
[`go-opentype`](https://github.com/go-opentype/opentype).
## How it is checked
100% of statements, including every branch that handles a dictionary saying
something it should not.
And the glyph-name table — which is the one piece of data here, and where a
single wrong entry would silently give the wrong letter — is held up
against the documents themselves. A font that both names a code and carries
a `/ToUnicode` entry for it states the same fact twice, so the two can be
compared. Over 12 000 corpus files: **63 515 names checked, 63 074 agreed —
99.31%**. The disagreements are producers whose own maps are wrong
(`alpha` → `↵`, `ff` → `!`), plus `Delta` and `Omega`, which Adobe's list
gives as the increment and ohm signs; the Greek letters written here are
the same characters by Unicode's own equivalence, and the ones a person
searching would type.
## The rest of the family
[**reader**](https://github.com/go-pdfkit/reader) reads and writes the format ·
[**ops**](https://github.com/go-pdfkit/ops) is the verbs and the `pdfops` command ·
[**render**](https://github.com/go-pdfkit/render) turns a page into pixels ·
[**pdffont**](https://github.com/go-pdfkit/pdffont) is what a document says about a font ·
[**extract**](https://github.com/go-pdfkit/extract) reads a page back ·
[**coedit**](https://github.com/go-pdfkit/coedit) is a PDF several people edit at once ·
[**app**](https://github.com/go-pdfkit/app) is the browser workbench.
Documentation for all of it: <https://go-pdfkit.github.io/docs/>
## Licence
BSD-3-Clause.
+302
View File
@@ -0,0 +1,302 @@
package pdffont
import "strings"
// The two encodings a simple font is addressed through when it does not carry
// one of its own, as a name per code. Only the codes that name a glyph appear;
// the rest are blank.
//
// These are the tables the specification prints. They matter because a font
// program is addressed by glyph name or by character, and a content stream is
// written in single bytes: without them, every byte would have to be guessed
// at.
// StandardEncoding is Adobe's own, which most Type 1 fonts assume, and what a
// simple font is read with when it says nothing else.
var StandardEncoding = [256]string{
32: "space", 33: "exclam", 34: "quotedbl", 35: "numbersign", 36: "dollar",
37: "percent", 38: "ampersand", 39: "quoteright", 40: "parenleft",
41: "parenright", 42: "asterisk", 43: "plus", 44: "comma", 45: "hyphen",
46: "period", 47: "slash", 48: "zero", 49: "one", 50: "two", 51: "three",
52: "four", 53: "five", 54: "six", 55: "seven", 56: "eight", 57: "nine",
58: "colon", 59: "semicolon", 60: "less", 61: "equal", 62: "greater",
63: "question", 64: "at", 65: "A", 66: "B", 67: "C", 68: "D", 69: "E",
70: "F", 71: "G", 72: "H", 73: "I", 74: "J", 75: "K", 76: "L", 77: "M",
78: "N", 79: "O", 80: "P", 81: "Q", 82: "R", 83: "S", 84: "T", 85: "U",
86: "V", 87: "W", 88: "X", 89: "Y", 90: "Z", 91: "bracketleft",
92: "backslash", 93: "bracketright", 94: "asciicircum", 95: "underscore",
96: "quoteleft", 97: "a", 98: "b", 99: "c", 100: "d", 101: "e", 102: "f",
103: "g", 104: "h", 105: "i", 106: "j", 107: "k", 108: "l", 109: "m",
110: "n", 111: "o", 112: "p", 113: "q", 114: "r", 115: "s", 116: "t",
117: "u", 118: "v", 119: "w", 120: "x", 121: "y", 122: "z",
123: "braceleft", 124: "bar", 125: "braceright", 126: "asciitilde",
161: "exclamdown", 162: "cent", 163: "sterling", 164: "fraction",
165: "yen", 166: "florin", 167: "section", 168: "currency",
169: "quotesingle", 170: "quotedblleft", 171: "guillemotleft",
172: "guilsinglleft", 173: "guilsinglright", 174: "fi", 175: "fl",
177: "endash", 178: "dagger", 179: "daggerdbl", 180: "periodcentered",
182: "paragraph", 183: "bullet", 184: "quotesinglbase",
185: "quotedblbase", 186: "quotedblright", 187: "guillemotright",
188: "ellipsis", 189: "perthousand", 191: "questiondown", 193: "grave",
194: "acute", 195: "circumflex", 196: "tilde", 197: "macron", 198: "breve",
199: "dotaccent", 200: "dieresis", 202: "ring", 203: "cedilla",
205: "hungarumlaut", 206: "ogonek", 207: "caron", 208: "emdash",
225: "AE", 227: "ordfeminine", 232: "Lslash", 233: "Oslash", 234: "OE",
235: "ordmasculine", 241: "ae", 245: "dotlessi", 248: "lslash",
249: "oslash", 250: "oe", 251: "germandbls",
}
// WinAnsiEncoding is the one a file written on Windows assumes, and the one
// most producers name.
var WinAnsiEncoding = [256]string{
32: "space", 33: "exclam", 34: "quotedbl", 35: "numbersign", 36: "dollar",
37: "percent", 38: "ampersand", 39: "quotesingle", 40: "parenleft",
41: "parenright", 42: "asterisk", 43: "plus", 44: "comma", 45: "hyphen",
46: "period", 47: "slash", 48: "zero", 49: "one", 50: "two", 51: "three",
52: "four", 53: "five", 54: "six", 55: "seven", 56: "eight", 57: "nine",
58: "colon", 59: "semicolon", 60: "less", 61: "equal", 62: "greater",
63: "question", 64: "at", 65: "A", 66: "B", 67: "C", 68: "D", 69: "E",
70: "F", 71: "G", 72: "H", 73: "I", 74: "J", 75: "K", 76: "L", 77: "M",
78: "N", 79: "O", 80: "P", 81: "Q", 82: "R", 83: "S", 84: "T", 85: "U",
86: "V", 87: "W", 88: "X", 89: "Y", 90: "Z", 91: "bracketleft",
92: "backslash", 93: "bracketright", 94: "asciicircum", 95: "underscore",
96: "grave", 97: "a", 98: "b", 99: "c", 100: "d", 101: "e", 102: "f",
103: "g", 104: "h", 105: "i", 106: "j", 107: "k", 108: "l", 109: "m",
110: "n", 111: "o", 112: "p", 113: "q", 114: "r", 115: "s", 116: "t",
117: "u", 118: "v", 119: "w", 120: "x", 121: "y", 122: "z",
123: "braceleft", 124: "bar", 125: "braceright", 126: "asciitilde",
128: "Euro", 130: "quotesinglbase", 131: "florin", 132: "quotedblbase",
133: "ellipsis", 134: "dagger", 135: "daggerdbl", 136: "circumflex",
137: "perthousand", 138: "Scaron", 139: "guilsinglleft", 140: "OE",
142: "Zcaron", 145: "quoteleft", 146: "quoteright", 147: "quotedblleft",
148: "quotedblright", 149: "bullet", 150: "endash", 151: "emdash",
152: "tilde", 153: "trademark", 154: "scaron", 155: "guilsinglright",
156: "oe", 158: "zcaron", 159: "Ydieresis", 160: "space",
161: "exclamdown", 162: "cent", 163: "sterling", 164: "currency",
165: "yen", 166: "brokenbar", 167: "section", 168: "dieresis",
169: "copyright", 170: "ordfeminine", 171: "guillemotleft",
172: "logicalnot", 173: "hyphen", 174: "registered", 175: "macron",
176: "degree", 177: "plusminus", 178: "twosuperior",
179: "threesuperior", 180: "acute", 181: "mu", 182: "paragraph",
183: "periodcentered", 184: "cedilla", 185: "onesuperior",
186: "ordmasculine", 187: "guillemotright", 188: "onequarter",
189: "onehalf", 190: "threequarters", 191: "questiondown", 192: "Agrave",
193: "Aacute", 194: "Acircumflex", 195: "Atilde", 196: "Adieresis",
197: "Aring", 198: "AE", 199: "Ccedilla", 200: "Egrave", 201: "Eacute",
202: "Ecircumflex", 203: "Edieresis", 204: "Igrave", 205: "Iacute",
206: "Icircumflex", 207: "Idieresis", 208: "Eth", 209: "Ntilde",
210: "Ograve", 211: "Oacute", 212: "Ocircumflex", 213: "Otilde",
214: "Odieresis", 215: "multiply", 216: "Oslash", 217: "Ugrave",
218: "Uacute", 219: "Ucircumflex", 220: "Udieresis", 221: "Yacute",
222: "Thorn", 223: "germandbls", 224: "agrave", 225: "aacute",
226: "acircumflex", 227: "atilde", 228: "adieresis", 229: "aring",
230: "ae", 231: "ccedilla", 232: "egrave", 233: "eacute",
234: "ecircumflex", 235: "edieresis", 236: "igrave", 237: "iacute",
238: "icircumflex", 239: "idieresis", 240: "eth", 241: "ntilde",
242: "ograve", 243: "oacute", 244: "ocircumflex", 245: "otilde",
246: "odieresis", 247: "divide", 248: "oslash", 249: "ugrave",
250: "uacute", 251: "ucircumflex", 252: "udieresis", 253: "yacute",
254: "thorn", 255: "ydieresis",
}
// glyphRunes is what the names above stand for. A font program is looked up by
// character, so a name has to become one; anything not here, and not written
// in one of the two conventional forms, cannot be looked up that way.
var glyphRunes = map[string]rune{
"space": ' ', "exclam": '!', "quotedbl": '"', "numbersign": '#',
"dollar": '$', "percent": '%', "ampersand": '&', "quotesingle": '\'',
"quoteright": '’', "quoteleft": '‘', "parenleft": '(',
"parenright": ')', "asterisk": '*', "plus": '+', "comma": ',',
"hyphen": '-', "period": '.', "slash": '/', "zero": '0', "one": '1',
"two": '2', "three": '3', "four": '4', "five": '5', "six": '6',
"seven": '7', "eight": '8', "nine": '9', "colon": ':', "semicolon": ';',
"less": '<', "equal": '=', "greater": '>', "question": '?', "at": '@',
"bracketleft": '[', "backslash": '\\', "bracketright": ']',
"asciicircum": '^', "underscore": '_', "braceleft": '{', "bar": '|',
"braceright": '}', "asciitilde": '~', "exclamdown": '¡', "cent": '¢',
"sterling": '£', "fraction": '⁄', "yen": '¥', "florin": 'ƒ',
"section": '§', "currency": '¤', "quotedblleft": '“',
"quotedblright": '”', "guillemotleft": '«', "guillemotright": '»',
"guilsinglleft": '‹', "guilsinglright": '›', "fi": 'fi', "fl": 'fl',
"endash": '–', "emdash": '—', "dagger": '†', "daggerdbl": '‡',
"periodcentered": '·', "paragraph": '¶', "bullet": '•',
"quotesinglbase": '‚', "quotedblbase": '„', "ellipsis": '…',
"perthousand": '‰', "questiondown": '¿', "grave": '`', "acute": '´',
"circumflex": 'ˆ', "tilde": '˜', "macron": '¯', "breve": '˘',
"dotaccent": '˙', "dieresis": '¨', "ring": '˚', "cedilla": '¸',
"hungarumlaut": '˝', "ogonek": '˛', "caron": 'ˇ', "AE": 'Æ', "ae": 'æ',
"ordfeminine": 'ª', "ordmasculine": 'º', "Lslash": 'Ł', "lslash": 'ł',
"Oslash": 'Ø', "oslash": 'ø', "OE": 'Œ', "oe": 'œ', "dotlessi": 'ı',
"germandbls": 'ß', "Euro": '€', "Scaron": 'Š', "scaron": 'š',
"Zcaron": 'Ž', "zcaron": 'ž', "Ydieresis": 'Ÿ', "trademark": '™',
"brokenbar": '¦', "copyright": '©', "logicalnot": '¬', "registered": '®',
"degree": '°', "plusminus": '±', "twosuperior": '²',
"threesuperior": '³', "mu": 'µ', "onesuperior": '¹', "onequarter": '¼',
"onehalf": '½', "threequarters": '¾', "multiply": '×', "divide": '÷',
"Agrave": 'À', "Aacute": 'Á', "Acircumflex": 'Â', "Atilde": 'Ã',
"Adieresis": 'Ä', "Aring": 'Å', "Ccedilla": 'Ç', "Egrave": 'È',
"Eacute": 'É', "Ecircumflex": 'Ê', "Edieresis": 'Ë', "Igrave": 'Ì',
"Iacute": 'Í', "Icircumflex": 'Î', "Idieresis": 'Ï', "Eth": 'Ð',
"Ntilde": 'Ñ', "Ograve": 'Ò', "Oacute": 'Ó', "Ocircumflex": 'Ô',
"Otilde": 'Õ', "Odieresis": 'Ö', "Ugrave": 'Ù', "Uacute": 'Ú',
"Ucircumflex": 'Û', "Udieresis": 'Ü', "Yacute": 'Ý', "Thorn": 'Þ',
"agrave": 'à', "aacute": 'á', "acircumflex": 'â', "atilde": 'ã',
"adieresis": 'ä', "aring": 'å', "ccedilla": 'ç', "egrave": 'è',
"eacute": 'é', "ecircumflex": 'ê', "edieresis": 'ë', "igrave": 'ì',
"iacute": 'í', "icircumflex": 'î', "idieresis": 'ï', "eth": 'ð',
"ntilde": 'ñ', "ograve": 'ò', "oacute": 'ó', "ocircumflex": 'ô',
"otilde": 'õ', "odieresis": 'ö', "ugrave": 'ù', "uacute": 'ú',
"ucircumflex": 'û', "udieresis": 'ü', "yacute": 'ý', "thorn": 'þ',
"ydieresis": 'ÿ',
}
// RuneOfGlyphName turns a glyph name into the character it stands for, through
// the table above or through the two conventions the specification defines for
// naming a character directly. ok is false for a name that says nothing about
// which character it is, which a subsetted font's own names often do not.
func RuneOfGlyphName(name string) (rune, bool) {
s, ok := TextOfGlyphName(name)
if !ok {
return 0, false
}
r := []rune(s)
if len(r) != 1 {
return 0, false
}
return r[0], true
}
// TextOfGlyphName turns a glyph name into the text it stands for, which is not
// always one character: a name may say it is a ligature of several.
//
// Three of the rules here are the ones the Adobe Glyph List lays down for
// reading a name nobody has listed, and each was costing real text. A name may
// carry a variant after a full stop — "a.sc" is a small-capital A and is still
// an A, 7 272 of them in the corpus. A name may be several component names
// with underscores between them — "f_i" is the fi ligature, and without this
// "Definition" comes back "Denition", which is not a missing character so much
// as a misspelt word. And a name may simply be one nobody thought to list.
func TextOfGlyphName(name string) (string, bool) {
if name == "" {
return "", false
}
if r, ok := oneGlyphRune(name); ok {
return string(r), true
}
if seq, ok := unicodeSequence(name); ok {
return seq, true
}
// A variant of a character is still that character: the part before the
// first full stop is the name, and what follows says which cut of it.
if base, _, found := strings.Cut(name, "."); found {
if base == "" {
return "", false
}
return TextOfGlyphName(base)
}
// A name made of parts with underscores between them is the parts, in
// order — which is how a ligature is named when it has no name of its own.
if strings.Contains(name, "_") {
// Every part that can be read contributes at least one character, and
// a part that cannot gives up on the whole name, so what comes out of
// this is never empty.
var out strings.Builder
for _, part := range strings.Split(name, "_") {
piece, ok := TextOfGlyphName(part)
if !ok {
return "", false
}
out.WriteString(piece)
}
return out.String(), true
}
return "", false
}
// oneGlyphRune is a name that stands for exactly one character, by any of the
// ways a name can.
func oneGlyphRune(name string) (rune, bool) {
if r, ok := glyphRunes[name]; ok {
return r, true
}
if r, ok := latinExtendedRunes[name]; ok {
return r, true
}
if r, ok := greekAndMathRunes[name]; ok {
return r, true
}
if len(name) == 1 {
return rune(name[0]), true
}
if r, ok := parseHexName(name, "uni", 4); ok {
return r, true
}
if r, ok := parseHexName(name, "u", 4); ok {
return r, true
}
return 0, false
}
// unicodeSequence reads the uniXXXXYYYY form, which names several characters
// at once: a letter and the accent that goes over it, or the three pieces a
// Hebrew cluster is written in.
//
// It has to be told apart from something that looks exactly like it. Producers
// write a glyph's own number in the same shape — uni00000048 is glyph 72, not
// U+0000 followed by U+0048 — and 49 740 names in the corpus are that. The two
// are separable by one observation: a real sequence never begins with U+0000,
// because nothing is written after a character that does not exist. Of the
// 3 875 genuine sequences found across 14 823 embedded fonts, not one starts
// with a zero group; of the disguised glyph numbers, all of them do.
func unicodeSequence(name string) (string, bool) {
digits, ok := strings.CutPrefix(name, "uni")
if !ok || len(digits) < 8 || len(digits)%4 != 0 {
return "", false
}
var out strings.Builder
for i := 0; i < len(digits); i += 4 {
r, ok := parseHexName("uni"+digits[i:i+4], "uni", 4)
if !ok {
return "", false
}
if r == 0 {
// A glyph number wearing a character's clothes.
return "", false
}
out.WriteRune(r)
}
return out.String(), true
}
// parseHexName reads the uniXXXX and uXXXX conventions.
func parseHexName(name, prefix string, minDigits int) (rune, bool) {
if len(name) <= len(prefix) || name[:len(prefix)] != prefix {
return 0, false
}
digits := name[len(prefix):]
if len(digits) < minDigits || len(digits) > 6 {
return 0, false
}
var v rune
for i := 0; i < len(digits); i++ {
d := hexDigit(digits[i])
if d < 0 {
return 0, false
}
v = v<<4 | rune(d)
}
return v, true
}
// hexDigit reads one hexadecimal digit, or reports -1.
func hexDigit(c byte) int {
switch {
case c >= '0' && c <= '9':
return int(c - '0')
case c >= 'A' && c <= 'F':
return int(c-'A') + 10
case c >= 'a' && c <= 'f':
return int(c-'a') + 10
}
return -1
}
+71
View File
@@ -0,0 +1,71 @@
package pdffont
// The glyph names above cover the Latin every English document is set in and
// stop there. A document in Czech, Polish, Slovak, Hungarian, Turkish,
// Romanian, Latvian, Lithuanian or Maltese is set in the block that follows,
// and every one of its accented letters was being read as nothing at all —
// which is worse than it sounds, because a dropped character leaves no mark:
// "Příliš" comes back "Píliš" and looks like a word.
//
// Counted across the 118 833-file corpus: of 52.4 million glyph names, 483 382
// could not be turned into text, and the largest nameable share of those was
// this block — rcaron 650, zdotaccent 617, Ccaron 611, nacute 602, aogonek
// 602, scedilla 597, gbreve 597, and so on for a hundred more.
//
// These are the names the Adobe Glyph List gives, and the characters are not
// in doubt: unlike the subsetted fonts that call their glyphs g0 or dress a
// glyph number up as uni00000048, a name like "rcaron" says exactly one thing.
var latinExtendedRunes = map[string]rune{
"Amacron": 'Ā', "amacron": 'ā', "Abreve": 'Ă', "abreve": 'ă',
"Aogonek": 'Ą', "aogonek": 'ą', "Cacute": 'Ć', "cacute": 'ć',
"Ccircumflex": 'Ĉ', "ccircumflex": 'ĉ', "Cdotaccent": 'Ċ', "cdotaccent": 'ċ',
"Ccaron": 'Č', "ccaron": 'č', "Dcaron": 'Ď', "dcaron": 'ď',
"Dcroat": 'Đ', "dcroat": 'đ', "Dslash": 'Đ', "dmacron": 'đ',
"Emacron": 'Ē', "emacron": 'ē', "Ebreve": 'Ĕ', "ebreve": 'ĕ',
"Edotaccent": 'Ė', "edotaccent": 'ė', "Eogonek": 'Ę', "eogonek": 'ę',
"Ecaron": 'Ě', "ecaron": 'ě', "Gcircumflex": 'Ĝ', "gcircumflex": 'ĝ',
"Gbreve": 'Ğ', "gbreve": 'ğ', "Gdotaccent": 'Ġ', "gdotaccent": 'ġ',
"Gcommaaccent": 'Ģ', "gcommaaccent": 'ģ',
"Hcircumflex": 'Ĥ', "hcircumflex": 'ĥ', "Hbar": 'Ħ', "hbar": 'ħ',
"Itilde": 'Ĩ', "itilde": 'ĩ', "Imacron": 'Ī', "imacron": 'ī',
"Ibreve": 'Ĭ', "ibreve": 'ĭ', "Iogonek": 'Į', "iogonek": 'į',
"Idotaccent": 'İ', "dotlessi": 'ı', "IJ": 'IJ', "ij": 'ij',
"Jcircumflex": 'Ĵ', "jcircumflex": 'ĵ',
"Kcommaaccent": 'Ķ', "kcommaaccent": 'ķ', "kgreenlandic": 'ĸ',
"Lacute": 'Ĺ', "lacute": 'ĺ', "Lcommaaccent": 'Ļ', "lcommaaccent": 'ļ',
"Lcaron": 'Ľ', "lcaron": 'ľ', "Ldot": 'Ŀ', "ldot": 'ŀ',
"Nacute": 'Ń', "nacute": 'ń', "Ncommaaccent": 'Ņ', "ncommaaccent": 'ņ',
"Ncaron": 'Ň', "ncaron": 'ň', "napostrophe": 'ʼn', "Eng": 'Ŋ', "eng": 'ŋ',
"Omacron": 'Ō', "omacron": 'ō', "Obreve": 'Ŏ', "obreve": 'ŏ',
"Ohungarumlaut": 'Ő', "ohungarumlaut": 'ő',
"Racute": 'Ŕ', "racute": 'ŕ', "Rcommaaccent": 'Ŗ', "rcommaaccent": 'ŗ',
"Rcaron": 'Ř', "rcaron": 'ř', "Sacute": 'Ś', "sacute": 'ś',
"Scircumflex": 'Ŝ', "scircumflex": 'ŝ', "Scedilla": 'Ş', "scedilla": 'ş',
"Tcommaaccent": 'Ţ', "tcommaaccent": 'ţ', "Tcaron": 'Ť', "tcaron": 'ť',
"Tbar": 'Ŧ', "tbar": 'ŧ', "Utilde": 'Ũ', "utilde": 'ũ',
"Umacron": 'Ū', "umacron": 'ū', "Ubreve": 'Ŭ', "ubreve": 'ŭ',
"Uring": 'Ů', "uring": 'ů', "Uhungarumlaut": 'Ű', "uhungarumlaut": 'ű',
"Uogonek": 'Ų', "uogonek": 'ų', "Wcircumflex": 'Ŵ', "wcircumflex": 'ŵ',
"Ycircumflex": 'Ŷ', "ycircumflex": 'ŷ',
"Zacute": 'Ź', "zacute": 'ź', "Zdotaccent": 'Ż', "zdotaccent": 'ż',
"longs": 'ſ',
// A handful outside that block that the same documents use: the Romanian
// letters written with a comma below rather than a cedilla, the Turkish
// and Azeri schwa, and the two the Adobe list names for Welsh and Maltese.
"Scommaaccent": 'Ș', "scommaaccent": 'ș',
"Afii10017": 'А', "afii10017": 'А',
"Wgrave": 'Ẁ', "wgrave": 'ẁ', "Wacute": 'Ẃ', "wacute": 'ẃ',
"Wdieresis": 'Ẅ', "wdieresis": 'ẅ',
"Ygrave": 'Ỳ', "ygrave": 'ỳ',
"Hcaron": 'Ȟ', "hcaron": 'ȟ',
// The rest of what the corpus asks for and nothing could answer, each of
// them a name the Adobe list gives and none of them in doubt: the cedilla
// spellings of the Romanian letters, the capital sharp s, the script ell
// mathematicians write lengths with, and the angle brackets.
"Tcedilla": 'Ţ', "tcedilla": 'ţ', "Germandbls": 'ẞ',
"lscript": 'ℓ', "openbullet": '◦',
"angbracketleft": '〈', "angbracketright": '〉',
"Scommabelow": 'Ș', "scommabelow": 'ș',
"Tcommabelow": 'Ț', "tcommabelow": 'ț',
}
+245
View File
@@ -0,0 +1,245 @@
// Package pdffont is the PDF side of a font: not the outlines, which are the
// font program's business, but everything the document itself says about how a
// string of bytes is to be read. Which codes a string holds, how wide each one
// is, what each is called, and what text each stands for.
//
// It is what a renderer needs before it can ask a font program for a glyph,
// and all an extractor needs to read a page back as words. Both would
// otherwise have to say the same things twice.
package pdffont
import (
"github.com/go-pdfkit/reader"
)
// A Kind says how a font's codes are read.
type Kind int
const (
// Simple fonts take one byte a code and are addressed by glyph name.
Simple Kind = iota
// Composite fonts take a character identifier, usually two bytes wide.
Composite
// Type3 fonts draw each glyph by running a little content stream.
Type3
)
// A Font is a font as the document describes it.
type Font struct {
kind Kind
dict reader.Dict
descriptor reader.Dict
doc *reader.Document
names map[int]string // what each code is called, for a simple font
// chosen says which of those names the document chose itself, in a
// Differences array, rather than inheriting from a base encoding.
chosen map[int]bool
widths map[int]float64 // in text space: a thousandth of the point size
defaultW float64
toUni map[int]string
symbolic bool
// fallback is a last resort for codes the document itself says nothing
// about: what the embedded font program calls them. A caller that can
// read the program installs one.
fallback func(code int) (string, bool)
// cidToGID maps a character identifier to a glyph number, for a composite
// font whose descendant carries a map rather than using them as they are.
cidToGID []byte
// The pieces only a Type 3 font has.
fontMatrix [6]float64
charProcs reader.Dict
t3Res reader.Dict
}
// Read takes what a font dictionary says. It never fails: a dictionary that
// says nothing useful gives a font that reads its codes one byte at a time and
// advances by half an em, which is what a reader has to do with one anyway.
func Read(d *reader.Document, dict reader.Dict) *Font {
f := &Font{
doc: d, dict: dict, widths: map[int]float64{}, defaultW: 0.5,
fontMatrix: [6]float64{0.001, 0, 0, 0.001, 0, 0},
}
switch sub, _ := reader.ToName(resolve(d, dict.Get("Subtype"))); sub {
case "Type0":
f.kind = Composite
f.readComposite()
case "Type3":
f.kind = Type3
f.readType3()
default:
f.kind = Simple
f.readSimple()
}
f.toUni = f.readToUnicode()
return f
}
// Kind says how this font's codes are read.
func (f *Font) Kind() Kind { return f.kind }
// Dict is the font dictionary itself, for whatever a caller needs that this
// does not say.
func (f *Font) Dict() reader.Dict { return f.dict }
// Descriptor is the font descriptor, which is where an embedded program lives.
func (f *Font) Descriptor() reader.Dict { return f.descriptor }
// Symbolic reports a font that says it is addressed through its own character
// map rather than through an encoding of names.
func (f *Font) Symbolic() bool { return f.symbolic }
// FontMatrix is how a Type 3 font's glyph space relates to text space. For
// every other kind it is a thousandth, which is what the format assumes.
func (f *Font) FontMatrix() [6]float64 { return f.fontMatrix }
// CharProcs are a Type 3 font's glyph drawings, by name.
func (f *Font) CharProcs() reader.Dict { return f.charProcs }
// Type3Resources are what a Type 3 font's glyph drawings draw with.
func (f *Font) Type3Resources() reader.Dict { return f.t3Res }
// Codes cuts a string into the codes it holds: one byte at a time for a simple
// font, two for a composite one, which is what every composite encoding seen in
// the wild uses.
func (f *Font) Codes(s []byte) []int {
if f.kind != Composite {
out := make([]int, len(s))
for i, b := range s {
out[i] = int(b)
}
return out
}
out := make([]int, 0, len(s)/2)
for i := 0; i+1 < len(s); i += 2 {
out = append(out, int(s[i])<<8|int(s[i+1]))
}
return out
}
// Width is how far the pen moves for one code, in text space — a multiple of
// the point size, so a width of half means half an em.
func (f *Font) Width(code int) float64 {
if w, ok := f.widths[code]; ok {
return w
}
return f.defaultW
}
// HasWidth reports whether the document said how wide this code is, rather
// than the width being the fallback [Font.Width] gives for one it did not.
//
// It matters for a font the document does not carry. One of the fourteen
// standard faces is written with no widths at all — every reader is expected
// to know Times and Helvetica and Courier by heart — so a reader drawing a
// stand-in has to take the advances from the stand-in too. Half an em for
// every letter reads as a typewriter, which is not what the page says.
func (f *Font) HasWidth(code int) bool {
_, ok := f.widths[code]
return ok
}
// GlyphName is what the document's encoding calls a code. ok is false for a
// composite font, which names nothing, and for a code the encoding passes over.
func (f *Font) GlyphName(code int) (string, bool) {
name, ok := f.names[code]
return name, ok
}
// Text is what a code stands for as characters: what the font's ToUnicode map
// says, or failing that what its glyph name says. ok is false when neither
// does — a subsetted font naming its glyphs g17 and carrying no map cannot be
// read back, and saying so is better than guessing.
func (f *Font) Text(code int) (string, bool) {
if s, ok := f.toUni[code]; ok && s != "" {
return s, true
}
if name, ok := f.names[code]; ok && f.namedByTheDocument(code) {
// Not one character: a name may say it is a ligature of several, and
// a code that stands for two letters has to give back two.
if text, ok := TextOfGlyphName(name); ok {
return text, true
}
}
if f.fallback != nil {
if s, ok := f.fallback(code); ok && s != "" {
return s, true
}
}
return "", false
}
// SetFallback installs a last resort for codes the document says nothing
// about: what the embedded font program calls them.
//
// A font that says it is symbolic is addressed through its own character map,
// and what that map says is the only honest answer for a code the document did
// not name. Reading the program is another package's work, so a caller that
// can do it hands the answer in here.
func (f *Font) SetFallback(fn func(code int) (string, bool)) { f.fallback = fn }
// namedByTheDocument reports whether a code's name can be believed as a
// statement about which character it is.
//
// A font that says it is symbolic is addressed through its own character map,
// and the base encoding is then a guess. It is a bad one: a mathematical font
// puts a capital gamma where the standard encoding puts an inverted
// exclamation mark, and reading the name would give the wrong letter with
// nothing to say so. A name the document chose itself, in a Differences
// array, is a different matter — that one it meant.
func (f *Font) namedByTheDocument(code int) bool {
return !f.symbolic || f.chosen[code]
}
// Chosen reports whether the document named this code itself rather than
// inheriting the name from a base encoding.
func (f *Font) Chosen(code int) bool { return f.chosen[code] }
// CIDToGID maps a character identifier to a glyph number for a composite font.
// ok is false when the font's map does not reach that far.
func (f *Font) CIDToGID(cid int) (int, bool) {
if f.cidToGID == nil {
return cid, true
}
i := cid * 2
if i+1 >= len(f.cidToGID) {
return 0, false
}
return int(f.cidToGID[i])<<8 | int(f.cidToGID[i+1]), true
}
// Program is the embedded font program: which key it arrived under, and its
// bytes. ok is false for a font whose program is not embedded, which is every
// standard face and many others besides.
func (f *Font) Program() (reader.Name, []byte, bool) {
for _, key := range []reader.Name{"FontFile2", "FontFile3", "FontFile"} {
stream, ok := reader.ToStream(resolve(f.doc, f.descriptor.Get(key)))
if !ok {
continue
}
data, img, err := f.doc.DecodeStream(stream)
if err != nil || img != "" {
continue
}
return key, data, true
}
return "", nil, false
}
// resolve follows an indirect reference. A document that opened cannot fail to
// resolve one, so there is nothing to handle at every use.
func resolve(d *reader.Document, o reader.Object) reader.Object {
out, _ := d.Resolve(o)
return out
}
// floatOf reads a number, or gives a default.
func floatOf(o reader.Object, def float64) float64 {
if v, ok := reader.ToFloat(o); ok {
return v
}
return def
}
+69
View File
@@ -0,0 +1,69 @@
package pdffont
// greekAndMathRunes are the names a mathematical document gives its glyphs.
// They are not in the Latin encodings, so a page of equations addressed by
// name — which is what every TeX document is — could not be read back without
// them: the words came out and the symbols did not.
//
// The names are Adobe's, from the Symbol font and the glyph list every
// producer follows. They are checked against the documents themselves rather
// than taken on trust: a font that carries both a name for a code and a
// ToUnicode entry for it states the same fact twice, and the corpus has tens
// of thousands of such pairs to compare against.
var greekAndMathRunes = map[string]rune{
// The Greek alphabet, lower case and upper.
"alpha": 'α', "beta": 'β', "gamma": 'γ', "delta": 'δ', "epsilon": 'ε',
"zeta": 'ζ', "eta": 'η', "theta": 'θ', "iota": 'ι', "kappa": 'κ',
"lambda": 'λ', "mu1": 'μ', "nu": 'ν', "xi": 'ξ', "omicron": 'ο',
"pi": 'π', "rho": 'ρ', "sigma": 'σ', "sigma1": 'ς', "tau": 'τ',
"upsilon": 'υ', "phi": 'φ', "chi": 'χ', "psi": 'ψ', "omega": 'ω',
"theta1": 'ϑ', "phi1": 'ϕ', "epsilon1": 'ϵ', "rho1": 'ϱ', "omega1": 'ϖ',
"kappa1": 'ϰ', "pi1": 'ϖ',
// Adobe's list gives Delta and Omega the increment and ohm signs, which
// Unicode says are the same characters as the Greek letters written
// here — canonically equivalent, and these are the forms a person
// searching the text would type.
"Alpha": 'Α', "Beta": 'Β', "Gamma": 'Γ', "Delta": 'Δ', "Epsilon": 'Ε',
"Zeta": 'Ζ', "Eta": 'Η', "Theta": 'Θ', "Iota": 'Ι', "Kappa": 'Κ',
"Lambda": 'Λ', "Mu": 'Μ', "Nu": 'Ν', "Xi": 'Ξ', "Omicron": 'Ο',
"Pi": 'Π', "Rho": 'Ρ', "Sigma": 'Σ', "Tau": 'Τ', "Upsilon": 'Υ',
"Phi": 'Φ', "Chi": 'Χ', "Psi": 'Ψ', "Omega": 'Ω', "Upsilon1": 'ϒ',
// The signs an equation is built from.
"minus": '−', "plus": '+', "equal": '=', "asteriskmath": '∗',
"lessequal": '≤', "greaterequal": '≥', "notequal": '≠', "approxequal": '≈',
"equivalence": '≡', "similar": '∼', "congruent": '≅', "proportional": '∝',
"infinity": '∞', "partialdiff": '∂', "gradient": '∇', "integral": '∫',
"summation": '∑', "product": '∏', "radical": '√', "angle": '∠',
"perpendicular": '⊥', "therefore": '∴', "dotmath": '⋅', "bullet3": '∙',
"element": '∈', "notelement": '∉', "universal": '∀', "existential": '∃',
"emptyset": '∅', "intersection": '∩', "union": '∪',
"propersubset": '⊂', "propersuperset": '⊃',
"reflexsubset": '⊆', "reflexsuperset": '⊇', "notsubset": '⊄',
"logicaland": '∧', "logicalor": '∨', "logicalnot1": '¬',
"circleplus": '⊕', "circlemultiply": '⊗',
"prime": '′', "second": '″', "minute": '′',
// Arrows, and the brackets a display equation is built out of.
"arrowleft": '←', "arrowup": '↑', "arrowright": '→', "arrowdown": '↓',
"arrowboth": '↔', "arrowupdn": '↕',
"arrowdblleft": '⇐', "arrowdblup": '⇑', "arrowdblright": '⇒',
"arrowdbldown": '⇓', "arrowdblboth": '⇔',
"angleleft": '⟨', "angleright": '⟩',
"bracketlefttp": '⎡', "bracketleftbt": '⎣', "bracketleftex": '⎢',
"bracketrighttp": '⎤', "bracketrightbt": '⎦', "bracketrightex": '⎥',
"parenlefttp": '⎛', "parenleftbt": '⎝', "parenleftex": '⎜',
"parenrighttp": '⎞', "parenrightbt": '⎠', "parenrightex": '⎟',
"bracelefttp": '⎧', "braceleftmid": '⎨', "braceleftbt": '⎩',
"bracerighttp": '⎫', "bracerightmid": '⎬', "bracerightbt": '⎭',
"braceex": '⎪',
// The ligatures and marks a book is set with.
"ff": 'ff', "ffi": 'ffi', "ffl": 'ffl', "dotlessj": 'ȷ',
"circumflexbig": 'ˆ', "tildebig": '˜',
"suchthat": '∍', "carriagereturn": '↵', "aleph": 'ℵ',
"Ifraktur": 'ℑ', "Rfraktur": 'ℜ', "weierstrass": '℘',
"circlecopyrt": '©', "trademarksans": '™', "trademarkserif": '™',
"degree1": '°', "florin1": 'ƒ',
"lozenge": '◊', "spade": '♠', "club": '♣', "heart": '♥', "diamond": '♦',
}
+223
View File
@@ -0,0 +1,223 @@
package pdffont
import "github.com/go-pdfkit/reader"
// readSimple reads a font addressed one byte at a time: what each code is
// called, and how wide it is.
func (f *Font) readSimple() {
f.descriptor, _ = f.doc.GetDict(f.dict, "FontDescriptor")
f.symbolic = symbolicFlag(f.doc, f.descriptor)
f.names = f.readEncoding()
f.readSimpleWidths()
}
// readType3 reads a font whose glyphs are little drawings.
func (f *Font) readType3() {
f.descriptor, _ = f.doc.GetDict(f.dict, "FontDescriptor")
f.names = f.readEncoding()
f.charProcs, _ = f.doc.GetDict(f.dict, "CharProcs")
f.t3Res, _ = f.doc.GetDict(f.dict, "Resources")
if m := floatArray(f.doc, f.dict.Get("FontMatrix")); len(m) >= 6 {
copy(f.fontMatrix[:], m[:6])
}
f.readSimpleWidths()
// A Type 3 font's widths are in its own glyph space, which the font
// matrix maps onto text space.
for code, w := range f.widths {
f.widths[code] = w * 1000 * f.fontMatrix[0]
}
f.defaultW = 0
}
// symbolicFlag reads whether a font says it is addressed through its own
// character map rather than through an encoding of names.
func symbolicFlag(d *reader.Document, descriptor reader.Dict) bool {
flags, ok := reader.ToInt(resolve(d, descriptor.Get("Flags")))
if !ok {
return false
}
const symbolic, nonSymbolic = 1 << 2, 1 << 5
return flags&symbolic != 0 && flags&nonSymbolic == 0
}
// readEncoding works out what each code of a simple font is called: a base
// table, and then whatever differences the font names on top of it.
func (f *Font) readEncoding() map[int]string {
base := StandardEncoding
out := map[int]string{}
f.chosen = map[int]bool{}
enc := resolve(f.doc, f.dict.Get("Encoding"))
if name, ok := reader.ToName(enc); ok {
table := NamedEncoding(name, base)
fillNames(out, table)
// A named encoding is the document's own word on what its codes are,
// symbolic font or not.
for code, n := range table {
if n != "" {
f.chosen[code] = true
}
}
return out
}
encDict, ok := reader.ToDict(enc)
if !ok {
fillNames(out, base)
return out
}
if name, ok := reader.ToName(resolve(f.doc, encDict.Get("BaseEncoding"))); ok {
base = NamedEncoding(name, base)
}
fillNames(out, base)
f.applyDifferences(out, encDict)
return out
}
// NamedEncoding is the table a name stands for, or the fallback for a name
// that is not one of them.
func NamedEncoding(name reader.Name, fallback [256]string) [256]string {
switch name {
case "WinAnsiEncoding":
return WinAnsiEncoding
case "StandardEncoding", "MacRomanEncoding", "MacExpertEncoding":
// Mac Roman differs from the standard encoding above code 127, where
// almost nothing in a Latin document lives; using the standard table
// is closer than using none.
return StandardEncoding
}
return fallback
}
// fillNames copies a table into the map the rest of this works from.
func fillNames(out map[int]string, table [256]string) {
for code, name := range table {
if name != "" {
out[code] = name
}
}
}
// applyDifferences reads the array that names glyphs a code at a time: a
// number, then the names of the codes from there on.
func (f *Font) applyDifferences(out map[int]string, encDict reader.Dict) {
arr, ok := reader.ToArray(resolve(f.doc, encDict.Get("Differences")))
if !ok {
return
}
code := 0
for _, e := range arr {
v := resolve(f.doc, e)
if n, ok := reader.ToInt(v); ok {
code = int(n)
continue
}
if name, ok := reader.ToName(v); ok {
if code >= 0 && code < 256 {
out[code] = string(name)
f.chosen[code] = true
}
code++
}
}
}
// readSimpleWidths reads /Widths, which gives one width per code from
// /FirstChar on.
func (f *Font) readSimpleWidths() {
first, ok := reader.ToInt(resolve(f.doc, f.dict.Get("FirstChar")))
if !ok {
return
}
arr, ok := reader.ToArray(resolve(f.doc, f.dict.Get("Widths")))
if !ok {
return
}
for i, e := range arr {
if v, ok := reader.ToFloat(resolve(f.doc, e)); ok {
f.widths[int(first)+i] = v / 1000
}
}
if mw := floatOf(resolve(f.doc, f.descriptor.Get("MissingWidth")), -1); mw >= 0 {
f.defaultW = mw / 1000
}
}
// readComposite reads a font addressed by character identifier: its descendant
// holds the widths, the descriptor and the map from identifier to glyph.
func (f *Font) readComposite() {
f.defaultW = 1
arr, ok := reader.ToArray(resolve(f.doc, f.dict.Get("DescendantFonts")))
if !ok || len(arr) == 0 {
return
}
kid, ok := reader.ToDict(resolve(f.doc, arr[0]))
if !ok {
return
}
if v, ok := reader.ToFloat(resolve(f.doc, kid.Get("DW"))); ok {
f.defaultW = v / 1000
}
f.readCIDWidths(kid)
f.descriptor, _ = f.doc.GetDict(kid, "FontDescriptor")
f.symbolic = symbolicFlag(f.doc, f.descriptor)
if stream, ok := reader.ToStream(resolve(f.doc, kid.Get("CIDToGIDMap"))); ok {
if data, img, err := f.doc.DecodeStream(stream); err == nil && img == "" {
f.cidToGID = data
}
}
}
// readCIDWidths reads the /W array, which names widths one identifier at a
// time or a run at a time.
func (f *Font) readCIDWidths(kid reader.Dict) {
arr, ok := reader.ToArray(resolve(f.doc, kid.Get("W")))
if !ok {
return
}
for i := 0; i < len(arr); {
first, ok := reader.ToInt(resolve(f.doc, arr[i]))
if !ok || i+1 >= len(arr) {
return
}
next := resolve(f.doc, arr[i+1])
if list, ok := reader.ToArray(next); ok {
for k, e := range list {
if v, ok := reader.ToFloat(resolve(f.doc, e)); ok {
f.widths[int(first)+k] = v / 1000
}
}
i += 2
continue
}
last, ok := reader.ToInt(next)
if !ok || i+2 >= len(arr) {
return
}
w, ok := reader.ToFloat(resolve(f.doc, arr[i+2]))
if !ok {
return
}
if last >= first && last-first < 1<<20 {
for c := first; c <= last; c++ {
f.widths[int(c)] = w / 1000
}
}
i += 3
}
}
// floatArray reads an array of numbers, or nothing.
func floatArray(d *reader.Document, o reader.Object) []float64 {
arr, ok := reader.ToArray(resolve(d, o))
if !ok {
return nil
}
out := make([]float64, 0, len(arr))
for _, e := range arr {
v, ok := reader.ToFloat(resolve(d, e))
if !ok {
return nil
}
out = append(out, v)
}
return out
}
+252
View File
@@ -0,0 +1,252 @@
package pdffont
import (
"strings"
"unicode/utf16"
"github.com/go-pdfkit/reader"
)
// readToUnicode reads the font's /ToUnicode map, which is the one thing in a
// PDF that says what its text actually says. Without it a page can be drawn
// but not read: a subsetted font naming its glyphs by number carries no other
// clue.
func (f *Font) readToUnicode() map[int]string {
stream, ok := reader.ToStream(resolve(f.doc, f.dict.Get("ToUnicode")))
if !ok {
return nil
}
data, img, err := f.doc.DecodeStream(stream)
if err != nil || img != "" {
return nil
}
return ReadToUnicode(data)
}
// ReadToUnicode decodes a ToUnicode CMap: a little PostScript program whose
// only meaningful parts are the bfchar and bfrange blocks saying which
// characters each code stands for.
func ReadToUnicode(data []byte) map[int]string {
out := map[int]string{}
toks := cmapTokens(data)
for i := 0; i < len(toks); i++ {
switch toks[i].word {
case "beginbfchar":
i = readBFChar(toks, i+1, out)
case "beginbfrange":
i = readBFRange(toks, i+1, out)
}
}
if len(out) == 0 {
return nil
}
return out
}
// maxToUnicodeEntries is how many codes one map may name.
//
// A bfrange names a run of codes in about twenty bytes, and only the width of
// a single run was bounded — not how many runs a map could hold. So the size
// of the answer had nothing to do with the size of the question: 585 bytes
// produced 655 360 entries in 2.3 seconds, and 10 655 bytes produced
// 13 107 200 entries and a gigabyte. Every font on every page is read this
// way, so that was a gigabyte per font.
//
// The bound is not a guess. Across 5 338 /ToUnicode maps taken out of real
// documents — arXiv figures, government forms, and mozilla's pdf.js corpus —
// the median names 13 codes, the 99th percentile names 538, and the largest
// names exactly 65 536: one whole two-byte code space, which is as many codes
// as a font of that shape can have. This allows four times that, so a
// document has to be malformed or hostile to reach it, and a map that does is
// cut off there rather than being allowed to ask for everything.
const maxToUnicodeEntries = 1 << 18
// A cmapToken is one piece of such a program: a hexadecimal string, an array
// bracket, or a bare word.
type cmapToken struct {
word string
hex []byte
isHex bool
}
// cmapTokens cuts the program up. Only three shapes matter — a hexadecimal
// string, a bracket, and a word — and everything else can be a word.
func cmapTokens(b []byte) []cmapToken {
var out []cmapToken
for i := 0; i < len(b); {
c := b[i]
switch {
case c == '%':
for i < len(b) && b[i] != '\n' && b[i] != '\r' {
i++
}
case c == '<':
j := i + 1
for j < len(b) && b[j] != '>' {
j++
}
out = append(out, cmapToken{hex: unhexBytes(b[i+1 : min(j, len(b))]), isHex: true})
i = min(j+1, len(b))
case c == '[' || c == ']':
out = append(out, cmapToken{word: string(c)})
i++
case c <= ' ':
i++
default:
j := i
for j < len(b) && b[j] > ' ' && b[j] != '<' && b[j] != '[' && b[j] != ']' && b[j] != '%' {
j++
}
out = append(out, cmapToken{word: string(b[i:j])})
i = j
}
}
return out
}
// readBFChar reads pairs of "code, what it stands for" up to the block's end.
func readBFChar(toks []cmapToken, i int, out map[int]string) int {
for i+1 < len(toks) {
if toks[i].word == "endbfchar" {
return i
}
if len(out) >= maxToUnicodeEntries {
return i
}
if !toks[i].isHex || !toks[i+1].isHex {
return i
}
out[codeOf(toks[i].hex)] = textOf(toks[i+1].hex)
i += 2
}
return i
}
// readBFRange reads runs of codes: either a first character the run counts on
// from, or a list naming each one.
func readBFRange(toks []cmapToken, i int, out map[int]string) int {
for i+2 < len(toks) {
if toks[i].word == "endbfrange" {
return i
}
if !toks[i].isHex || !toks[i+1].isHex {
return i
}
lo, hi := codeOf(toks[i].hex), codeOf(toks[i+1].hex)
if hi < lo || hi-lo > 1<<16 {
return i
}
if len(out) >= maxToUnicodeEntries {
return i
}
switch {
case toks[i+2].isHex:
base := utf16Runes(toks[i+2].hex)
for c := lo; c <= hi && len(out) < maxToUnicodeEntries; c++ {
out[c] = countOn(base, c-lo)
}
i += 3
case toks[i+2].word == "[":
j := i + 3
for c := lo; c <= hi && j < len(toks) && toks[j].word != "]"; c++ {
if toks[j].isHex && len(out) < maxToUnicodeEntries {
out[c] = textOf(toks[j].hex)
}
j++
}
for j < len(toks) && toks[j].word != "]" {
j++
}
i = j + 1
default:
return i
}
}
return i
}
// codeOf reads a code, which is one or two bytes written as hexadecimal.
func codeOf(b []byte) int {
v := 0
for _, c := range b {
v = v<<8 | int(c)
}
return v
}
// textOf reads what a code stands for: characters written as sixteen-bit
// units, the way the format carries text.
func textOf(b []byte) string {
return string(utf16Runes(b))
}
// utf16Runes decodes the sixteen-bit units a ToUnicode value is written in.
func utf16Runes(b []byte) []rune {
units := make([]uint16, 0, len(b)/2)
for i := 0; i+1 < len(b); i += 2 {
units = append(units, uint16(b[i])<<8|uint16(b[i+1]))
}
return utf16.Decode(units)
}
// countOn is what the n'th code of a range stands for, counting on from the
// first: the last character moves and the rest stay, which is what the
// specification says a range means.
func countOn(base []rune, n int) string {
if len(base) == 0 {
return ""
}
out := append([]rune{}, base...)
out[len(out)-1] += rune(n)
return string(out)
}
// unhexBytes reads a hexadecimal string, ignoring whatever it was laid out
// with. An odd digit at the end is taken as the high half of a byte, which is
// what the format says.
func unhexBytes(b []byte) []byte {
var digits []byte
for _, c := range b {
if isHexDigit(c) {
digits = append(digits, hexVal(c))
}
}
if len(digits)%2 == 1 {
digits = append(digits, 0)
}
out := make([]byte, 0, len(digits)/2)
for i := 0; i+1 < len(digits); i += 2 {
out = append(out, digits[i]<<4|digits[i+1])
}
return out
}
func isHexDigit(c byte) bool {
return c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F'
}
func hexVal(c byte) byte {
switch {
case c <= '9':
return c - '0'
case c <= 'F':
return c - 'A' + 10
default:
return c - 'a' + 10
}
}
// min is the smaller of two numbers.
func min(a, b int) int {
if a < b {
return a
}
return b
}
// TrimText tidies what a map gives back: the format allows a value to carry
// the replacement character or nothing at all, and neither is text.
func TrimText(s string) string {
s = strings.TrimFunc(s, func(r rune) bool { return r == 0 || r == 0xFFFD })
return s
}
+3
View File
@@ -0,0 +1,3 @@
cov.out
coverage.out
*.pdf
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2026 the go-pdfkit/reader authors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
+144
View File
@@ -0,0 +1,144 @@
# reader
[![CI](https://github.com/go-pdfkit/reader/actions/workflows/ci.yml/badge.svg)](https://github.com/go-pdfkit/reader/actions/workflows/ci.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/go-pdfkit/reader.svg)](https://pkg.go.dev/github.com/go-pdfkit/reader)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-pdfkit/reader)](https://goreportcard.com/report/github.com/go-pdfkit/reader)
[![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](LICENSE)
[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](#testing)
A pure-Go, **zero-C** PDF **reader** — the parsing half of
[go-pdfkit](https://github.com/go-pdfkit). Where
[`pdfkit`](https://github.com/go-pdfkit/pdfkit) authors new PDF 1.7, this
module takes existing PDF bytes apart: lexer, object model, cross-reference
tables and streams, stream filters, the standard security handler, the page
tree, and content streams. It also writes the same object graph back out,
which is what every operation on an existing file needs.
Nothing outside the Go standard library is required, so it builds for
`GOOS=js/wasm` and every 64-bit architecture the fleet targets.
## Status
Landing in waves. This module currently provides the **object layer** and
the **document structure**:
- **Lexer** — the full PDF object syntax: integers and reals in the shapes
producers actually write, literal strings with their escapes, hex strings,
names with `#xx` escapes, arrays, dictionaries, streams, and keywords.
- **Objects** — `Null`, `Bool`, `Integer`, `Real`, `String`, `Name`, `Array`,
`Dict`, `Stream`, and indirect `Ref`, with the `N G R` / `N G obj` lookahead
resolved the way the specification requires.
- **Filters** — `FlateDecode`, `LZWDecode` (with PDF's `EarlyChange`, which the
standard library's LZW does not implement), `ASCIIHexDecode`,
`ASCII85Decode`, `RunLengthDecode`, `Crypt`, plus the PNG and TIFF
predictors. Image filters (`DCTDecode`, `JPXDecode`, `CCITTFaxDecode`,
`JBIG2Decode`) are reported rather than applied, so an image consumer gets
the encoded bytes.
- **Filters that fail** — a chain that cannot be run to the end is not the end
of the stream. `DecodeRecovering` returns what the filters before the failure
produced — the prefix a damaged Flate stream did inflate, not its compressed
bytes — and says so through `Decoded.Recovered` and `Decoded.Cause`. Bytes no
filter decoded arrive in `Decoded.Undecoded`, never in `Decoded.Data`, so a
compressed stream cannot be painted as samples or tokenised as content by a
caller that forgot to check a flag.
`Decode` is the strict reading and refuses such a stream outright, so a
caller that must not act on damaged data says which it wants by which one it
calls. Page content takes the lenient reading, because a page whose content
will not decode is still a page; `PageContentDecoded` reports whether any
salvaging happened. Cross-reference streams take the strict one: a table that
has to be guessed at is worse than no table, and the repair below is the
answer to one.
- **Cross-references** — classic tables, cross-reference **streams**, **object
streams**, `/Prev` chains, and the `/XRefStm` of a hybrid file, newest
definition winning.
- **Repair** — a file whose tables are missing, truncated or simply wrong is
rebuilt by scanning it for object headers, trailers and, failing those, for
a catalogue; a file that kept its pages but lost its catalogue gets one.
This is not an exceptional path: it is what makes a reader usable. The
rebuild establishes the file key before it reads object streams, so a file
whose catalogue lives in an encrypted one can be rebuilt at all; it takes
`/Encrypt` from a cross-reference stream's dictionary when the file has no
trailer keyword to name it in. Where two object streams claim the same
object, the later one in the file wins, the same rule the header scan
applies to objects written directly — so the same damaged file reads the
same way twice. When a rebuild fails, the error says what the rebuild found
and not merely that the tables could not be read.
- **Documents** — `Open`, object resolution with cycle and recursion guards,
the trailer, the catalogue, and the page tree with the four attributes a
page inherits from its ancestors.
- **Encryption** — the standard security handler in every revision, both ways:
reading RC4 at 40 and 128 bits, AESV2, and the AES-256 of `/R 5` and `/R 6`,
with crypt filters, `/Identity` and `/EncryptMetadata`; and writing AES-256
or AES-128, with permissions, a user password and an owner password.
A password is tried as the user password and as the owner password, and
`Open` uses the empty one, so a file protected only against editing opens
with no password at all.
and steps over rubbish rather than losing the operations around it, with
inline images read whole. Where an inline image ends is the one genuinely
ambiguous thing in a content stream, since its data may spell EI itself;
the length is computed from the image's own geometry where it can be, and
otherwise every candidate EI is tried until the data before one actually
decodes.
- **Writing** — the exact inverse of the parser: objects rendered in PDF
syntax with stable dictionary ordering and strings in whichever of the two
forms is shorter, and a `Writer` that copies whole object graphs out of one
or more documents, renumbering as it goes, then lays down a cross-reference
table and a trailer. `NewPackedWriter` instead puts what it can into
compressed **object streams** and ends in a **cross-reference stream**,
which is what makes a file small.
Measured against a corpus of **118 863 real PDFs** — Matplotlib, cairo,
pdfTeX, Ghostscript, Adobe, R, Apache FOP, PDF 1.3 through 1.7 — `Open`
succeeds on **118 833** of them and finds 138 337 pages, in 8 seconds, with no
panics. Of the thirty it refuses, twenty-seven are PNG files with a `.pdf`
extension, one is PostScript, and two are PDFs truncated past recovery.
The one encrypted PDF the corpus holds — `/V 4 /R 4`, written by macOS
Quartz — decrypts to readable metadata and a valid content stream, which is
the only genuinely independent check of the key derivation there is here;
the other revisions are round-tripped against a test encryptor written from
the same algorithms.
The content-stream tokeniser reads **1 536 769 753 operations** across those
138 337 pages in a minute and a half, with no panics, no page failing to
decode, and no operator outside the seventy the format defines — beyond four
`arc` and six `nan` written by a producer that was simply wrong.
Rewriting is checked the same way: every one of the **118 833** files that
open is copied object by object into a new file, which is then re-read and
compared on page count, media boxes and the bytes of every content stream.
**All 118 833 match**, 35.5 GB in and 35.2 GB out, in twenty-eight seconds —
and **all 118 833 match again** when the packed writer is used instead, which
brings the output down to **94.6%** of the input.
Next wave: the operations built on all of this.
## Install
```sh
go get github.com/go-pdfkit/reader
```
## Testing
```sh
go test -covermode=set ./...
```
CI gates on **exact 100% statement coverage**, `go vet`, and a cross-compile
across `linux/{amd64,arm64,riscv64,loong64,ppc64le,s390x}`, `js/wasm`,
`darwin/arm64` and `windows/amd64`.
## The rest of the family
[**reader**](https://github.com/go-pdfkit/reader) reads and writes the format ·
[**ops**](https://github.com/go-pdfkit/ops) is the verbs and the `pdfops` command ·
[**render**](https://github.com/go-pdfkit/render) turns a page into pixels ·
[**pdffont**](https://github.com/go-pdfkit/pdffont) is what a document says about a font ·
[**extract**](https://github.com/go-pdfkit/extract) reads a page back ·
[**coedit**](https://github.com/go-pdfkit/coedit) is a PDF several people edit at once ·
[**app**](https://github.com/go-pdfkit/app) is the browser workbench.
Documentation for all of it: <https://go-pdfkit.github.io/docs/>
## License
BSD-3-Clause — see [LICENSE](LICENSE). Copyright the go-pdfkit/reader authors.
+579
View File
@@ -0,0 +1,579 @@
package reader
import "fmt"
// CCITT Group 3 and Group 4 fax decoding, as used by /CCITTFaxDecode.
//
// WHY THIS IS HERE
//
// A scanned form is a fax. 67 of the 1 633 real forms in the corpus — the
// eleven issuing bodies, not the vendor test suites — carry a CCITT-encoded
// image, 263 images between them, and 6 of their pages have nothing else on
// them at all. Those six pages drew entirely blank, and a blank page is the
// failure a reader notices before any other.
//
// WHY IT BELONGS IN THE READER RATHER THAN IN A RENDERER
//
// The other filters this package stops at — DCT, JPX, JBIG2 — carry an image
// with its own idea of how many components it has and how deep they are. This
// one does not: it produces bilevel samples, one bit a pixel, rows padded to a
// byte, and the stream dictionary says what they mean. That is a byte stream,
// so it is a filter, and every caller gets it rather than each writing its own.
//
// THE REFERENCE READ BEFORE WRITING THIS
//
// ITU-T Recommendations T.4 (Group 3) and T.6 (Group 4), by way of the tables
// and the changing-element algorithm in golang.org/x/image/ccitt — read, not
// imported, because this package has no dependencies outside the standard
// library and gains none. The code tables below were extracted mechanically
// from that package's gen.go rather than retyped, because a table of 218
// variable-length codes transcribed by hand is a table with a mistake in it.
//
// The algorithm is the one T.6 Figure 1 describes. A row is decoded into one
// byte a pixel — 0xFF white, 0x00 black — and the row before it is the
// reference against which the two-dimensional modes are resolved:
//
// b1 b2
// v v
// prev: BBBBBwwwwwBBBwwwww
// curr: BBBwwwwwBBBBBBwwww
// ^ ^ ^
// a0 a1 a2
//
// a0 is where the pen is; a1 the next colour change to its right on this row;
// b1 the first change on the row above that is to the right of a0 and of the
// opposite colour to it; b2 the next change after b1. Pass mode says a1 is at
// or beyond b2; horizontal mode reads two runs and ignores the row above;
// vertical mode puts a1 at b1 plus an offset between -3 and +3.
// The two-dimensional modes of T.4 Table 1.
const (
modePass = iota
modeH
modeV0
modeVR1
modeVR2
modeVR3
modeVL1
modeVL2
modeVL3
modeExt
)
// A ccittCode is one row of a code table: the value a code names, and the bits
// that name it, most significant first.
type ccittCode struct {
value int
bits string
}
// ccittModeCodes is Table 1 of ITU-T T.4: the two-dimensional modes.
var ccittModeCodes = []ccittCode{
{modePass, "0001"}, {modeH, "001"}, {modeV0, "1"}, {modeVR1, "011"},
{modeVR2, "000011"}, {modeVR3, "0000011"}, {modeVL1, "010"}, {modeVL2, "000010"},
{modeVL3, "0000010"}, {modeExt, "0000001"},
}
// ccittWhiteCodes is Tables 2 and 3 of ITU-T T.4 for a white run: the
// terminating codes 0 to 63, then the make-up codes in steps of 64.
var ccittWhiteCodes = []ccittCode{
{0, "00110101"}, {1, "000111"}, {2, "0111"}, {3, "1000"},
{4, "1011"}, {5, "1100"}, {6, "1110"}, {7, "1111"},
{8, "10011"}, {9, "10100"}, {10, "00111"}, {11, "01000"},
{12, "001000"}, {13, "000011"}, {14, "110100"}, {15, "110101"},
{16, "101010"}, {17, "101011"}, {18, "0100111"}, {19, "0001100"},
{20, "0001000"}, {21, "0010111"}, {22, "0000011"}, {23, "0000100"},
{24, "0101000"}, {25, "0101011"}, {26, "0010011"}, {27, "0100100"},
{28, "0011000"}, {29, "00000010"}, {30, "00000011"}, {31, "00011010"},
{32, "00011011"}, {33, "00010010"}, {34, "00010011"}, {35, "00010100"},
{36, "00010101"}, {37, "00010110"}, {38, "00010111"}, {39, "00101000"},
{40, "00101001"}, {41, "00101010"}, {42, "00101011"}, {43, "00101100"},
{44, "00101101"}, {45, "00000100"}, {46, "00000101"}, {47, "00001010"},
{48, "00001011"}, {49, "01010010"}, {50, "01010011"}, {51, "01010100"},
{52, "01010101"}, {53, "00100100"}, {54, "00100101"}, {55, "01011000"},
{56, "01011001"}, {57, "01011010"}, {58, "01011011"}, {59, "01001010"},
{60, "01001011"}, {61, "00110010"}, {62, "00110011"}, {63, "00110100"},
{64, "11011"}, {128, "10010"}, {192, "010111"}, {256, "0110111"},
{320, "00110110"}, {384, "00110111"}, {448, "01100100"}, {512, "01100101"},
{576, "01101000"}, {640, "01100111"}, {704, "011001100"}, {768, "011001101"},
{832, "011010010"}, {896, "011010011"}, {960, "011010100"}, {1024, "011010101"},
{1088, "011010110"}, {1152, "011010111"}, {1216, "011011000"}, {1280, "011011001"},
{1344, "011011010"}, {1408, "011011011"}, {1472, "010011000"}, {1536, "010011001"},
{1600, "010011010"}, {1664, "011000"}, {1728, "010011011"}, {1792, "00000001000"},
{1856, "00000001100"}, {1920, "00000001101"}, {1984, "000000010010"}, {2048, "000000010011"},
{2112, "000000010100"}, {2176, "000000010101"}, {2240, "000000010110"}, {2304, "000000010111"},
{2368, "000000011100"}, {2432, "000000011101"}, {2496, "000000011110"}, {2560, "000000011111"},
}
// ccittBlackCodes is Tables 2 and 3 of ITU-T T.4 for a black run.
var ccittBlackCodes = []ccittCode{
{0, "0000110111"}, {1, "010"}, {2, "11"}, {3, "10"},
{4, "011"}, {5, "0011"}, {6, "0010"}, {7, "00011"},
{8, "000101"}, {9, "000100"}, {10, "0000100"}, {11, "0000101"},
{12, "0000111"}, {13, "00000100"}, {14, "00000111"}, {15, "000011000"},
{16, "0000010111"}, {17, "0000011000"}, {18, "0000001000"}, {19, "00001100111"},
{20, "00001101000"}, {21, "00001101100"}, {22, "00000110111"}, {23, "00000101000"},
{24, "00000010111"}, {25, "00000011000"}, {26, "000011001010"}, {27, "000011001011"},
{28, "000011001100"}, {29, "000011001101"}, {30, "000001101000"}, {31, "000001101001"},
{32, "000001101010"}, {33, "000001101011"}, {34, "000011010010"}, {35, "000011010011"},
{36, "000011010100"}, {37, "000011010101"}, {38, "000011010110"}, {39, "000011010111"},
{40, "000001101100"}, {41, "000001101101"}, {42, "000011011010"}, {43, "000011011011"},
{44, "000001010100"}, {45, "000001010101"}, {46, "000001010110"}, {47, "000001010111"},
{48, "000001100100"}, {49, "000001100101"}, {50, "000001010010"}, {51, "000001010011"},
{52, "000000100100"}, {53, "000000110111"}, {54, "000000111000"}, {55, "000000100111"},
{56, "000000101000"}, {57, "000001011000"}, {58, "000001011001"}, {59, "000000101011"},
{60, "000000101100"}, {61, "000001011010"}, {62, "000001100110"}, {63, "000001100111"},
{64, "0000001111"}, {128, "000011001000"}, {192, "000011001001"}, {256, "000001011011"},
{320, "000000110011"}, {384, "000000110100"}, {448, "000000110101"}, {512, "0000001101100"},
{576, "0000001101101"}, {640, "0000001001010"}, {704, "0000001001011"}, {768, "0000001001100"},
{832, "0000001001101"}, {896, "0000001110010"}, {960, "0000001110011"}, {1024, "0000001110100"},
{1088, "0000001110101"}, {1152, "0000001110110"}, {1216, "0000001110111"}, {1280, "0000001010010"},
{1344, "0000001010011"}, {1408, "0000001010100"}, {1472, "0000001010101"}, {1536, "0000001011010"},
{1600, "0000001011011"}, {1664, "0000001100100"}, {1728, "0000001100101"}, {1792, "00000001000"},
{1856, "00000001100"}, {1920, "00000001101"}, {1984, "000000010010"}, {2048, "000000010011"},
{2112, "000000010100"}, {2176, "000000010101"}, {2240, "000000010110"}, {2304, "000000010111"},
{2368, "000000011100"}, {2432, "000000011101"}, {2496, "000000011110"}, {2560, "000000011111"},
}
// A ccittTable decodes one of the code tables. Codes are grouped by their
// length, which is the whole of the decoding: read a bit, lengthen the code by
// one, and ask whether a code of that length says anything. A prefix-free code
// cannot answer twice, so the first answer is the only one.
type ccittTable struct {
byLength []map[uint32]int
}
// newCCITTTable groups a table's codes by length. The bits are read from the
// strings the tables are written in, so what the code says and what the
// specification says are the same text.
func newCCITTTable(codes []ccittCode) *ccittTable {
longest := 0
for _, c := range codes {
if len(c.bits) > longest {
longest = len(c.bits)
}
}
t := &ccittTable{byLength: make([]map[uint32]int, longest+1)}
for _, c := range codes {
var v uint32
for _, b := range c.bits {
v <<= 1
if b == '1' {
v |= 1
}
}
n := len(c.bits)
if t.byLength[n] == nil {
t.byLength[n] = map[uint32]int{}
}
t.byLength[n][v] = c.value
}
return t
}
var (
ccittModes = newCCITTTable(ccittModeCodes)
ccittWhite = newCCITTTable(ccittWhiteCodes)
ccittBlack = newCCITTTable(ccittBlackCodes)
)
// A ccittBits reads one bit at a time, most significant first, which is the
// order a fax is written in.
type ccittBits struct {
data []byte
pos int // in bits
}
func (b *ccittBits) next() (uint32, bool) {
if b.pos >= 8*len(b.data) {
return 0, false
}
bit := (b.data[b.pos/8] >> (7 - uint(b.pos%8))) & 1
b.pos++
return uint32(bit), true
}
// align moves to the next byte boundary, which /EncodedByteAlign asks for at
// the start of every row.
func (b *ccittBits) align() { b.pos = (b.pos + 7) &^ 7 }
func (b *ccittBits) exhausted() bool { return b.pos >= 8*len(b.data) }
// restIsFill says whether nothing but zero bits remain. A fax is padded to a
// byte, and often to rather more than a byte, and zeros decode as perfectly
// good two-dimensional modes: pass mode is 0001 and vertical-left-3 is 0000010,
// so a decoder that does not stop here goes on inventing rows that look like
// the last real one. One French form gave 1 636 rows for a 208-row image that
// way, and every one of the surplus rows had plausible ink on it.
func (b *ccittBits) restIsFill() bool {
for i := b.pos; i < 8*len(b.data); i++ {
if b.data[i/8]&(1<<(7-uint(i%8))) != 0 {
return false
}
}
return true
}
// read decodes one code. It returns false at the end of the data or on a code
// the table does not name — the caller decides which of those is fatal, since a
// truncated fax is common and worth showing as far as it goes.
func (t *ccittTable) read(b *ccittBits) (int, bool) {
var code uint32
for n := 1; n < len(t.byLength); n++ {
bit, ok := b.next()
if !ok {
return 0, false
}
code = code<<1 | bit
if v, ok := t.byLength[n][code]; ok {
return v, true
}
}
return 0, false
}
// skipEOL consumes an end-of-line code, 000000000001, if one is next, and any
// fill bits before it. A Group 3 fax may have one at the end of every row, may
// have none at all, and may have several together at the end; all three are
// found in the corpus, so none of them may be an error.
func (b *ccittBits) skipEOL() bool {
start := b.pos
zeros := 0
for {
bit, ok := b.next()
if !ok {
b.pos = start
return false
}
if bit == 0 {
zeros++
continue
}
if zeros >= 11 {
return true
}
b.pos = start
return false
}
}
// ccittParams are the decode parameters of /CCITTFaxDecode, with the defaults
// the specification gives.
type ccittParams struct {
k int // <0 Group 4, 0 Group 3 one-dimensional, >0 Group 3 mixed
columns int // default 1728
rows int // 0 means as many as the data holds
blackIs1 bool // false: a 0 bit is black
encodedByteAlign bool
endOfBlock bool // default true: the data ends with a block marker
}
func ccittParamsOf(parm Dict, r Resolver) ccittParams {
return ccittParams{
k: intParm(parm, "K", 0, r),
columns: intParm(parm, "Columns", 1728, r),
rows: intParm(parm, "Rows", 0, r),
blackIs1: boolParm(parm, "BlackIs1", false, r),
encodedByteAlign: boolParm(parm, "EncodedByteAlign", false, r),
endOfBlock: boolParm(parm, "EndOfBlock", true, r),
}
}
// endOfBlockEOLs is how many end-of-line codes in a row mean the data has
// ended: two for Group 4, which has none between its rows, and six for Group 3,
// where every row may legitimately be followed by one.
func endOfBlockEOLs(k int) int {
if k < 0 {
return 2
}
return 6
}
// maxCCITTPixels bounds what one image may decode to, the way maxDecodedSize
// bounds a Flate stream. A fax names its own width and height in the decode
// parameters, so a file can ask for an image of any size it likes; 2^28 pixels
// is more than four times A4 at 600 dots to the inch.
var maxCCITTPixels = 1 << 28
// ccittDecode turns a fax into bilevel samples: one bit a pixel, each row
// padded to a byte boundary, which is what every other image filter in this
// package produces and what a stream dictionary's /Width, /Height and
// /BitsPerComponent describe.
func ccittDecode(data []byte, p ccittParams) ([]byte, error) {
if p.columns <= 0 {
return nil, fmt.Errorf("reader: /CCITTFaxDecode names %d columns", p.columns)
}
if p.rows < 0 {
return nil, fmt.Errorf("reader: /CCITTFaxDecode names %d rows", p.rows)
}
if p.rows > 0 && p.columns > maxCCITTPixels/p.rows {
return nil, fmt.Errorf("reader: /CCITTFaxDecode names %d by %d pixels, "+
"past the limit of %d", p.columns, p.rows, maxCCITTPixels)
}
b := &ccittBits{data: data}
stride := (p.columns + 7) / 8
curr := make([]byte, p.columns)
prev := []byte(nil)
out := []byte(nil)
for row := 0; p.rows == 0 || row < p.rows; row++ {
if p.rows == 0 && len(out)/stride*p.columns >= maxCCITTPixels {
return nil, fmt.Errorf("reader: /CCITTFaxDecode ran past %d pixels "+
"without naming how many rows it has", maxCCITTPixels)
}
if p.encodedByteAlign {
b.align()
}
// A Group 3 row may be introduced by an end-of-line code. Consuming it
// here rather than after the row means a file that has them and a file
// that does not are read the same way — and counting them is how the
// end of the data announces itself, since the block marker is nothing
// but end-of-line codes in a row.
eols := 0
for b.skipEOL() {
eols++
}
if p.endOfBlock && eols >= endOfBlockEOLs(p.k) {
break
}
if b.exhausted() || b.restIsFill() {
break
}
twoDimensional := p.k < 0
if p.k > 0 {
// In mixed mode a bit after the end-of-line says which the row is:
// 1 for one-dimensional, 0 for two.
// The data was checked for bits a moment ago, so this cannot
// fail; the value is taken rather than the error handled, because
// a branch that cannot be reached cannot be tested.
bit, _ := b.next()
twoDimensional = bit == 0
}
if err := ccittRow(b, curr, prev, twoDimensional); err != nil {
if len(out) == 0 {
return nil, err
}
// A truncated fax is worth showing as far as it got: the rows
// already decoded are real, and refusing them turns a damaged
// scan into a blank page. Leaving the loop rather than returning
// here matters — the padding below is what makes the answer as
// long as /Rows promised, and a fuzz case of two bytes found that
// returning early handed the caller one row where it had asked
// for four, with no error to say so.
break
}
out = append(out, ccittPack(curr, stride, p.blackIs1)...)
if prev == nil {
prev = make([]byte, p.columns)
}
copy(prev, curr)
}
// A file that says how many rows it has gets that many, padded with white
// if the data ran out, so the samples match the /Height the dictionary
// promises rather than being short by a row.
if p.rows > 0 {
white := make([]byte, stride)
if !p.blackIs1 {
for i := range white {
white[i] = 0xFF
}
}
for len(out) < p.rows*stride {
out = append(out, white...)
}
}
return out, nil
}
// ccittPack turns a row of one byte a pixel into one bit a pixel. With
// /BlackIs1 false — the default — a 0 bit is black, so white becomes a 1.
func ccittPack(row []byte, stride int, blackIs1 bool) []byte {
out := make([]byte, stride)
for x, v := range row {
white := v != 0
if white != blackIs1 {
out[x/8] |= 1 << (7 - uint(x%8))
}
}
return out
}
// ccittRow decodes one row into curr, one byte a pixel: 0xFF white, 0x00 black.
func ccittRow(b *ccittBits, curr, prev []byte, twoDimensional bool) error {
for i := range curr {
curr[i] = 0
}
at, white, first := 0, true, true
for at < len(curr) {
if !twoDimensional {
n, err := ccittRun(b, white)
if err != nil {
return err
}
if at+n > len(curr) {
return fmt.Errorf("reader: a fax row of %d pixels named %d",
len(curr), at+n)
}
fill(curr[at:at+n], white)
at += n
white = !white
first = false
continue
}
mode, ok := ccittModes.read(b)
if !ok {
return fmt.Errorf("reader: a fax row named no mode this reader knows")
}
switch mode {
case modePass:
// ccittFindB starts where the pen is and only walks forward,
// stopping at the end of the row, so b2 is always a pixel of this
// row at or after the pen. There is nothing to check.
b2 := ccittFindB(prev, curr, at, white, first, true)
fill(curr[at:b2], white)
at = b2
case modeH:
// Two runs, of the current colour and then the other, neither of
// them looking at the row above.
for i := 0; i < 2; i++ {
n, err := ccittRun(b, white)
if err != nil {
return err
}
if at+n > len(curr) {
return fmt.Errorf("reader: a fax row of %d pixels named %d",
len(curr), at+n)
}
fill(curr[at:at+n], white)
at += n
white = !white
}
// Horizontal mode reads a pair, so the two flips above have put
// the colour back where it started; there is nothing to undo.
case modeV0, modeVR1, modeVR2, modeVR3, modeVL1, modeVL2, modeVL3:
a1 := ccittFindB(prev, curr, at, white, first, false) + ccittOffset(mode)
if a1 < at || a1 > len(curr) {
return fmt.Errorf("reader: a fax vertical mode named pixel %d of %d",
a1, len(curr))
}
fill(curr[at:a1], white)
at = a1
white = !white
default:
return fmt.Errorf("reader: a fax named an extension mode, which " +
"this reader does not decode")
}
first = false
}
return nil
}
// ccittOffset is how far a vertical mode moves a1 from b1.
func ccittOffset(mode int) int {
switch mode {
case modeVR1:
return 1
case modeVR2:
return 2
case modeVR3:
return 3
case modeVL1:
return -1
case modeVL2:
return -2
case modeVL3:
return -3
}
return 0
}
// ccittRun reads one run length, which is a sequence of make-up codes ending in
// a terminating code below 64.
func ccittRun(b *ccittBits, white bool) (int, error) {
table := ccittBlack
if white {
table = ccittWhite
}
total := 0
for {
n, ok := table.read(b)
if !ok {
return 0, fmt.Errorf("reader: a fax run named no length this reader knows")
}
total += n
if total > maxCCITTPixels {
return 0, fmt.Errorf("reader: a fax named a run of %d pixels", total)
}
if n <= 63 {
return total, nil
}
}
}
// ccittFindB finds b1, or b2 when second is set: the changing elements on the
// row above, as T.6 Figure 1 defines them.
func ccittFindB(prev, curr []byte, at int, white, first, second bool) int {
// The row above the first row is implicitly all white, so it has no
// changing elements and both b values are at the end of the row.
if prev == nil {
return len(curr)
}
i := at
if first {
// a0 is implicitly one pixel before the row, on white. b1 is the first
// black pixel above; b2 the first white pixel after that.
for i < len(prev) && prev[i] != 0 {
i++
}
if second {
for i < len(prev) && prev[i] == 0 {
i++
}
}
return i
}
// Walk past the run above that is of the opposite colour to the pen, then
// past the run of the pen's own colour: what follows is b1.
opposite := byte(0xFF)
if white {
opposite = 0
}
for i < len(prev) && prev[i] == opposite {
i++
}
same := ^opposite
for i < len(prev) && prev[i] == same {
i++
}
if second {
for i < len(prev) && prev[i] == opposite {
i++
}
}
return i
}
// boolParm reads a boolean decode parameter, falling back to its default.
func boolParm(parm Dict, key Name, def bool, r Resolver) bool {
if parm == nil {
return def
}
v, err := Resolve(parm.Get(key), r)
if err != nil {
return def
}
if b, ok := v.(Bool); ok {
return bool(b)
}
return def
}
// fill paints a run of pixels, one byte each.
func fill(row []byte, white bool) {
v := byte(0)
if white {
v = 0xFF
}
for i := range row {
row[i] = v
}
}
+339
View File
@@ -0,0 +1,339 @@
package reader
import (
"bytes"
"fmt"
)
// An Operation is one step of a content stream: an operator and the operands
// that precede it. An inline image is reported as the operator "BI" with its
// dictionary and data attached, since the three keywords it spans are one
// thing.
type Operation struct {
Operator string
Operands []Object
Image *InlineImage
}
// An InlineImage is the dictionary and the still-encoded bytes of a BI … ID …
// EI sequence. Its dictionary uses the abbreviated keys inline images are
// written with; [InlineImage.Expanded] gives the ordinary spelling.
type InlineImage struct {
Dict Dict
Raw []byte
}
// inlineKeys maps the abbreviations an inline image dictionary uses to the
// names their long form would have.
var inlineKeys = map[Name]Name{
"BPC": "BitsPerComponent",
"CS": "ColorSpace",
"D": "Decode",
"DP": "DecodeParms",
"F": "Filter",
"H": "Height",
"IM": "ImageMask",
"I": "Interpolate",
"W": "Width",
"L": "Length",
}
// inlineColourSpaces maps the abbreviated colour space names.
var inlineColourSpaces = map[Name]Name{
"G": "DeviceGray",
"RGB": "DeviceRGB",
"CMYK": "DeviceCMYK",
"I": "Indexed",
}
// Expanded returns the image's dictionary with the abbreviated keys and colour
// space names written out, so it can be read like any image XObject.
//
// A dictionary may say the same thing twice and disagree with itself: /W 20
// beside /Width 10, /CS /RGB beside /ColorSpace /3chanRGB. The specification
// permits both spellings in an inline image and says nothing about which wins,
// and the public implementations disagree — pdf.js asks for the abbreviation
// and falls back to the written-out name, MuPDF asks the other way round.
//
// The abbreviation wins here. Two reasons: it is the spelling Table 93 gives
// for an inline image, the written-out name being the tolerated alias; and
// safedocs' own fixture for this case marks the abbreviation as the line to
// remove "to see the effect", which is a statement about what the reference
// viewer does.
//
// What is not defensible is the answer changing between runs. Expanding in one
// pass over the map made the winner depend on Go's map iteration order, which
// is deliberately random: the same file drew differently each time it was
// opened, with no way for a caller to notice. Two passes settle it — the
// written-out names first, then the abbreviations over them — and no order
// within either pass can matter, because the names in each are distinct.
func (im *InlineImage) Expanded() Dict {
out := Dict{}
for k, v := range im.Dict {
if _, abbreviated := inlineKeys[k]; abbreviated {
continue
}
out[k] = expandColourSpace(k, v)
}
for k, v := range im.Dict {
long, abbreviated := inlineKeys[k]
if !abbreviated {
continue
}
out[long] = expandColourSpace(long, v)
}
return out
}
// expandColourSpace writes out an abbreviated colour space name, which is the
// one value that is abbreviated as well as its key.
func expandColourSpace(k Name, v Object) Object {
if k != "ColorSpace" {
return v
}
n, ok := ToName(v)
if !ok {
return v
}
if long, ok := inlineColourSpaces[n]; ok {
return long
}
return v
}
// A ContentScanner walks a content stream one operation at a time. A stream
// with rubbish in it yields the operations around the rubbish rather than
// nothing at all, which is what a renderer needs; [ContentScanner.Err] reports
// the first thing that did not parse.
type ContentScanner struct {
lex lexer
pending []Object
err error
}
// NewContentScanner reads operations from a decoded content stream.
func NewContentScanner(data []byte) *ContentScanner {
return &ContentScanner{lex: lexer{buf: data}}
}
// Err reports the first malformed token the scan stepped over, if any.
func (s *ContentScanner) Err() error { return s.err }
// Next returns the next operation, and false once the stream is exhausted.
func (s *ContentScanner) Next() (Operation, bool) {
for {
t, err := s.lex.next()
if err != nil {
s.note(err)
// Step over the byte that would not parse and carry on.
if s.lex.pos < len(s.lex.buf) {
s.lex.pos++
continue
}
return Operation{}, false
}
if t.kind == tokEOF {
return Operation{}, false
}
if t.kind != tokKeyword {
s.operand(t)
continue
}
switch string(t.text) {
case "true":
s.pending = append(s.pending, Bool(true))
continue
case "false":
s.pending = append(s.pending, Bool(false))
continue
case "null":
s.pending = append(s.pending, Null{})
continue
case "BI":
img, err := s.inlineImage()
if err != nil {
s.note(err)
return Operation{}, false
}
return s.emit("BI", img), true
}
return s.emit(string(t.text), nil), true
}
}
// operand parses one operand, an already-read token starting it.
func (s *ContentScanner) operand(t token) {
p := &parser{lex: s.lex}
o, err := p.parseFrom(t)
s.lex = p.lex
if err != nil {
s.note(err)
if s.lex.pos < len(s.lex.buf) {
s.lex.pos++
}
return
}
s.pending = append(s.pending, o)
}
// emit finishes an operation and clears the operands waiting for it.
func (s *ContentScanner) emit(op string, img *InlineImage) Operation {
out := Operation{Operator: op, Operands: s.pending, Image: img}
s.pending = nil
return out
}
// note keeps the first error the scan met.
func (s *ContentScanner) note(err error) {
if s.err == nil {
s.err = err
}
}
// inlineImage reads the dictionary after BI and the data after ID.
func (s *ContentScanner) inlineImage() (*InlineImage, error) {
d := Dict{}
for {
t, err := s.lex.next()
if err != nil {
return nil, err
}
if t.kind == tokEOF {
return nil, &SyntaxError{t.pos, "an inline image has no ID"}
}
if t.kind == tokKeyword && string(t.text) == "ID" {
break
}
if t.kind != tokName {
return nil, &SyntaxError{t.pos, "an inline image key is not a name"}
}
p := &parser{lex: s.lex}
v, err := p.parseObject()
s.lex = p.lex
if err != nil {
return nil, err
}
d[Name(t.text)] = v
}
// Exactly one white-space byte separates ID from the data.
if s.lex.pos < len(s.lex.buf) && isSpace(s.lex.buf[s.lex.pos]) {
s.lex.pos++
}
start := s.lex.pos
end, err := inlineImageEnd(s.lex.buf, start, d)
if err != nil {
return nil, err
}
raw := s.lex.buf[start:end]
s.lex.pos = end
// Step over the EI that follows.
for s.lex.pos < len(s.lex.buf) && isSpace(s.lex.buf[s.lex.pos]) {
s.lex.pos++
}
if bytes.HasPrefix(s.lex.buf[s.lex.pos:], []byte("EI")) {
s.lex.pos += 2
}
return &InlineImage{Dict: d, Raw: raw}, nil
}
// Operations tokenises a whole content stream. The error it returns describes
// what did not parse; the operations it returns are still worth having.
func Operations(data []byte) ([]Operation, error) {
s := NewContentScanner(data)
var out []Operation
for {
op, ok := s.Next()
if !ok {
return out, s.Err()
}
out = append(out, op)
}
}
// PageContent returns the decoded content stream of the i'th page, counting
// from one. A page whose /Contents is an array has its streams joined, as the
// specification requires, with a newline between them.
//
// A content stream whose filters cannot be applied contributes what could be
// salvaged from it rather than failing the page, which is what a renderer
// needs; the error is for a page that is not there at all.
// [Document.PageContentDecoded] says whether any salvaging happened.
func (d *Document) PageContent(i int) ([]byte, error) {
dec, err := d.PageContentDecoded(i)
return dec.Data, err
}
// PageContentDecoded is [Document.PageContent] with the outcome of the decode
// attached: [Decoded.Recovered] reports that at least one of the page's content
// streams could not be decoded cleanly, and [Decoded.Cause] says why.
func (d *Document) PageContentDecoded(i int) (Decoded, error) {
page, err := d.Page(i)
if err != nil {
return Decoded{}, err
}
return d.contentOf(page)
}
// contentOf decodes a page's /Contents, whether one stream or several.
func (d *Document) contentOf(page Dict) (Decoded, error) {
o, err := d.Resolve(page.Get("Contents"))
if err != nil {
return Decoded{}, err
}
switch v := o.(type) {
case *Stream:
return d.decodedContent(v), nil
case Array:
var out Decoded
for _, e := range v {
eo, err := d.Resolve(e)
if err != nil {
return Decoded{}, err
}
s, ok := ToStream(eo)
if !ok {
continue
}
part := d.decodedContent(s)
if part.Recovered && !out.Recovered {
out.Recovered, out.Cause, out.Filter = true, part.Cause, part.Filter
out.Undecoded = part.Undecoded
}
if len(part.Data) == 0 {
continue
}
if len(out.Data) > 0 {
out.Data = append(out.Data, '\n')
}
out.Data = append(out.Data, part.Data...)
}
return out, nil
}
return Decoded{}, nil
}
// decodedContent decodes one content stream. An image filter has no business
// being there, so the bytes it holds are not content: they are reported as
// salvage, not handed over as if they could be tokenised.
func (d *Document) decodedContent(s *Stream) Decoded {
dec := d.DecodeStreamRecovering(s)
if dec.Image != "" {
return Decoded{
Undecoded: dec.Data,
Recovered: true,
Filter: dec.Image,
Cause: fmt.Errorf("reader: a content stream is filtered as an image (/%s)", dec.Image),
}
}
return dec
}
// PageOperations tokenises the i'th page's content stream, counting from one.
func (d *Document) PageOperations(i int) ([]Operation, error) {
data, err := d.PageContent(i)
if err != nil {
return nil, err
}
ops, _ := Operations(data)
return ops, nil
}
+572
View File
@@ -0,0 +1,572 @@
package reader
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rc4"
"crypto/sha256"
"crypto/sha512"
"errors"
"fmt"
"hash"
)
// ErrWrongPassword is returned when a file is encrypted and neither the
// password given nor the empty one opens it.
var ErrWrongPassword = errors.New("reader: the password does not open this file")
// ErrUnsupportedEncryption is returned for a security handler this package
// does not implement — anything other than the standard one.
var ErrUnsupportedEncryption = errors.New("reader: unsupported security handler")
// pad is the 32-byte string every pre-2.0 password is padded with.
var pad = []byte{
0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56,
0xFF, 0xFA, 0x01, 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A,
}
// cryptMethod is how one class of data — strings or streams — is protected.
type cryptMethod uint8
const (
cryptNone cryptMethod = iota // /Identity: not encrypted at all
cryptRC4
cryptAESV2 // 128-bit AES, per-object key
cryptAESV3 // 256-bit AES, the file key used directly
)
// A decryptor holds the file encryption key and how to apply it.
type decryptor struct {
key []byte
strings cryptMethod
streams cryptMethod
revision int
perm Permissions
owner bool // the password given was the owner's, not the user's
skipObj int // the /Encrypt dictionary's own object number, never decrypted
skipKnown bool // whether skipObj is meaningful
}
// A Protection is what a file's security handler says about it: how it is
// encrypted, and what a reader that opened it with the user password may do.
type Protection struct {
// Method names the algorithm the way a person would say it: "RC4-40",
// "RC4-128", "AES-128", "AES-256", or "none" for a file that declares an
// /Encrypt dictionary and then encrypts nothing with it.
Method string
// Revision is the standard security handler's revision: 2, 3, 4, 5 or 6.
Revision int
// Permissions is what the file grants whoever opened it with the user
// password.
Permissions Permissions
// Owner is true when the password the file was opened with was the
// owner's, in which case the permissions do not apply to this reader.
Owner bool
}
// Protection reports how the file is protected, and false when it is not
// protected at all. A document that opened has already been decrypted; this
// says what it said about itself on the way.
func (d *Document) Protection() (Protection, bool) {
if d.decrypt == nil {
return Protection{}, false
}
return Protection{
Method: d.decrypt.methodName(),
Revision: d.decrypt.revision,
Permissions: d.decrypt.perm,
Owner: d.decrypt.owner,
}, true
}
// methodName says which algorithm protects the streams, which is the one that
// matters: it is where the content is.
func (dec *decryptor) methodName() string {
switch dec.streams {
case cryptAESV3:
return "AES-256"
case cryptAESV2:
return "AES-128"
case cryptRC4:
return fmt.Sprintf("RC4-%d", len(dec.key)*8)
}
return "none"
}
// Encrypted reports whether the file declares an /Encrypt dictionary.
func (d *Document) Encrypted() bool {
return d.trailer != nil && d.trailer.Get("Encrypt").Kind() != KindNull
}
// setUpDecryption reads /Encrypt and derives the file key. It is called once,
// before any string or stream is handed out.
func (d *Document) setUpDecryption(password string) error {
if !d.Encrypted() {
return nil
}
if ref, ok := d.trailer.Get("Encrypt").(Ref); ok {
d.encryptNum, d.encryptKnown = ref.Num, true
}
o, err := d.Resolve(d.trailer.Get("Encrypt"))
if err != nil {
return err
}
enc, ok := ToDict(o)
if !ok {
return fmt.Errorf("reader: /Encrypt is a %s, not a dictionary", o.Kind())
}
id := d.firstID()
dec, err := newDecryptor(enc, id, password, d.Get)
if err != nil {
return err
}
dec.skipObj, dec.skipKnown = d.encryptNum, d.encryptKnown
d.decrypt = dec
return nil
}
// firstID returns the first element of the trailer's /ID, which takes part in
// the pre-2.0 key derivation. A file without one contributes nothing.
func (d *Document) firstID() []byte {
arr, ok := ToArray(d.trailer.Get("ID"))
if !ok || len(arr) == 0 {
return nil
}
s, ok := ToString(arr[0])
if !ok {
return nil
}
return s
}
// newDecryptor derives the file encryption key from a password.
func newDecryptor(enc Dict, id []byte, password string, r Resolver) (*decryptor, error) {
if f, ok := ToName(resolved(enc, "Filter", r)); ok && f != "Standard" {
return nil, ErrUnsupportedEncryption
}
v := int(intOf(resolved(enc, "V", r), 0))
rev := int(intOf(resolved(enc, "R", r), 0))
if v == 0 || rev == 0 {
return nil, fmt.Errorf("reader: /Encrypt has no usable /V and /R")
}
length := int(intOf(resolved(enc, "Length", r), 40))
if length < 40 || length > 256 || length%8 != 0 {
length = 40
}
dec := &decryptor{revision: rev}
if err := dec.readMethods(enc, v, r); err != nil {
return nil, err
}
owner, _ := ToString(resolved(enc, "O", r))
user, _ := ToString(resolved(enc, "U", r))
perm := int32(intOf(resolved(enc, "P", r), 0))
metadata := true
if b, ok := ToBool(resolved(enc, "EncryptMetadata", r)); ok {
metadata = b
}
dec.perm = Permissions(uint32(perm)) & AllPermissions
if dec.streams == cryptNone && dec.strings == cryptNone {
// /StmF and /StrF both name the identity crypt filter, so nothing in
// the document body is encrypted and no key is needed to read it. A
// file like that is opened without validating the password: refusing
// it would refuse a document whose every byte is already plain, which
// is what every other reader shows. Only /EFF — the embedded files —
// would need the key, and this reader does not hand those out.
//
// Nothing was authenticated, so dec.owner stays false and Protection
// reports the method as "none": a caller can see exactly what it got.
return dec, nil
}
if rev >= 5 {
key, asOwner, err := deriveKeyR5(enc, password, r)
if err != nil {
return nil, err
}
dec.key, dec.owner = key, asOwner
return dec, nil
}
n := length / 8
if rev == 2 {
n = 5
}
key, asOwner, err := deriveKeyLegacy(password, owner, user, id, perm, n, rev, metadata)
if err != nil {
return nil, err
}
dec.key, dec.owner = key, asOwner
return dec, nil
}
// readMethods works out how strings and streams are protected. Before /V 4
// there is one method for everything; from /V 4 the crypt filters in /CF are
// named by /StmF and /StrF.
func (dec *decryptor) readMethods(enc Dict, v int, r Resolver) error {
if v < 4 {
dec.strings, dec.streams = cryptRC4, cryptRC4
return nil
}
cf, _ := ToDict(resolved(enc, "CF", r))
pick := func(key Name) (cryptMethod, error) {
name, ok := ToName(resolved(enc, key, r))
if !ok || name == "Identity" {
return cryptNone, nil
}
f, ok := ToDict(resolved(cf, name, r))
if !ok {
return cryptNone, nil
}
switch m, _ := ToName(resolved(f, "CFM", r)); m {
case "V2":
return cryptRC4, nil
case "AESV2":
return cryptAESV2, nil
case "AESV3":
return cryptAESV3, nil
case "None":
return cryptNone, nil
default:
return cryptNone, fmt.Errorf("reader: unsupported crypt filter method /%s", m)
}
}
var err error
if dec.streams, err = pick("StmF"); err != nil {
return err
}
dec.strings, err = pick("StrF")
return err
}
// deriveKeyLegacy is the pre-2.0 key derivation, trying the password as the
// user password and then as the owner password.
func deriveKeyLegacy(password string, owner, user, id []byte, perm int32, n, rev int, metadata bool) (key []byte, asOwner bool, err error) {
for _, candidate := range legacyCandidates(password, owner, n, rev) {
k := legacyFileKey(candidate.padded, owner, id, perm, n, rev, metadata)
if legacyUserKeyMatches(k, user, id, rev) {
return k, candidate.owner, nil
}
}
return nil, false, ErrWrongPassword
}
// A legacyCandidate is one padded password to try, and whether reaching it
// meant knowing the owner's rather than the user's.
type legacyCandidate struct {
padded []byte
owner bool
}
// legacyCandidates lists the padded passwords worth trying: the one given, the
// empty one, and the user password the owner password unlocks.
func legacyCandidates(password string, owner []byte, n, rev int) []legacyCandidate {
out := []legacyCandidate{{padded: padPassword([]byte(password))}}
if password != "" {
out = append(out, legacyCandidate{padded: padPassword(nil)})
}
if u := userFromOwner([]byte(password), owner, n, rev); u != nil {
out = append(out, legacyCandidate{padded: u, owner: true})
}
return out
}
// padPassword truncates or pads a password to the 32 bytes the algorithm wants.
func padPassword(p []byte) []byte {
out := make([]byte, 32)
k := copy(out, p)
copy(out[k:], pad)
return out
}
// legacyFileKey is algorithm 2: the file encryption key from a padded password.
func legacyFileKey(padded, owner, id []byte, perm int32, n, rev int, metadata bool) []byte {
h := md5.New()
h.Write(padded)
h.Write(owner)
h.Write([]byte{byte(perm), byte(perm >> 8), byte(perm >> 16), byte(perm >> 24)})
h.Write(id)
if rev >= 4 && !metadata {
h.Write([]byte{0xFF, 0xFF, 0xFF, 0xFF})
}
key := h.Sum(nil)
if rev >= 3 {
for i := 0; i < 50; i++ {
sum := md5.Sum(key[:n])
key = sum[:]
}
}
return key[:n]
}
// legacyUserKeyMatches is algorithm 6: does this key reproduce /U?
func legacyUserKeyMatches(key, user, id []byte, rev int) bool {
if rev == 2 {
return bytes.Equal(rc4Bytes(key, pad), user)
}
h := md5.New()
h.Write(pad)
h.Write(id)
x := rc4Bytes(key, h.Sum(nil))
for i := 1; i <= 19; i++ {
x = rc4Bytes(xorKey(key, i), x)
}
return len(user) >= 16 && bytes.Equal(x, user[:16])
}
// userFromOwner is algorithm 7: recover the padded user password from /O using
// the owner password.
func userFromOwner(password, owner []byte, n, rev int) []byte {
if len(owner) < 32 {
return nil
}
sum := md5.Sum(padPassword(password))
key := sum[:]
if rev >= 3 {
for i := 0; i < 50; i++ {
s := md5.Sum(key)
key = s[:]
}
}
key = key[:n]
if rev == 2 {
return rc4Bytes(key, owner[:32])
}
x := owner[:32]
for i := 19; i >= 0; i-- {
x = rc4Bytes(xorKey(key, i), x)
}
return x
}
// xorKey returns key with every byte exclusive-ored with v, which is how both
// password algorithms walk their nineteen rounds.
func xorKey(key []byte, v int) []byte {
out := make([]byte, len(key))
for i := range key {
out[i] = key[i] ^ byte(v)
}
return out
}
// deriveKeyR5 is the PDF 2.0 derivation, /R 5 and /R 6: the password is
// validated against a salted hash and then unwraps the file key.
func deriveKeyR5(enc Dict, password string, r Resolver) (key []byte, asOwner bool, err error) {
user, _ := ToString(resolved(enc, "U", r))
userE, _ := ToString(resolved(enc, "UE", r))
owner, _ := ToString(resolved(enc, "O", r))
ownerE, _ := ToString(resolved(enc, "OE", r))
rev := int(intOf(resolved(enc, "R", r), 6))
if len(user) < 48 {
return nil, false, fmt.Errorf("reader: /U is %d bytes, not the 48 this revision needs", len(user))
}
pw := []byte(password)
for _, candidate := range [][]byte{pw, nil} {
if candidate == nil && password == "" {
break
}
if key := unlockR5(candidate, user, userE, nil, rev); key != nil {
return key, false, nil
}
if len(owner) >= 48 {
if key := unlockR5(candidate, owner, ownerE, user[:48], rev); key != nil {
return key, true, nil
}
}
}
return nil, false, ErrWrongPassword
}
// unlockR5 checks one password against a 48-byte /U or /O entry and, when it
// matches, unwraps the file key from the matching /UE or /OE.
func unlockR5(password, entry, wrapped, udata []byte, rev int) []byte {
valSalt, keySalt := entry[32:40], entry[40:48]
if !bytes.Equal(hash2B(password, valSalt, udata, rev), entry[:32]) {
return nil
}
if len(wrapped) < 32 {
return nil
}
inter := hash2B(password, keySalt, udata, rev)
// inter is always the 32 bytes hash2B returns, so the cipher cannot refuse it.
block, _ := aes.NewCipher(inter)
out := make([]byte, 32)
cipher.NewCBCDecrypter(block, make([]byte, aes.BlockSize)).CryptBlocks(out, wrapped[:32])
return out
}
// hash2B is algorithm 2.B: SHA-256 for revision 5, and for revision 6 the
// hardening loop that repeatedly encrypts and re-hashes.
func hash2B(password, salt, udata []byte, rev int) []byte {
h := sha256.New()
h.Write(password)
h.Write(salt)
h.Write(udata)
k := h.Sum(nil)
if rev < 6 {
return k
}
for round := 0; ; round++ {
var k1 []byte
one := append(append(append([]byte{}, password...), k...), udata...)
for i := 0; i < 64; i++ {
k1 = append(k1, one...)
}
// k is always 32 bytes, so a 16-byte key is always well formed.
block, _ := aes.NewCipher(k[:16])
e := make([]byte, len(k1)-len(k1)%aes.BlockSize)
cipher.NewCBCEncrypter(block, k[16:32]).CryptBlocks(e, k1[:len(e)])
sum := 0
for _, b := range e[:16] {
sum += int(b)
}
var next hash.Hash
switch sum % 3 {
case 0:
next = sha256.New()
case 1:
next = sha512.New384()
default:
next = sha512.New()
}
next.Write(e)
k = next.Sum(nil)
if round >= 63 && int(e[len(e)-1]) <= round-31 {
break
}
}
return k[:32]
}
// objectKey is the per-object key the pre-2.0 methods use; AESV3 uses the file
// key unchanged.
func (dec *decryptor) objectKey(num, gen int, method cryptMethod) []byte {
if method == cryptAESV3 {
return dec.key
}
h := md5.New()
h.Write(dec.key)
h.Write([]byte{byte(num), byte(num >> 8), byte(num >> 16), byte(gen), byte(gen >> 8)})
if method == cryptAESV2 {
h.Write([]byte{0x73, 0x41, 0x6C, 0x54}) // "sAlT"
}
k := h.Sum(nil)
if n := len(dec.key) + 5; n < 16 {
return k[:n]
}
return k
}
// decryptBytes undoes one method over one object's data.
func (dec *decryptor) decryptBytes(num, gen int, method cryptMethod, data []byte) []byte {
switch method {
case cryptNone:
return data
case cryptRC4:
return rc4Bytes(dec.objectKey(num, gen, method), data)
default:
return aesDecrypt(dec.objectKey(num, gen, method), data)
}
}
// rc4Bytes applies RC4, which is its own inverse. A key length the cipher
// refuses yields nil, which every caller reads as a password that does not
// fit — the derivations here never produce one.
func rc4Bytes(key, data []byte) []byte {
c, err := rc4.NewCipher(key)
if err != nil {
return nil
}
out := make([]byte, len(data))
c.XORKeyStream(out, data)
return out
}
// aesDecrypt undoes AES in CBC mode, the initialisation vector being the first
// block of the data and the padding the one CBC calls for.
func aesDecrypt(key, data []byte) []byte {
if len(data) < 2*aes.BlockSize || len(data)%aes.BlockSize != 0 {
return nil
}
block, err := aes.NewCipher(key)
if err != nil {
return nil
}
out := make([]byte, len(data)-aes.BlockSize)
cipher.NewCBCDecrypter(block, data[:aes.BlockSize]).CryptBlocks(out, data[aes.BlockSize:])
n := int(out[len(out)-1])
if n < 1 || n > aes.BlockSize || n > len(out) {
return out
}
return out[:len(out)-n]
}
// decryptObject walks an object, decrypting every string in it and, for a
// stream, its data. The /Encrypt dictionary itself, and any cross-reference
// stream, are left alone: neither is encrypted.
func (dec *decryptor) decryptObject(num, gen int, o Object) Object {
if dec.skipKnown && num == dec.skipObj {
return o
}
return dec.walk(num, gen, o)
}
// walk is decryptObject's recursion.
func (dec *decryptor) walk(num, gen int, o Object) Object {
switch v := o.(type) {
case String:
return String(dec.decryptBytes(num, gen, dec.strings, v))
case Array:
for i, e := range v {
v[i] = dec.walk(num, gen, e)
}
return v
case Dict:
for k, e := range v {
v[k] = dec.walk(num, gen, e)
}
return v
case *Stream:
if t, ok := ToName(v.Dict.Get("Type")); ok && t == "XRef" {
return v
}
v.Dict, _ = ToDict(dec.walk(num, gen, v.Dict))
if streamIsPlain(v.Dict) {
return v
}
v.Raw = dec.decryptBytes(num, gen, dec.streams, v.Raw)
return v
}
return o
}
// streamIsPlain reports whether a stream's own filter chain says its bytes were
// left unencrypted: a leading /Crypt filter naming /Identity, which is what
// /Crypt with no /Name means too. Producers use it for the metadata stream of a
// file whose /EncryptMetadata is false, and decrypting such a stream turns
// readable XML into noise.
func streamIsPlain(d Dict) bool {
if first, ok := firstFilter(d); !ok || first != "Crypt" {
return false
}
name, ok := ToName(firstDecodeParms(d).Get("Name"))
return !ok || name == "Identity"
}
// resolved is a helper for reading an /Encrypt entry that may be indirect.
func resolved(d Dict, key Name, r Resolver) Object {
o, err := Resolve(d.Get(key), r)
if err != nil {
return Null{}
}
return o
}
// intOf reads an integer with a default.
func intOf(o Object, def int64) int64 {
if n, ok := ToInt(o); ok {
return n
}
return def
}
+431
View File
@@ -0,0 +1,431 @@
package reader
import (
"bytes"
"fmt"
)
// maxPageTreeNodes bounds a page tree walk. /Count is a claim, not a fact, and
// /Kids can be made to point back at an ancestor.
const maxPageTreeNodes = 1 << 20
// inheritable lists the page attributes a node may take from an ancestor.
var inheritable = []Name{"Resources", "MediaBox", "CropBox", "Rotate"}
// A Document is a parsed PDF file: its cross-reference information, its
// trailer, and lazy access to every object in it.
type Document struct {
buf []byte
xref map[int]xrefEntry
trailer Dict
cache map[int]Object
loading map[int]bool
objStms map[int]map[int]Object
pages []Ref
password string
decrypt *decryptor
encryptNum int
encryptKnown bool
repaired bool
}
// Open parses the cross-reference information of a PDF file held in memory,
// using the empty password. A file whose tables are damaged is rebuilt by
// scanning it, so Open fails only when there is no usable document structure
// at all, or when the file is encrypted and the empty password does not open
// it — [OpenWithPassword] then does.
func Open(b []byte) (*Document, error) { return OpenWithPassword(b, "") }
// OpenWithPassword is Open with a password to try. The password is tried both
// as the user password and as the owner password, the empty one as well.
func OpenWithPassword(b []byte, password string) (*Document, error) {
d := &Document{
buf: b,
xref: map[int]xrefEntry{},
cache: map[int]Object{},
loading: map[int]bool{},
objStms: map[int]map[int]Object{},
password: password,
}
err := d.loadXref()
if err == nil {
if derr := d.setUpDecryption(password); derr != nil {
return nil, derr
}
d.cache = map[int]Object{}
cerr := error(nil)
if _, cerr = d.Catalog(); cerr == nil {
return d, nil
}
// Tables that parse but lead nowhere are worse than none: rebuild.
// What the catalogue lookup actually said is kept: "no page tree" and
// "/Root is a null" send a reader to different places.
err = fmt.Errorf("reader: the cross-reference tables do not lead to a catalogue: %w", cerr)
}
if rerr := d.repair(); rerr != nil {
// Both diagnoses matter, and the rebuild's is the one about the file
// as it really is: it has read every object header in it, where the
// tables only failed to be read. Returning the tables' error alone
// reported "no startxref" for a file that turned out to hold no
// objects at all, or an encrypted one whose catalogue could not be
// reached — a different problem with a different answer.
return nil, fmt.Errorf("%w (the cross-reference information was unusable too: %v)", rerr, err)
}
// Setting the key up again here used to be necessary, because the rebuild
// ran before anything knew the file was encrypted. repair() establishes it
// itself now, so by this point it is either known or not needed.
return d, nil
}
// Trailer returns the file's trailer dictionary, the newest value of every key
// across the chain of cross-reference sections.
func (d *Document) Trailer() Dict { return d.trailer }
// Repaired reports whether the document's structure was rebuilt by scanning
// the file rather than read from its cross-reference tables.
func (d *Document) Repaired() bool { return d.repaired }
// Resolver returns the function that turns this document's indirect references
// into objects, for [Resolve] and [Decode].
func (d *Document) Resolver() Resolver { return d.Get }
// Resolve follows indirect references until it reaches a direct object.
func (d *Document) Resolve(o Object) (Object, error) { return Resolve(o, d.Get) }
// GetDict resolves an entry of a dictionary to a dictionary.
func (d *Document) GetDict(from Dict, key Name) (Dict, bool) {
o, err := d.Resolve(from.Get(key))
if err != nil {
return nil, false
}
return ToDict(o)
}
// Get returns the object an indirect reference names, or [Null] when the file
// does not define it.
func (d *Document) Get(ref Ref) (Object, error) {
if o, ok := d.cache[ref.Num]; ok {
return o, nil
}
e, ok := d.xref[ref.Num]
if !ok || e.kind == 'f' {
return Null{}, nil
}
if d.loading[ref.Num] {
// An object stream whose own /Length or /Filter points back at an
// object inside itself would otherwise recurse forever.
return Null{}, nil
}
d.loading[ref.Num] = true
defer delete(d.loading, ref.Num)
var obj Object
var err error
if e.kind == 'o' {
obj, err = d.getFromObjectStream(ref.Num, e)
} else {
obj, err = d.getAtOffset(ref.Num, e)
}
if err != nil {
return nil, err
}
d.cache[ref.Num] = obj
return obj, nil
}
// getAtOffset reads an object written directly in the file. An offset that
// does not hold the expected object number means the tables are wrong, which
// is common enough that it triggers a rebuild rather than an error.
func (d *Document) getAtOffset(num int, e xrefEntry) (Object, error) {
if e.offset < 0 || e.offset >= int64(len(d.buf)) {
return d.retryAfterRepair(num, fmt.Errorf("reader: object %d is at offset %d, outside the file", num, e.offset))
}
got, obj, _, err := ParseIndirectObject(d.buf[e.offset:], d.Get)
if err != nil {
return d.retryAfterRepair(num, err)
}
if got.Num != num {
return d.retryAfterRepair(num, fmt.Errorf("reader: offset %d holds object %d, not %d", e.offset, got.Num, num))
}
if d.decrypt != nil {
obj = d.decrypt.decryptObject(num, got.Gen, obj)
}
return obj, nil
}
// retryAfterRepair rebuilds the tables once and looks the object up again.
func (d *Document) retryAfterRepair(num int, cause error) (Object, error) {
if d.repaired {
return Null{}, nil
}
if err := d.repair(); err != nil {
return nil, cause
}
e, ok := d.xref[num]
if !ok || e.kind != 'n' || e.offset >= int64(len(d.buf)) {
return Null{}, nil
}
got, obj, _, err := ParseIndirectObject(d.buf[e.offset:], d.Get)
if err != nil || got.Num != num {
return Null{}, nil
}
if d.decrypt != nil {
obj = d.decrypt.decryptObject(num, got.Gen, obj)
}
return obj, nil
}
// getFromObjectStream reads an object held inside a /Type /ObjStm stream.
func (d *Document) getFromObjectStream(num int, e xrefEntry) (Object, error) {
objs, err := d.objectStream(e.strmNum)
if err != nil {
return nil, err
}
if o, ok := objs[num]; ok {
return o, nil
}
// The index is a hint; a stream that does not hold the object simply
// does not define it.
return Null{}, nil
}
// objectStream parses an object stream once and keeps its contents.
func (d *Document) objectStream(num int) (map[int]Object, error) {
if objs, ok := d.objStms[num]; ok {
return objs, nil
}
objs := map[int]Object{}
d.objStms[num] = objs
o, err := d.Get(Ref{Num: num})
if err != nil {
return objs, err
}
s, ok := ToStream(o)
if !ok {
return objs, nil
}
// An object stream that stops in the middle still holds whole objects in
// the part that did decode, and losing them loses pages.
dec := d.DecodeStreamRecovering(s)
if dec.Image != "" {
return objs, nil
}
data := dec.Data
n := int(intOr(s.Dict.Get("N"), 0))
first := int(intOr(s.Dict.Get("First"), 0))
if n <= 0 || first <= 0 || first > len(data) {
return objs, nil
}
l := &lexer{buf: data[:first]}
type pair struct{ num, off int }
pairs := make([]pair, 0, n)
for i := 0; i < n; i++ {
t1, err1 := l.next()
t2, err2 := l.next()
if err1 != nil || err2 != nil || t1.kind != tokInteger || t2.kind != tokInteger {
break
}
pairs = append(pairs, pair{int(t1.i), first + int(t2.i)})
}
for _, p := range pairs {
if p.off < 0 || p.off > len(data) {
continue
}
obj, _, err := ParseObject(data[p.off:])
if err != nil {
continue
}
objs[p.num] = obj
}
return objs, nil
}
// intOr resolves nothing; it reads a direct integer or returns a default.
func intOr(o Object, def int64) int64 {
if n, ok := ToInt(o); ok {
return n
}
return def
}
// DecodeStream applies a stream's filter chain, resolving any indirect decode
// parameters against this document. A chain that cannot be run to the end is
// an error; [Document.DecodeStreamRecovering] salvages instead.
func (d *Document) DecodeStream(s *Stream) ([]byte, Name, error) {
return Decode(s.Dict, s.Raw, d.Get)
}
// DecodeStreamRecovering applies a stream's filter chain, salvaging what it can
// from a chain that cannot be run to the end and saying that it did so.
func (d *Document) DecodeStreamRecovering(s *Stream) Decoded {
return DecodeRecovering(s.Dict, s.Raw, d.Get)
}
// Catalog returns the document catalogue the trailer's /Root names.
func (d *Document) Catalog() (Dict, error) {
if d.trailer == nil {
return nil, fmt.Errorf("reader: the file has no trailer")
}
o, err := d.Resolve(d.trailer.Get("Root"))
if err != nil {
return nil, err
}
cat, ok := ToDict(o)
if !ok {
return nil, fmt.Errorf("reader: /Root is a %s, not a dictionary", o.Kind())
}
if _, ok := d.GetDict(cat, "Pages"); !ok {
return nil, fmt.Errorf("reader: the catalogue has no page tree")
}
return cat, nil
}
// PageCount reports how many pages the document has.
func (d *Document) PageCount() int { return len(d.pageRefs()) }
// PageRef returns the reference of the i'th page, counting from one.
func (d *Document) PageRef(i int) (Ref, bool) {
refs := d.pageRefs()
if i < 1 || i > len(refs) {
return Ref{}, false
}
return refs[i-1], true
}
// Page returns the i'th page's dictionary, counting from one, with the
// attributes it inherits from its ancestors filled in.
func (d *Document) Page(i int) (Dict, error) {
ref, ok := d.PageRef(i)
if !ok {
return nil, fmt.Errorf("reader: page %d is out of range (the document has %d)", i, d.PageCount())
}
o, err := d.Get(ref)
if err != nil {
return nil, err
}
page, ok := ToDict(o)
if !ok {
return nil, fmt.Errorf("reader: page %d is a %s, not a dictionary", i, o.Kind())
}
return d.withInherited(page), nil
}
// withInherited copies the page and fills in what its ancestors provide.
func (d *Document) withInherited(page Dict) Dict {
out := Dict{}
for k, v := range page {
out[k] = v
}
node := page
for depth := 0; depth < maxRefDepth; depth++ {
missing := false
for _, k := range inheritable {
if _, ok := out[k]; !ok {
missing = true
}
}
if !missing {
break
}
parent, ok := d.GetDict(node, "Parent")
if !ok {
break
}
for _, k := range inheritable {
if _, ok := out[k]; ok {
continue
}
if v, ok := parent[k]; ok {
out[k] = v
}
}
node = parent
}
return out
}
// pageRefs walks the page tree once, in document order.
func (d *Document) pageRefs() []Ref {
if d.pages != nil {
return d.pages
}
d.pages = []Ref{}
cat, err := d.Catalog()
if err != nil {
return d.pages
}
root := cat["Pages"]
seen := map[Ref]bool{}
d.walkPages(root, seen, 0)
if len(d.pages) == 0 {
// A tree that yields nothing but a catalogue that names one page node
// is still a one-page document.
d.pages = d.scanForPages()
}
return d.pages
}
// walkPages appends the leaves of a page tree node in order.
func (d *Document) walkPages(node Object, seen map[Ref]bool, depth int) {
if depth > 64 || len(d.pages) >= maxPageTreeNodes {
return
}
ref, isRef := node.(Ref)
if isRef {
if seen[ref] {
return
}
seen[ref] = true
}
o, err := d.Resolve(node)
if err != nil {
return
}
dict, ok := ToDict(o)
if !ok {
return
}
kids, hasKids := d.kidsOf(dict)
if !hasKids {
if isRef {
d.pages = append(d.pages, ref)
}
return
}
for _, kid := range kids {
d.walkPages(kid, seen, depth+1)
}
}
// kidsOf reports a node's /Kids when it is an interior node of the page tree.
func (d *Document) kidsOf(node Dict) (Array, bool) {
if t, ok := ToName(node.Get("Type")); ok && t == "Page" {
return nil, false
}
o, err := d.Resolve(node.Get("Kids"))
if err != nil {
return nil, false
}
kids, ok := ToArray(o)
return kids, ok
}
// Version reports the PDF version the header declares, "1.7" and the like.
func (d *Document) Version() string {
i := bytes.Index(d.buf, []byte("%PDF-"))
if i < 0 || i+8 > len(d.buf) {
return ""
}
return string(bytes.TrimRight(d.buf[i+5:i+8], "\r\n \t"))
}
// scanForPages is the last resort for a page tree that resolves to nothing:
// every object in the file that calls itself a page, in object-number order.
func (d *Document) scanForPages() []Ref {
var out []Ref
for _, num := range d.objectsOfType("Page") {
out = append(out, Ref{Num: num})
}
return out
}
+365
View File
@@ -0,0 +1,365 @@
package reader
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"fmt"
"strings"
)
// Permissions say what a reader may do with a file it can open with the user
// password. They are advisory — nothing enforces them but the reader's own
// good manners — and a file opened with the owner password ignores them
// entirely.
type Permissions uint32
// The permissions a standard security handler can express. Anything not
// granted is refused; [AllPermissions] grants everything.
const (
PermPrint Permissions = 1 << 2
PermModify Permissions = 1 << 3
PermCopy Permissions = 1 << 4
PermAnnotate Permissions = 1 << 5
PermFillForms Permissions = 1 << 8
PermExtract Permissions = 1 << 9 // for accessibility
PermAssemble Permissions = 1 << 10
PermPrintFaithful Permissions = 1 << 11 // at full resolution
// AllPermissions grants every one of them.
AllPermissions = PermPrint | PermModify | PermCopy | PermAnnotate |
PermFillForms | PermExtract | PermAssemble | PermPrintFaithful
)
// permissionNames pairs each permission with what it lets a reader do, in the
// order a person would want to read them.
var permissionNames = []struct {
bit Permissions
name string
}{
{PermPrint, "print"},
{PermPrintFaithful, "print at full resolution"},
{PermModify, "modify"},
{PermAssemble, "assemble"},
{PermCopy, "copy"},
{PermExtract, "extract for accessibility"},
{PermAnnotate, "annotate"},
{PermFillForms, "fill in forms"},
}
// Allows reports whether every one of the given permissions is granted.
func (p Permissions) Allows(want Permissions) bool { return p&want == want }
// String lists what is granted, in words.
func (p Permissions) String() string {
var out []string
for _, e := range permissionNames {
if p.Allows(e.bit) {
out = append(out, e.name)
}
}
if len(out) == 0 {
return "nothing"
}
return strings.Join(out, ", ")
}
// permissionBase is the pattern of reserved bits the specification requires
// around the permissions themselves.
const permissionBase uint32 = 0xFFFFF0C0
// An Encryption says how a file is to be protected.
//
// Two people can open it: whoever knows the user password, subject to the
// permissions, and whoever knows the owner password, subject to nothing. An
// empty user password means the file opens without one and the permissions are
// all it says.
type Encryption struct {
UserPassword string
OwnerPassword string
Permissions Permissions
// AES128 asks for the older method, which readers before 2008 understand.
// The default is AES-256, which is what a file should use today.
AES128 bool
}
// Encrypt protects everything written from here on. It must be called before
// anything is written, and the file it produces is not byte-for-byte
// reproducible: encryption needs randomness, by design.
func (w *Writer) Encrypt(e Encryption) {
if len(w.offsets) > 0 || len(w.pending) > 0 {
w.note(fmt.Errorf("reader: a file cannot be encrypted after it has been written to"))
return
}
enc, err := newEncrypter(e)
if err != nil {
w.note(err)
return
}
w.encrypt = enc
}
// An encrypter holds the file key and the dictionary that describes it.
type encrypter struct {
key []byte
method cryptMethod
dict Dict
id []byte
err error
// number is the object the /Encrypt dictionary is written under, which is
// the one thing in the file that is never encrypted.
number int
}
// newEncrypter derives a file key and builds the dictionary that lets a reader
// derive it again from a password.
func newEncrypter(e Encryption) (*encrypter, error) {
perm := permissionBase | uint32(e.Permissions)
id, err := randomBytes(16)
if err != nil {
return nil, err
}
if e.AES128 {
return newLegacyEncrypter(e, perm, id)
}
return newAES256Encrypter(e, perm, id)
}
// newAES256Encrypter builds the revision 6 form: a random file key, wrapped
// twice, once for each password.
func newAES256Encrypter(e Encryption, perm uint32, id []byte) (*encrypter, error) {
key, err := randomBytes(32)
if err != nil {
return nil, err
}
// An owner password nobody set is the user's: leaving it empty would
// mean anything at all opens the file with every permission, which is
// the opposite of what asking for encryption means.
ownerPassword := e.OwnerPassword
if ownerPassword == "" {
ownerPassword = e.UserPassword
}
user, userE, err := wrapKey([]byte(e.UserPassword), key, nil)
if err != nil {
return nil, err
}
owner, ownerE, err := wrapKey([]byte(ownerPassword), key, user)
if err != nil {
return nil, err
}
perms, err := permsEntry(key, perm)
if err != nil {
return nil, err
}
return &encrypter{
key: key,
method: cryptAESV3,
id: id,
dict: Dict{
"Filter": Name("Standard"),
"V": Integer(5),
"R": Integer(6),
"Length": Integer(256),
"P": Integer(int32(perm)),
"U": String(user),
"UE": String(userE),
"O": String(owner),
"OE": String(ownerE),
"Perms": String(perms),
"CF": Dict{"StdCF": Dict{"CFM": Name("AESV3"), "Length": Integer(32)}},
"StmF": Name("StdCF"),
"StrF": Name("StdCF"),
"EncryptMetadata": Bool(true),
},
}, nil
}
// wrapKey builds one 48-byte password entry and the 32 bytes that hold the
// file key wrapped for it.
func wrapKey(password, key, udata []byte) (entry, wrapped []byte, err error) {
salts, err := randomBytes(16)
if err != nil {
return nil, nil, err
}
validation, keySalt := salts[:8], salts[8:]
entry = append(append(hash2B(password, validation, udata, 6), validation...), keySalt...)
// hash2B always returns 32 bytes, so the cipher cannot refuse the key.
block, _ := aes.NewCipher(hash2B(password, keySalt, udata, 6))
wrapped = make([]byte, 32)
cipher.NewCBCEncrypter(block, make([]byte, aes.BlockSize)).CryptBlocks(wrapped, key)
return entry, wrapped, nil
}
// permsEntry is the block that lets a reader check the permissions have not
// been tampered with: they are encrypted with the file key itself.
func permsEntry(key []byte, perm uint32) ([]byte, error) {
tail, err := randomBytes(4)
if err != nil {
return nil, err
}
plain := []byte{
byte(perm), byte(perm >> 8), byte(perm >> 16), byte(perm >> 24),
0xFF, 0xFF, 0xFF, 0xFF,
'T', // metadata is encrypted too
'a', 'd', 'b', // the specification's own marker
tail[0], tail[1], tail[2], tail[3],
}
// The file key is 32 bytes by construction.
block, _ := aes.NewCipher(key)
out := make([]byte, 16)
// One block, with no chaining: this is the one place the format uses it.
block.Encrypt(out, plain)
return out, nil
}
// newLegacyEncrypter builds the revision 4 form, whose key comes from the
// passwords rather than the other way round.
func newLegacyEncrypter(e Encryption, perm uint32, id []byte) (*encrypter, error) {
const n = 16 // 128 bits
owner := legacyOwnerValue([]byte(e.OwnerPassword), []byte(e.UserPassword), n)
key := legacyFileKey(padPassword([]byte(e.UserPassword)), owner, id, int32(perm), n, 4, true)
user := legacyUserValue(key, id)
return &encrypter{
key: key,
method: cryptAESV2,
id: id,
dict: Dict{
"Filter": Name("Standard"),
"V": Integer(4),
"R": Integer(4),
"Length": Integer(128),
"P": Integer(int32(perm)),
"O": String(owner),
"U": String(user),
"CF": Dict{"StdCF": Dict{"CFM": Name("AESV2"), "Length": Integer(16)}},
"StmF": Name("StdCF"),
"StrF": Name("StdCF"),
"EncryptMetadata": Bool(true),
},
}, nil
}
// legacyOwnerValue is algorithm 3: the /O entry, which holds the user password
// encrypted under the owner's.
func legacyOwnerValue(ownerPassword, userPassword []byte, n int) []byte {
if len(ownerPassword) == 0 {
ownerPassword = userPassword
}
sum := md5Sum(padPassword(ownerPassword))
key := sum
for i := 0; i < 50; i++ {
key = md5Sum(key)
}
key = key[:n]
x := padPassword(userPassword)
for i := 0; i <= 19; i++ {
x = rc4Bytes(xorKey(key, i), x)
}
return x
}
// legacyUserValue is algorithm 5: the /U entry.
func legacyUserValue(key, id []byte) []byte {
x := rc4Bytes(key, md5Sum(append(append([]byte{}, pad...), id...)))
for i := 1; i <= 19; i++ {
x = rc4Bytes(xorKey(key, i), x)
}
return append(x, make([]byte, 16)...)
}
// randomSource is where randomness comes from. It is a variable so a test
// can take it away and see that a file is refused rather than written
// without the protection it was asked for.
var randomSource = rand.Read
// randomBytes is the only source of randomness here, and the reason an
// encrypted file is not reproducible.
func randomBytes(n int) ([]byte, error) {
out := make([]byte, n)
if _, err := randomSource(out); err != nil {
return nil, fmt.Errorf("reader: no randomness available: %w", err)
}
return out, nil
}
// apply encrypts one object's strings and, for a stream, its data.
func (e *encrypter) apply(num, gen int, o Object) Object {
switch v := o.(type) {
case String:
return String(e.encryptBytes(num, gen, v))
case Array:
out := make(Array, len(v))
for i, x := range v {
out[i] = e.apply(num, gen, x)
}
return out
case Dict:
out := Dict{}
for k, x := range v {
out[k] = e.apply(num, gen, x)
}
return out
case *Stream:
out := &Stream{Dict: Dict{}, Raw: e.encryptBytes(num, gen, v.Raw)}
for k, x := range v.Dict {
out.Dict[k] = e.apply(num, gen, x)
}
return out
}
return o
}
// encryptBytes protects one object's bytes.
func (e *encrypter) encryptBytes(num, gen int, data []byte) []byte {
dec := &decryptor{key: e.key}
key := dec.objectKey(num, gen, e.method)
iv, err := randomBytes(aes.BlockSize)
if err != nil {
// Writing the bytes as they are would hand out in the clear what was
// asked to be protected, so the failure is remembered and the whole
// file is refused.
e.note(err)
return data
}
// The file key is 32 bytes by construction.
block, _ := aes.NewCipher(key)
// The padding CBC calls for, always at least one whole block of it.
n := aes.BlockSize - len(data)%aes.BlockSize
padded := append(append([]byte{}, data...), bytes.Repeat([]byte{byte(n)}, n)...)
out := make([]byte, len(padded))
cipher.NewCBCEncrypter(block, iv).CryptBlocks(out, padded)
return append(iv, out...)
}
// md5Sum is the hash the pre-2.0 handlers are built on.
func md5Sum(b []byte) []byte {
sum := md5.Sum(b)
return sum[:]
}
// writeEncryptDict writes the /Encrypt dictionary and returns the trailer
// entries a reader needs to find and use it. The dictionary is the one object
// in the file that is never encrypted, since it says how to decrypt the rest.
func (w *Writer) writeEncryptDict() Dict {
if w.encrypt == nil {
return nil
}
ref := w.Reserve()
w.encrypt.number = ref.Num
w.writeInlineRaw(ref, w.encrypt.dict)
// The identifier takes part in the older key derivation and has to be in
// the trailer for a reader to reach it.
id := String(w.encrypt.id)
return Dict{"Encrypt": ref, "ID": Array{id, id}}
}
// note keeps the first thing that went wrong while encrypting, so a file that
// could not be protected is refused rather than written in the clear.
func (e *encrypter) note(err error) {
if e.err == nil {
e.err = err
}
}
+475
View File
@@ -0,0 +1,475 @@
package reader
import (
"bytes"
"compress/flate"
"compress/zlib"
"fmt"
"io"
)
// maxDecodedSize caps what one stream may expand to. A few hundred bytes of
// Flate can name gigabytes, and a reader that runs in a browser tab must
// refuse rather than fill the heap. Tests lower it.
var maxDecodedSize int64 = 1 << 30
// ImageFilter reports whether a filter yields an encoded image rather than a
// byte stream. [Decode] stops at one of these and hands the caller the still
// encoded bytes, because decoding them is an image decoder's job.
//
// /CCITTFaxDecode was on this list and is not any more. It does not carry an
// image with its own idea of how many components it has and how deep they are,
// the way DCT and JPX do: it produces bilevel samples, one bit a pixel, and the
// stream dictionary says what they mean. That is a byte stream, so it is a
// filter — and decoding it here means every caller gets it rather than each
// writing its own. See ccitt.go.
func ImageFilter(n Name) bool {
switch n {
case "DCTDecode", "DCT", "JPXDecode", "JBIG2Decode":
return true
}
return false
}
// A Decoded is the outcome of applying a stream's filter chain, including the
// outcome of a chain that could not be finished.
//
// The point of the type is that a caller can tell the three outcomes apart
// without having to be careful. Bytes a filter decoded and bytes no filter
// decoded arrive in different fields, so the second kind cannot be painted as
// samples or tokenised as content by a caller that forgot to check a flag.
type Decoded struct {
// Data is what the chain decoded. A recovered decode leaves fewer bytes
// here than the stream meant to carry, but they are bytes of the kind it
// meant to carry: Data never holds bytes that no filter has decoded.
//
// The one case where Data is still encoded is the deliberate one: a chain
// that stopped at an image filter, which Image names and Recovered does
// not flag, because stopping there is the contract.
Data []byte
// Undecoded holds the bytes the chain could not get past, still in the
// encoding Filter names. It is set instead of Data — never beside it —
// when the filter that failed produced nothing at all.
//
// This is where a compressed content stream, a truncated fax, or a filter
// nobody implements ends up. A caller that wants the stream as it lies has
// to ask for it by a name that says what it is; a caller that reads Data
// cannot be handed it by accident. Painting undecoded bytes as a one-bit
// image looks like a page with something on it, which is why nothing about
// this is left to a flag.
Undecoded []byte
// Image names the image filter the chain stopped at, when it stopped at
// one; Data is then the bytes still encoded in it.
Image Name
// Recovered says the chain could not be run to the end. A caller that must
// not act on damaged data stops here — or calls [Decode], which refuses
// outright.
Recovered bool
// Cause says why the chain stopped, and is set exactly when Recovered is.
Cause error
// Filter names the filter that could not be applied, when one is to blame:
// a chain whose /Filter entry itself is unreadable blames nothing.
Filter Name
}
// DecodeRecovering applies a stream dictionary's filter chain to raw and never
// fails. A filter that cannot be applied — corrupt Flate data, a stream that
// stops in the middle, a filter name nobody implements — ends the chain, and
// what the filters before it produced is returned with [Decoded.Recovered]
// set. That is deliberately what every other reader does: refusing the stream
// loses a page the file can still show.
//
// The salvage is always as far down the chain as the filters got, never the
// bytes as they arrived: a damaged Flate stream yields the prefix it did
// inflate, in [Decoded.Data]. A filter that produced nothing at all leaves the
// bytes in [Decoded.Undecoded] instead, still in its encoding, because that is
// what they are.
func DecodeRecovering(d Dict, raw []byte, resolve Resolver) Decoded {
filters, parms, err := filterChain(d, resolve)
if err != nil {
return Decoded{Undecoded: raw, Recovered: true, Cause: err}
}
data := raw
for i, f := range filters {
if ImageFilter(f) {
return Decoded{Data: data, Image: f}
}
out, err := applyFilter(f, data, parms[i], resolve)
if err != nil {
if len(out) == 0 {
return Decoded{Undecoded: data, Recovered: true, Cause: err, Filter: f}
}
return Decoded{Data: out, Recovered: true, Cause: err, Filter: f}
}
data = out
}
return Decoded{Data: data}
}
// Decode applies a stream dictionary's filter chain to raw. It returns the
// decoded bytes and, when the chain ends in an image filter, that filter's
// name together with the bytes still encoded in it.
//
// Decode is the strict reading: a chain that cannot be run to the end is an
// error and yields no bytes. [DecodeRecovering] is the lenient one, and says
// which of the two it gave you.
func Decode(d Dict, raw []byte, resolve Resolver) ([]byte, Name, error) {
r := DecodeRecovering(d, raw, resolve)
if r.Recovered {
return nil, "", r.Cause
}
return r.Data, r.Image, nil
}
// DecodeStream is Decode for a parsed stream.
func DecodeStream(s *Stream, resolve Resolver) ([]byte, Name, error) {
return Decode(s.Dict, s.Raw, resolve)
}
// DecodeStreamRecovering is DecodeRecovering for a parsed stream.
func DecodeStreamRecovering(s *Stream, resolve Resolver) Decoded {
return DecodeRecovering(s.Dict, s.Raw, resolve)
}
// filterChain reads /Filter and /DecodeParms, each of which may be a single
// value or an array, and returns them aligned.
func filterChain(d Dict, r Resolver) ([]Name, []Dict, error) {
fo, err := Resolve(d.Get("Filter"), r)
if err != nil {
return nil, nil, err
}
var names []Name
switch v := fo.(type) {
case Null:
case Name:
names = []Name{v}
case Array:
for _, e := range v {
eo, err := Resolve(e, r)
if err != nil {
return nil, nil, err
}
n, ok := ToName(eo)
if !ok {
return nil, nil, fmt.Errorf("reader: /Filter array holds a %s, not a name", eo.Kind())
}
names = append(names, n)
}
default:
return nil, nil, fmt.Errorf("reader: /Filter is a %s, not a name or an array", fo.Kind())
}
parms := make([]Dict, len(names))
po, err := Resolve(d.Get("DecodeParms"), r)
if err != nil {
return nil, nil, err
}
switch v := po.(type) {
case Null:
case Dict:
if len(parms) > 0 {
parms[0] = v
}
case Array:
for i, e := range v {
if i >= len(parms) {
break
}
eo, err := Resolve(e, r)
if err != nil {
return nil, nil, err
}
if pd, ok := ToDict(eo); ok {
parms[i] = pd
}
}
default:
return nil, nil, fmt.Errorf("reader: /DecodeParms is a %s, not a dictionary or an array", po.Kind())
}
return names, parms, nil
}
// applyFilter runs one filter. The abbreviated names are the ones inline
// images use; regular streams may legally carry them too.
func applyFilter(f Name, data []byte, parm Dict, r Resolver) ([]byte, error) {
switch f {
case "FlateDecode", "Fl":
out, err := flateDecode(data)
if err != nil {
return salvage(out, err, parm, r)
}
return applyPredictor(out, parm, r)
case "LZWDecode", "LZW":
early := intParm(parm, "EarlyChange", 1, r)
out, err := lzwDecode(data, early != 0)
if err != nil {
return salvage(out, err, parm, r)
}
return applyPredictor(out, parm, r)
case "ASCIIHexDecode", "AHx":
return asciiHexDecode(data)
case "ASCII85Decode", "A85":
return ascii85Decode(data)
case "RunLengthDecode", "RL":
return runLengthDecode(data)
case "CCITTFaxDecode", "CCF":
return ccittDecode(data, ccittParamsOf(parm, r))
case "Crypt":
return cryptFilter(data, parm, r)
}
return nil, fmt.Errorf("reader: unsupported filter /%s", f)
}
// cryptFilter applies the /Crypt filter, which does not transform anything: it
// names the crypt filter a stream's bytes were encrypted with, and /Identity —
// which is also what /Crypt with no /Name means — says they were not encrypted
// at all. Either way the document that owns the stream has already dealt with
// its encryption by the time the filter chain runs, so there is nothing here
// left to undo.
//
// A /Name this reader cannot account for is reported rather than waved through,
// because the bytes would then be ciphertext that only looks like data.
func cryptFilter(data []byte, parm Dict, r Resolver) ([]byte, error) {
o, err := Resolve(parm.Get("Name"), r)
if err != nil {
return nil, fmt.Errorf("reader: /Crypt filter: %w", err)
}
if n, ok := ToName(o); ok && n != "Identity" {
// No bytes come back: they are ciphertext, and handing them over as
// though the filter had run is how ciphertext gets painted.
return nil, fmt.Errorf("reader: /Crypt filter names /%s, which this reader cannot apply", n)
}
return data, nil
}
// firstFilter names the first filter of a stream's chain. Only direct values
// are read: it is consulted while the file key is being established, before
// following an indirect reference is safe.
func firstFilter(d Dict) (Name, bool) {
switch v := d.Get("Filter").(type) {
case Name:
return v, true
case Array:
if len(v) > 0 {
n, ok := ToName(v[0])
return n, ok
}
}
return "", false
}
// firstDecodeParms is the parameter dictionary belonging to the first filter,
// read directly for the same reason.
func firstDecodeParms(d Dict) Dict {
switch v := d.Get("DecodeParms").(type) {
case Dict:
return v
case Array:
if len(v) > 0 {
if pd, ok := ToDict(v[0]); ok {
return pd
}
}
}
return nil
}
// salvage finishes a filter that stopped part-way. The prefix it did produce is
// still worth having, and the predictor still applies to it: the predictors a
// PDF may name all undo one row at a time, so a buffer that stops in the middle
// undoes up to where it stops.
func salvage(out []byte, err error, parm Dict, r Resolver) ([]byte, error) {
if len(out) == 0 {
return nil, err
}
if p, perr := applyPredictor(out, parm, r); perr == nil {
return p, err
}
return out, err
}
// intParm reads an integer decode parameter, falling back to its default.
func intParm(parm Dict, key Name, def int, r Resolver) int {
if parm == nil {
return def
}
o, err := Resolve(parm.Get(key), r)
if err != nil {
return def
}
n, ok := ToInt(o)
if !ok {
return def
}
return int(n)
}
// flateDecode inflates a stream. Producers emit both zlib-wrapped and bare
// deflate data, sometimes with leading white-space, and truncate the last
// stream in a damaged file. All four cases yield whatever bytes are there — but
// a prefix comes back with the error that ended it, never dressed up as a whole
// stream, so the caller can tell the difference.
func flateDecode(data []byte) ([]byte, error) {
i := 0
for i < len(data) && isSpace(data[i]) {
i++
}
data = data[i:]
if zr, err := zlib.NewReader(bytes.NewReader(data)); err == nil {
out, err := readAllCapped(zr)
if err == nil {
return out, nil
}
if len(out) > 0 {
return out, fmt.Errorf("reader: FlateDecode: %w", err)
}
}
out, err := readAllCapped(flate.NewReader(bytes.NewReader(data)))
if err != nil {
return out, fmt.Errorf("reader: FlateDecode: %w", err)
}
return out, nil
}
// readAllCapped reads r, refusing to grow past maxDecodedSize.
func readAllCapped(r io.Reader) ([]byte, error) {
var buf bytes.Buffer
n, err := io.Copy(&buf, io.LimitReader(r, maxDecodedSize+1))
if err != nil {
return buf.Bytes(), err
}
if n > maxDecodedSize {
return nil, fmt.Errorf("reader: decoded stream exceeds %d bytes", maxDecodedSize)
}
return buf.Bytes(), nil
}
// asciiHexDecode reads hexadecimal digits up to the '>' terminator, padding a
// final odd digit with zero.
func asciiHexDecode(data []byte) ([]byte, error) {
out := make([]byte, 0, len(data)/2)
hi := -1
for i := 0; i < len(data); i++ {
c := data[i]
if c == '>' {
break
}
if isSpace(c) {
continue
}
v := hexVal(c)
if v < 0 {
return out, fmt.Errorf("reader: ASCIIHexDecode: invalid digit %q", rune(c))
}
if hi < 0 {
hi = v
continue
}
out = append(out, byte(hi<<4|v))
hi = -1
}
if hi >= 0 {
out = append(out, byte(hi<<4))
}
return out, nil
}
// ascii85Decode reads base-85 groups up to the "~>" terminator.
func ascii85Decode(data []byte) ([]byte, error) {
if bytes.HasPrefix(data, []byte("<~")) {
data = data[2:]
}
out := make([]byte, 0, len(data)*4/5)
var group [5]byte
n := 0
flush := func(n int) {
for i := n; i < 5; i++ {
group[i] = 'u'
}
v := uint32(0)
for _, c := range group {
v = v*85 + uint32(c-'!')
}
var b [4]byte
b[0], b[1], b[2], b[3] = byte(v>>24), byte(v>>16), byte(v>>8), byte(v)
out = append(out, b[:n-1]...)
}
for i := 0; i < len(data); i++ {
c := data[i]
if isSpace(c) {
continue
}
if c == '~' {
break
}
if c == 'z' && n == 0 {
out = append(out, 0, 0, 0, 0)
continue
}
if c < '!' || c > 'u' {
return out, fmt.Errorf("reader: ASCII85Decode: invalid character %q", rune(c))
}
group[n] = c
n++
if n == 5 {
if err := checkA85(group); err != nil {
return out, err
}
flush(5)
n = 0
}
}
if n == 1 {
return out, fmt.Errorf("reader: ASCII85Decode: truncated final group")
}
if n > 1 {
flush(n)
}
return out, nil
}
// checkA85 rejects a full group that names more than 2^32-1.
func checkA85(g [5]byte) error {
v := uint64(0)
for _, c := range g {
v = v*85 + uint64(c-'!')
}
if v > 0xFFFFFFFF {
return fmt.Errorf("reader: ASCII85Decode: group overflows 32 bits")
}
return nil
}
// runLengthDecode expands the byte-oriented run-length encoding: a length byte
// below 128 introduces that many literal bytes plus one, above 128 repeats the
// next byte, and 128 ends the data.
func runLengthDecode(data []byte) ([]byte, error) {
out := []byte{}
for i := 0; i < len(data); {
n := int(data[i])
i++
switch {
case n == 128:
return out, nil
case n < 128:
end := i + n + 1
if end > len(data) {
return out, fmt.Errorf("reader: RunLengthDecode: truncated literal run")
}
out = append(out, data[i:end]...)
i = end
default:
if i >= len(data) {
return out, fmt.Errorf("reader: RunLengthDecode: truncated repeat run")
}
out = append(out, bytes.Repeat(data[i:i+1], 257-n)...)
i++
}
}
return out, nil
}
+155
View File
@@ -0,0 +1,155 @@
package reader
import "bytes"
// inlineImageEnd finds where an inline image's data stops — the one genuinely
// ambiguous thing in a content stream, since the data is raw bytes that may
// spell EI themselves.
//
// The answers are tried in order of how much they can be trusted: an image
// with no filter says exactly how long it is through its width, height, depth
// and colour space; a declared /L is believed when an EI really does follow
// it; and failing both, every EI in the data is tried in turn and the first
// one whose data actually decodes is taken. That last step is what a scan
// alone gets wrong, because compressed bytes do spell EI by accident.
func inlineImageEnd(b []byte, start int, d Dict) (int, error) {
if n, ok := inlineImageDataLength(d); ok && start+n <= len(b) && eiFollows(b, start+n) {
return start + n, nil
}
if n, ok := ToInt(d.Get("L")); ok && n >= 0 && start+int(n) <= len(b) && eiFollows(b, start+int(n)) {
return start + int(n), nil
}
expanded := (&InlineImage{Dict: d}).Expanded()
want, haveWant := inlineSampleBytes(d)
// White-space before EI is how the keyword is meant to be written; a
// second pass allows for producers that run the data straight into it.
for _, needSpace := range []bool{true, false} {
for from := start; ; {
end, resume := nextEI(b, start, from, needSpace)
if end < 0 {
break
}
if inlineDataDecodes(expanded, b[start:end], want, haveWant) {
return end, nil
}
from = resume
}
}
return 0, &SyntaxError{start, "an inline image has no EI"}
}
// nextEI returns where the image data would end for the next EI candidate at
// or after from, and where to resume searching. It reports -1 when there is no
// candidate left.
func nextEI(b []byte, start, from int, needSpace bool) (end, resume int) {
for i := from; i+1 < len(b); i++ {
if b[i] != 'E' || b[i+1] != 'I' {
continue
}
if i+2 < len(b) && isRegular(b[i+2]) {
continue
}
if i > start && isSpace(b[i-1]) {
return i - 1, i + 2
}
if !needSpace {
return i, i + 2
}
}
return -1, len(b)
}
// eiFollows reports whether an EI keyword stands at i, after white-space.
func eiFollows(b []byte, i int) bool {
for i < len(b) && isSpace(b[i]) {
i++
}
if !bytes.HasPrefix(b[i:], []byte("EI")) {
return false
}
return i+2 >= len(b) || !isRegular(b[i+2])
}
// inlineDataDecodes reports whether a candidate stretch of bytes is really the
// whole of the image: it has to run through the declared filters without
// complaint, produce the number of samples the image says it has, and — for a
// JPEG, which no filter here can check — begin and end where a JPEG does.
func inlineDataDecodes(expanded Dict, data []byte, want int, haveWant bool) bool {
if expanded.Get("Filter").Kind() == KindNull {
return true
}
out, img, err := Decode(expanded, data, nil)
if err != nil {
return false
}
if img == "DCTDecode" {
return len(out) > 4 && out[0] == 0xFF && out[1] == 0xD8 &&
out[len(out)-2] == 0xFF && out[len(out)-1] == 0xD9
}
if img != "" {
return true
}
return !haveWant || len(out) == want
}
// inlineComponents reports how many samples one pixel of an inline image has,
// for the colour spaces that can be named inline.
func inlineComponents(d Dict) (int, bool) {
if b, ok := ToBool(d.Get("IM")); ok && b {
return 1, true
}
cs, ok := ToName(d.Get("CS"))
if !ok {
if _, isArray := ToArray(d.Get("CS")); isArray {
// An inline [/Indexed …] space has one component per sample.
return 1, true
}
return 0, false
}
switch cs {
case "G", "DeviceGray", "CalGray", "I", "Indexed":
return 1, true
case "RGB", "DeviceRGB", "CalRGB":
return 3, true
case "CMYK", "DeviceCMYK":
return 4, true
}
// A name that refers to a colour space in the page's resources: not
// something the arithmetic can settle here.
return 0, false
}
// inlineSampleBytes computes how many bytes an inline image's samples occupy
// once decoded.
func inlineSampleBytes(d Dict) (int, bool) {
w, okW := ToInt(d.Get("W"))
h, okH := ToInt(d.Get("H"))
if !okW || !okH || w <= 0 || h <= 0 {
return 0, false
}
bpc := int64(8)
if b, ok := ToInt(d.Get("BPC")); ok {
bpc = b
}
if im, ok := ToBool(d.Get("IM")); ok && im {
bpc = 1
}
if bpc <= 0 || bpc > 16 {
return 0, false
}
comps, ok := inlineComponents(d)
if !ok {
return 0, false
}
row := (w*int64(comps)*bpc + 7) / 8
return int(row * h), true
}
// inlineImageDataLength is inlineSampleBytes for an image with no filter, for
// which the samples are the data.
func inlineImageDataLength(d Dict) (int, bool) {
if d.Get("F").Kind() != KindNull || d.Get("Filter").Kind() != KindNull {
return 0, false
}
return inlineSampleBytes(d)
}
+340
View File
@@ -0,0 +1,340 @@
package reader
import (
"fmt"
"strconv"
)
// A SyntaxError reports malformed PDF syntax and where it was found, so a
// repair pass can restart from a known offset instead of giving up.
type SyntaxError struct {
Offset int
Msg string
}
// Error implements the error interface.
func (e *SyntaxError) Error() string {
return fmt.Sprintf("reader: %s at offset %d", e.Msg, e.Offset)
}
// isSpace reports the six bytes the specification calls white-space,
// NUL included.
func isSpace(c byte) bool {
return c == 0 || c == 9 || c == 10 || c == 12 || c == 13 || c == 32
}
// isDelim reports the eight delimiter bytes.
func isDelim(c byte) bool {
switch c {
case '(', ')', '<', '>', '[', ']', '{', '}', '/', '%':
return true
}
return false
}
// isRegular reports a byte that may appear inside a name, a number or a
// keyword: anything that is neither white-space nor a delimiter.
func isRegular(c byte) bool { return !isSpace(c) && !isDelim(c) }
// hexVal decodes one hexadecimal digit, or reports -1.
func hexVal(c byte) int {
switch {
case c >= '0' && c <= '9':
return int(c - '0')
case c >= 'a' && c <= 'f':
return int(c-'a') + 10
case c >= 'A' && c <= 'F':
return int(c-'A') + 10
}
return -1
}
type tokKind uint8
const (
tokEOF tokKind = iota
tokInteger
tokReal
tokString
tokName
tokArrayOpen
tokArrayClose
tokDictOpen
tokDictClose
tokBraceOpen
tokBraceClose
tokKeyword
)
// A token is one lexical unit. text carries the bytes of a string, name or
// keyword; i and f carry the value of a number.
type token struct {
kind tokKind
text []byte
i int64
f float64
pos int
}
// A lexer walks the PDF object syntax over a byte slice. It never copies the
// input except where an escape sequence forces it.
type lexer struct {
buf []byte
pos int
}
// skipSpace consumes white-space and comments.
func (l *lexer) skipSpace() {
for l.pos < len(l.buf) {
c := l.buf[l.pos]
if isSpace(c) {
l.pos++
continue
}
if c == '%' {
for l.pos < len(l.buf) && l.buf[l.pos] != '\n' && l.buf[l.pos] != '\r' {
l.pos++
}
continue
}
return
}
}
// next returns the token at the current position and advances past it.
func (l *lexer) next() (token, error) {
l.skipSpace()
start := l.pos
if l.pos >= len(l.buf) {
return token{kind: tokEOF, pos: start}, nil
}
switch c := l.buf[l.pos]; {
case c == '[':
l.pos++
return token{kind: tokArrayOpen, pos: start}, nil
case c == ']':
l.pos++
return token{kind: tokArrayClose, pos: start}, nil
case c == '{':
l.pos++
return token{kind: tokBraceOpen, pos: start}, nil
case c == '}':
l.pos++
return token{kind: tokBraceClose, pos: start}, nil
case c == '<':
if l.pos+1 < len(l.buf) && l.buf[l.pos+1] == '<' {
l.pos += 2
return token{kind: tokDictOpen, pos: start}, nil
}
return l.hexString(start)
case c == '>':
if l.pos+1 < len(l.buf) && l.buf[l.pos+1] == '>' {
l.pos += 2
return token{kind: tokDictClose, pos: start}, nil
}
l.pos++
return token{}, &SyntaxError{start, "stray '>'"}
case c == '(':
return l.literalString(start)
case c == ')':
l.pos++
return token{}, &SyntaxError{start, "stray ')'"}
case c == '/':
return l.name(start)
case c == '+' || c == '-' || c == '.' || (c >= '0' && c <= '9'):
return l.number(start)
default:
return l.keyword(start), nil
}
}
// keyword reads a run of regular characters: an operator, obj, endobj, R,
// true, false, null.
func (l *lexer) keyword(start int) token {
p := l.pos
for p < len(l.buf) && isRegular(l.buf[p]) {
p++
}
l.pos = p
return token{kind: tokKeyword, text: l.buf[start:p], pos: start}
}
// number reads an integer or a real. It stops at the first byte that cannot
// continue the number rather than swallowing the whole run of regular
// characters: producers do write "3.4-5" for two numbers, and a reader that
// rejects that loses the rest of the content stream.
func (l *lexer) number(start int) (token, error) {
p := l.pos
for p < len(l.buf) && (l.buf[p] == '+' || l.buf[p] == '-') {
p++
}
digits, dot := 0, false
for p < len(l.buf) {
c := l.buf[p]
switch {
case c >= '0' && c <= '9':
digits++
case c == '.' && !dot:
dot = true
default:
goto done
}
p++
}
done:
s := l.buf[l.pos:p]
l.pos = p
if digits == 0 {
return token{}, &SyntaxError{start, "malformed number " + strconv.Quote(string(s))}
}
if i, err := strconv.ParseInt(string(s), 10, 64); err == nil {
return token{kind: tokInteger, i: i, f: float64(i), pos: start}, nil
}
f, err := parseReal(s)
if err != nil {
return token{}, &SyntaxError{start, "malformed number " + strconv.Quote(string(s))}
}
return token{kind: tokReal, i: int64(f), f: f, pos: start}, nil
}
// parseReal accepts the doubled sign of "--5", which producers do write; only
// the first sign counts. Everything else has already been filtered out by the
// scan above.
func parseReal(s []byte) (float64, error) {
clean := make([]byte, 0, len(s))
i := 0
if i < len(s) && (s[i] == '+' || s[i] == '-') {
clean = append(clean, s[i])
i++
}
for i < len(s) && (s[i] == '+' || s[i] == '-') {
i++
}
clean = append(clean, s[i:]...)
return strconv.ParseFloat(string(clean), 64)
}
// literalString reads a (parenthesised) string, resolving escapes. Nested
// parentheses nest; an end-of-line inside the string, however written, becomes
// a single line feed.
func (l *lexer) literalString(start int) (token, error) {
l.pos++
depth := 1
out := []byte{}
for l.pos < len(l.buf) {
c := l.buf[l.pos]
l.pos++
switch c {
case '\\':
if l.pos >= len(l.buf) {
return token{}, &SyntaxError{start, "unterminated string"}
}
e := l.buf[l.pos]
l.pos++
switch {
case e == 'n':
out = append(out, '\n')
case e == 'r':
out = append(out, '\r')
case e == 't':
out = append(out, '\t')
case e == 'b':
out = append(out, '\b')
case e == 'f':
out = append(out, '\f')
case e == '\r':
// A backslash before an end-of-line continues the line.
if l.pos < len(l.buf) && l.buf[l.pos] == '\n' {
l.pos++
}
case e == '\n':
case e >= '0' && e <= '7':
v := int(e - '0')
for k := 0; k < 2 && l.pos < len(l.buf) && l.buf[l.pos] >= '0' && l.buf[l.pos] <= '7'; k++ {
v = v*8 + int(l.buf[l.pos]-'0')
l.pos++
}
out = append(out, byte(v))
default:
// \( \) \\ and, per the specification, any other escaped
// character stands for itself.
out = append(out, e)
}
case '(':
depth++
out = append(out, c)
case ')':
depth--
if depth == 0 {
return token{kind: tokString, text: out, pos: start}, nil
}
out = append(out, c)
case '\r':
if l.pos < len(l.buf) && l.buf[l.pos] == '\n' {
l.pos++
}
out = append(out, '\n')
default:
out = append(out, c)
}
}
return token{}, &SyntaxError{start, "unterminated string"}
}
// hexString reads a <hex> string. White-space is ignored and a final odd digit
// is padded with zero, as the specification requires.
func (l *lexer) hexString(start int) (token, error) {
l.pos++
out := []byte{}
hi := -1
for l.pos < len(l.buf) {
c := l.buf[l.pos]
l.pos++
if c == '>' {
if hi >= 0 {
out = append(out, byte(hi<<4))
}
return token{kind: tokString, text: out, pos: start}, nil
}
if isSpace(c) {
continue
}
v := hexVal(c)
if v < 0 {
// Producers do emit a stray byte inside <…>; skipping it is what
// every other reader does, and losing the whole object is worse.
continue
}
if hi < 0 {
hi = v
continue
}
out = append(out, byte(hi<<4|v))
hi = -1
}
return token{}, &SyntaxError{start, "unterminated hex string"}
}
// name reads a /Name, resolving #xx escapes.
func (l *lexer) name(start int) (token, error) {
l.pos++
out := []byte{}
for l.pos < len(l.buf) && isRegular(l.buf[l.pos]) {
c := l.buf[l.pos]
l.pos++
if c != '#' {
out = append(out, c)
continue
}
if l.pos+1 >= len(l.buf) {
return token{}, &SyntaxError{start, "truncated #xx escape in name"}
}
h1, h2 := hexVal(l.buf[l.pos]), hexVal(l.buf[l.pos+1])
if h1 < 0 || h2 < 0 {
return token{}, &SyntaxError{start, "invalid #xx escape in name"}
}
out = append(out, byte(h1<<4|h2))
l.pos += 2
}
return token{kind: tokName, text: out, pos: start}, nil
}
+86
View File
@@ -0,0 +1,86 @@
package reader
import "fmt"
// LZW code points that are not table entries.
const (
lzwClear = 256
lzwEOD = 257
lzwFirst = 258
lzwMax = 4096
)
// lzwDecode expands PDF's variable-code-width LZW.
//
// The standard library's compress/lzw cannot be used here: PDF's /EarlyChange
// parameter defaults to 1, which widens the code one entry before the table
// actually fills, and compress/lzw implements neither that convention nor a
// way to ask for it. Getting this wrong does not fail — it silently produces
// plausible rubbish a few hundred bytes in.
func lzwDecode(data []byte, early bool) ([]byte, error) {
var table [lzwMax][]byte
for i := 0; i < 256; i++ {
table[i] = []byte{byte(i)}
}
next := lzwFirst
width := 9
bit := 0
var out, prev []byte
for {
if bit+width > len(data)*8 {
// A truncated stream keeps what it managed to say.
return out, nil
}
code := 0
for k := 0; k < width; k++ {
code = code<<1 | int(data[(bit+k)/8]>>(7-(bit+k)%8)&1)
}
bit += width
switch code {
case lzwEOD:
return out, nil
case lzwClear:
next, width, prev = lzwFirst, 9, nil
continue
}
var entry []byte
switch {
case code < next && table[code] != nil:
entry = table[code]
case code == next && prev != nil:
// The encoder may name the entry it is about to define.
entry = append(append([]byte{}, prev...), prev[0])
default:
// What the stream did say is kept, with the reason it stopped.
return out, fmt.Errorf("reader: LZWDecode: code %d is not in the table", code)
}
out = append(out, entry...)
if prev != nil && next < lzwMax {
table[next] = append(append([]byte{}, prev...), entry[0])
next++
}
prev = entry
// With EarlyChange the width grows one entry sooner. Both conventions
// are the same comparison against a shifted count.
count := next
if early {
count++
}
switch {
case count >= 2048:
width = 12
case count >= 1024:
width = 11
case count >= 512:
width = 10
default:
width = 9
}
}
}
+244
View File
@@ -0,0 +1,244 @@
// Package reader takes existing PDF bytes apart: the object syntax, the
// cross-reference machinery, stream filters, and the document structure built
// on top of them. It is the parsing counterpart of github.com/go-pdfkit/pdfkit,
// which writes PDF.
//
// Nothing outside the Go standard library is used, so the package builds for
// js/wasm and for every architecture the fleet targets.
package reader
import "fmt"
// Kind names the eight basic PDF object types plus the two the file format
// adds on top of them: streams and indirect references.
type Kind uint8
// The object kinds. [Object.Kind] reports one of these.
const (
KindNull Kind = iota
KindBool
KindInteger
KindReal
KindString
KindName
KindArray
KindDict
KindStream
KindRef
)
var kindNames = [...]string{
KindNull: "null",
KindBool: "boolean",
KindInteger: "integer",
KindReal: "real",
KindString: "string",
KindName: "name",
KindArray: "array",
KindDict: "dictionary",
KindStream: "stream",
KindRef: "reference",
}
// String names the kind, for error messages.
func (k Kind) String() string {
if int(k) >= len(kindNames) {
return fmt.Sprintf("Kind(%d)", uint8(k))
}
return kindNames[k]
}
// An Object is any value the PDF object syntax can express. The concrete types
// are [Null], [Bool], [Integer], [Real], [String], [Name], [Array], [Dict],
// [Ref] and *[Stream]; Kind tells them apart without a type switch.
type Object interface {
Kind() Kind
}
// Null is the PDF null object. A missing dictionary entry resolves to Null
// rather than to a nil Object, so callers never have to nil-check.
type Null struct{}
// Kind implements [Object].
func (Null) Kind() Kind { return KindNull }
// Bool is a PDF boolean.
type Bool bool
// Kind implements [Object].
func (Bool) Kind() Kind { return KindBool }
// Integer is a PDF integer. Numbers too large for an int64 are parsed as
// [Real], which is what every other reader does.
type Integer int64
// Kind implements [Object].
func (Integer) Kind() Kind { return KindInteger }
// Real is a PDF real number.
type Real float64
// Kind implements [Object].
func (Real) Kind() Kind { return KindReal }
// String is a PDF string, already unescaped: the bytes a literal or hex string
// denotes, with no interpretation of their text encoding.
type String []byte
// Kind implements [Object].
func (String) Kind() Kind { return KindString }
// Name is a PDF name with its #xx escapes resolved and the leading slash
// dropped, so /Type is Name("Type").
type Name string
// Kind implements [Object].
func (Name) Kind() Kind { return KindName }
// Array is a PDF array.
type Array []Object
// Kind implements [Object].
func (Array) Kind() Kind { return KindArray }
// Dict is a PDF dictionary.
type Dict map[Name]Object
// Kind implements [Object].
func (Dict) Kind() Kind { return KindDict }
// Get returns the entry, or [Null] when it is absent. The value may still be a
// [Ref]; use [Resolve] to follow it.
func (d Dict) Get(k Name) Object {
if v, ok := d[k]; ok && v != nil {
return v
}
return Null{}
}
// Ref is an indirect reference: the "12 0 R" that stands in for an object
// stored elsewhere in the file.
type Ref struct {
Num int
Gen int
}
// Kind implements [Object].
func (Ref) Kind() Kind { return KindRef }
// String renders the reference the way it appears in a file.
func (r Ref) String() string { return fmt.Sprintf("%d %d R", r.Num, r.Gen) }
// Stream is a PDF stream: a dictionary plus the raw, still-encoded bytes
// between the stream and endstream keywords. [Decode] applies the filter chain.
type Stream struct {
Dict Dict
Raw []byte
}
// Kind implements [Object].
func (*Stream) Kind() Kind { return KindStream }
// A Resolver fetches the object an indirect reference names. A document
// supplies one; a nil Resolver makes every reference resolve to [Null], which
// is what a caller parsing an isolated fragment wants.
type Resolver func(Ref) (Object, error)
// maxRefDepth bounds a chain of references pointing at references. A file can
// make that chain a cycle, and a cycle must not hang the reader.
const maxRefDepth = 64
// Resolve follows indirect references until it reaches a direct object. A nil
// object, an absent entry and an unresolvable reference all yield [Null].
func Resolve(o Object, r Resolver) (Object, error) {
if o == nil {
return Null{}, nil
}
for depth := 0; ; depth++ {
ref, ok := o.(Ref)
if !ok {
return o, nil
}
if r == nil {
return Null{}, nil
}
if depth >= maxRefDepth {
return nil, fmt.Errorf("reader: reference chain longer than %d at %s", maxRefDepth, ref)
}
v, err := r(ref)
if err != nil {
return nil, err
}
if v == nil {
return Null{}, nil
}
o = v
}
}
// ToBool reports the value of a boolean object.
func ToBool(o Object) (bool, bool) {
v, ok := o.(Bool)
return bool(v), ok
}
// ToInt reports the value of an integer object, and of a real whose value is a
// whole number — files do write "3.0" where an integer belongs.
func ToInt(o Object) (int64, bool) {
switch v := o.(type) {
case Integer:
return int64(v), true
case Real:
if float64(int64(v)) == float64(v) {
return int64(v), true
}
}
return 0, false
}
// ToFloat reports the value of any numeric object.
func ToFloat(o Object) (float64, bool) {
switch v := o.(type) {
case Integer:
return float64(v), true
case Real:
return float64(v), true
}
return 0, false
}
// ToString reports the bytes of a string object.
func ToString(o Object) ([]byte, bool) {
v, ok := o.(String)
return []byte(v), ok
}
// ToName reports the value of a name object.
func ToName(o Object) (Name, bool) {
v, ok := o.(Name)
return v, ok
}
// ToArray reports the elements of an array object.
func ToArray(o Object) (Array, bool) {
v, ok := o.(Array)
return v, ok
}
// ToDict reports the dictionary of a dictionary object, or a stream's own
// dictionary — a stream is a dictionary everywhere a dictionary is expected.
func ToDict(o Object) (Dict, bool) {
switch v := o.(type) {
case Dict:
return v, true
case *Stream:
return v.Dict, true
}
return nil, false
}
// ToStream reports the stream object.
func ToStream(o Object) (*Stream, bool) {
v, ok := o.(*Stream)
return v, ok
}
+128
View File
@@ -0,0 +1,128 @@
package reader
import (
"bytes"
"compress/zlib"
"fmt"
)
// objectsPerStream bounds how many objects go into one object stream. A
// stream has to be inflated whole to read any object in it, so packing
// everything into one would make opening a file read all of it.
const objectsPerStream = 200
// packedAt says which object stream an object was packed into, and where.
type packedAt struct {
stream int
index int
}
// pendingObject is an object waiting to be packed.
type pendingObject struct {
ref Ref
obj Object
}
// packObjects writes every waiting object into object streams. It is called
// once, from Finish.
func (w *Writer) packObjects() {
for start := 0; start < len(w.pending); start += objectsPerStream {
end := start + objectsPerStream
if end > len(w.pending) {
end = len(w.pending)
}
w.packGroup(w.pending[start:end])
}
w.pending = nil
}
// packGroup writes one object stream holding the given objects.
func (w *Writer) packGroup(group []pendingObject) {
var head, payload bytes.Buffer
for _, p := range group {
fmt.Fprintf(&head, "%d %d ", p.ref.Num, payload.Len())
payload.Write(AppendObject(nil, p.obj))
payload.WriteByte('\n')
}
body := append(head.Bytes(), payload.Bytes()...)
stream := w.Reserve()
for i, p := range group {
w.packed[p.ref.Num] = packedAt{stream: stream.Num, index: i}
}
w.writeInline(stream, &Stream{
Dict: Dict{
"Type": Name("ObjStm"),
"N": Integer(len(group)),
"First": Integer(head.Len()),
},
Raw: body,
})
}
// flateCompress compresses a stream's data, which is what makes packing worth doing.
func flateCompress(data []byte) []byte {
var buf bytes.Buffer
zw := zlib.NewWriter(&buf)
// A bytes.Buffer never refuses bytes, and Close only flushes.
zw.Write(data)
zw.Close()
return buf.Bytes()
}
// finishWithXrefStream writes the cross-reference information as a stream
// rather than a table, which is the only form that can name an object inside
// an object stream.
func (w *Writer) finishWithXrefStream(trailer Dict) ([]byte, error) {
// Finish has already written the /Encrypt dictionary and put what a
// reader needs into the trailer.
w.packObjects()
xref := w.Reserve()
// The cross-reference stream is reserved after everything else — the
// object streams included — so it always carries the highest number.
high := xref.Num
start := w.buf.Len()
w.offsets[xref.Num] = start
rows := make([]byte, 0, (high+1)*7)
for num := 0; num <= high; num++ {
switch {
case num == 0:
rows = append(rows, 0, 0, 0, 0, 0, 0xFF, 0xFF)
case w.offsets[num] != 0 || num == xref.Num:
rows = append(rows, xrefRow(1, int64(w.offsets[num]), 0)...)
default:
if at, ok := w.packed[num]; ok {
rows = append(rows, xrefRow(2, int64(at.stream), int64(at.index))...)
continue
}
rows = append(rows, 0, 0, 0, 0, 0, 0xFF, 0xFF)
}
}
dict := Dict{
"Type": Name("XRef"),
"Size": Integer(high + 1),
"W": Array{Integer(1), Integer(4), Integer(2)},
}
for k, v := range trailer {
dict[k] = v
}
w.writeInlineRaw(xref, &Stream{Dict: dict, Raw: rows})
fmt.Fprintf(&w.buf, "startxref\n%d\n%%%%EOF\n", start)
if w.encrypt != nil && w.encrypt.err != nil {
w.note(w.encrypt.err)
}
if w.err != nil {
return nil, w.err
}
return w.buf.Bytes(), nil
}
// xrefRow renders one three-field row of a cross-reference stream.
func xrefRow(kind byte, f2, f3 int64) []byte {
return []byte{
kind,
byte(f2 >> 24), byte(f2 >> 16), byte(f2 >> 8), byte(f2),
byte(f3 >> 8), byte(f3),
}
}
+295
View File
@@ -0,0 +1,295 @@
package reader
import (
"bytes"
"strconv"
)
// A parser turns the token stream into objects. resolve is consulted only for
// a stream's /Length, which files are allowed to store indirectly.
type parser struct {
lex lexer
resolve Resolver
}
// ParseObject parses a single direct object from the start of b and reports how
// many bytes it consumed.
func ParseObject(b []byte) (Object, int, error) {
p := &parser{lex: lexer{buf: b}}
o, err := p.parseObject()
if err != nil {
return nil, 0, err
}
return o, p.lex.pos, nil
}
// ParseIndirectObject parses "N G obj … endobj" from the start of b, returning
// the reference it defines, the object itself, and how many bytes it consumed.
// A stream's /Length may be an indirect reference; resolve supplies it, and a
// nil or unhelpful resolve falls back to scanning for the endstream keyword.
func ParseIndirectObject(b []byte, resolve Resolver) (Ref, Object, int, error) {
p := &parser{lex: lexer{buf: b}, resolve: resolve}
num, err := p.expectUint("object number")
if err != nil {
return Ref{}, nil, 0, err
}
gen, err := p.expectUint("generation number")
if err != nil {
return Ref{}, nil, 0, err
}
if err := p.expectKeyword("obj"); err != nil {
return Ref{}, nil, 0, err
}
ref := Ref{Num: num, Gen: gen}
// An indirect object may have no value at all — "7 0 obj endobj" —
// which the corpus does contain. Its value is null.
if p.accept("endobj") {
return ref, Null{}, p.lex.pos, nil
}
obj, err := p.parseObject()
if err != nil {
return ref, nil, 0, err
}
obj, err = p.maybeStream(obj)
if err != nil {
return ref, nil, 0, err
}
p.accept("endobj")
return ref, obj, p.lex.pos, nil
}
// expectUint reads a non-negative integer token.
func (p *parser) expectUint(what string) (int, error) {
t, err := p.lex.next()
if err != nil {
return 0, err
}
if t.kind != tokInteger || t.i < 0 {
return 0, &SyntaxError{t.pos, what + " expected"}
}
return int(t.i), nil
}
// expectKeyword requires the given keyword next.
func (p *parser) expectKeyword(kw string) error {
t, err := p.lex.next()
if err != nil {
return err
}
if t.kind != tokKeyword || string(t.text) != kw {
return &SyntaxError{t.pos, strconv.Quote(kw) + " expected"}
}
return nil
}
// accept consumes the keyword if it is next, and reports whether it was.
func (p *parser) accept(kw string) bool {
save := p.lex.pos
t, err := p.lex.next()
if err == nil && t.kind == tokKeyword && string(t.text) == kw {
return true
}
p.lex.pos = save
return false
}
// maybeStream turns a dictionary followed by the stream keyword into a Stream.
func (p *parser) maybeStream(obj Object) (Object, error) {
save := p.lex.pos
t, err := p.lex.next()
if err != nil || t.kind != tokKeyword || string(t.text) != "stream" {
p.lex.pos = save
return obj, nil
}
d, ok := obj.(Dict)
if !ok {
return nil, &SyntaxError{t.pos, "stream keyword after a " + obj.Kind().String()}
}
return p.readStream(d, t.pos)
}
// readStream extracts the bytes between stream and endstream. The declared
// /Length is trusted only when endstream really does follow it — producers get
// it wrong often enough that the scan below is the common path, not the
// exception.
func (p *parser) readStream(d Dict, kwPos int) (Object, error) {
b := p.lex.buf
i := p.lex.pos
for i < len(b) && (b[i] == ' ' || b[i] == '\t') {
i++
}
if i < len(b) && b[i] == '\r' {
i++
}
if i < len(b) && b[i] == '\n' {
i++
}
start := i
if n, ok := p.streamLength(d); ok && start+n <= len(b) {
if end := endstreamAt(b, start+n); end >= 0 {
p.lex.pos = end
return &Stream{Dict: d, Raw: b[start : start+n]}, nil
}
}
j := bytes.Index(b[start:], []byte("endstream"))
if j < 0 {
return nil, &SyntaxError{kwPos, "unterminated stream"}
}
end := start + j
p.lex.pos = end + len("endstream")
// The end-of-line that precedes endstream belongs to the file, not to the
// stream's data.
if end > start && b[end-1] == '\n' {
end--
}
if end > start && b[end-1] == '\r' {
end--
}
return &Stream{Dict: d, Raw: b[start:end]}, nil
}
// streamLength reads /Length, following an indirect reference when it can.
func (p *parser) streamLength(d Dict) (int, bool) {
o, err := Resolve(d.Get("Length"), p.resolve)
if err != nil {
return 0, false
}
n, ok := ToInt(o)
if !ok || n < 0 {
return 0, false
}
return int(n), true
}
// endstreamAt reports the offset just past an endstream keyword that follows
// white-space at i, or -1 when something else is there.
func endstreamAt(b []byte, i int) int {
for i < len(b) && isSpace(b[i]) {
i++
}
if bytes.HasPrefix(b[i:], []byte("endstream")) {
return i + len("endstream")
}
return -1
}
// parseObject reads one object.
func (p *parser) parseObject() (Object, error) {
t, err := p.lex.next()
if err != nil {
return nil, err
}
return p.parseFrom(t)
}
// parseFrom reads the object that starts with an already-read token.
func (p *parser) parseFrom(t token) (Object, error) {
switch t.kind {
case tokEOF:
return nil, &SyntaxError{t.pos, "unexpected end of input"}
case tokInteger:
return p.maybeRef(t)
case tokReal:
return Real(t.f), nil
case tokString:
return String(t.text), nil
case tokName:
return Name(t.text), nil
case tokArrayOpen:
return p.parseArray()
case tokDictOpen:
return p.parseDict()
case tokKeyword:
switch string(t.text) {
case "true":
return Bool(true), nil
case "false":
return Bool(false), nil
case "null":
return Null{}, nil
}
return nil, &SyntaxError{t.pos, "unexpected keyword " + strconv.Quote(string(t.text))}
}
return nil, &SyntaxError{t.pos, "unexpected token"}
}
// maybeRef decides between the integer just read and the "N G R" that starts
// the same way. Only two tokens of lookahead separate them.
func (p *parser) maybeRef(t token) (Object, error) {
save := p.lex.pos
if t.i >= 0 {
if t2, err := p.lex.next(); err == nil && t2.kind == tokInteger && t2.i >= 0 {
if t3, err := p.lex.next(); err == nil && t3.kind == tokKeyword && string(t3.text) == "R" {
return Ref{Num: int(t.i), Gen: int(t2.i)}, nil
}
}
}
p.lex.pos = save
return Integer(t.i), nil
}
// parseArray reads the body of an array, the opening bracket already consumed.
func (p *parser) parseArray() (Object, error) {
arr := Array{}
for {
t, err := p.lex.next()
if err != nil {
return nil, err
}
switch t.kind {
case tokArrayClose:
return arr, nil
case tokEOF:
return nil, &SyntaxError{t.pos, "unterminated array"}
}
o, err := p.parseFrom(t)
if err != nil {
return nil, err
}
arr = append(arr, o)
}
}
// parseDict reads the body of a dictionary, "<<" already consumed.
func (p *parser) parseDict() (Object, error) {
d := Dict{}
for {
t, err := p.lex.next()
if err != nil {
return nil, err
}
switch t.kind {
case tokDictClose:
return d, nil
case tokEOF:
return nil, &SyntaxError{t.pos, "unterminated dictionary"}
case tokName:
default:
return nil, &SyntaxError{t.pos, "dictionary key is a " + t.kind.describe() + ", not a name"}
}
v, err := p.parseObject()
if err != nil {
return nil, err
}
d[Name(t.text)] = v
}
}
// describe names a token kind for an error message.
func (k tokKind) describe() string {
switch k {
case tokInteger, tokReal:
return "number"
case tokString:
return "string"
case tokArrayOpen, tokArrayClose:
return "bracket"
case tokDictOpen, tokDictClose:
return "dictionary delimiter"
case tokBraceOpen, tokBraceClose:
return "brace"
case tokKeyword:
return "keyword"
}
return "token"
}
+130
View File
@@ -0,0 +1,130 @@
package reader
import "fmt"
// applyPredictor undoes the PNG or TIFF predictor a Flate or LZW stream was
// filtered through. /Predictor 1, and an absent parameter dictionary, mean
// there is nothing to undo.
func applyPredictor(data []byte, parm Dict, r Resolver) ([]byte, error) {
pred := intParm(parm, "Predictor", 1, r)
if pred <= 1 {
return data, nil
}
colors := intParm(parm, "Colors", 1, r)
bpc := intParm(parm, "BitsPerComponent", 8, r)
columns := intParm(parm, "Columns", 1, r)
if colors < 1 || bpc < 1 || columns < 1 {
return nil, fmt.Errorf("reader: predictor: /Colors %d /BitsPerComponent %d /Columns %d", colors, bpc, columns)
}
switch {
case pred == 2:
return tiffPredictor(data, colors, bpc, columns)
case pred >= 10:
return pngPredictor(data, colors, bpc, columns)
}
return nil, fmt.Errorf("reader: predictor: /Predictor %d is not defined", pred)
}
// rowGeometry gives the bytes per row and the byte distance between a sample
// and the one to its left, both rounded up as the specification requires.
func rowGeometry(colors, bpc, columns int) (rowLen, bpp int) {
return (colors*bpc*columns + 7) / 8, (colors*bpc + 7) / 8
}
// pngPredictor undoes the per-row filters PNG defines, each row of the stream
// being preceded by its filter type byte.
func pngPredictor(data []byte, colors, bpc, columns int) ([]byte, error) {
rowLen, bpp := rowGeometry(colors, bpc, columns)
prev := make([]byte, rowLen)
cur := make([]byte, rowLen)
out := make([]byte, 0, len(data))
for i := 0; i < len(data); {
ft := data[i]
i++
n := copy(cur, data[i:])
// A damaged file can end mid-row; treat the missing bytes as zero
// rather than dropping the row.
clear(cur[n:])
i += n
switch ft {
case 0:
case 1:
for k := bpp; k < rowLen; k++ {
cur[k] += cur[k-bpp]
}
case 2:
for k := 0; k < rowLen; k++ {
cur[k] += prev[k]
}
case 3:
for k := 0; k < rowLen; k++ {
left := 0
if k >= bpp {
left = int(cur[k-bpp])
}
cur[k] += byte((left + int(prev[k])) / 2)
}
case 4:
for k := 0; k < rowLen; k++ {
var left, upLeft byte
if k >= bpp {
left, upLeft = cur[k-bpp], prev[k-bpp]
}
cur[k] += paeth(left, prev[k], upLeft)
}
default:
return nil, fmt.Errorf("reader: predictor: unknown PNG row filter %d", ft)
}
out = append(out, cur...)
copy(prev, cur)
}
return out, nil
}
// paeth is the PNG predictor of that name: the neighbour closest to a+b-c.
func paeth(a, b, c byte) byte {
p := int(a) + int(b) - int(c)
pa, pb, pc := abs(p-int(a)), abs(p-int(b)), abs(p-int(c))
if pa <= pb && pa <= pc {
return a
}
if pb <= pc {
return b
}
return c
}
// abs is the integer absolute value.
func abs(v int) int {
if v < 0 {
return -v
}
return v
}
// tiffPredictor undoes horizontal differencing. Only the 8- and 16-bit
// component depths are implemented, which is every one seen in practice; a
// sub-byte depth is reported rather than silently mis-decoded.
func tiffPredictor(data []byte, colors, bpc, columns int) ([]byte, error) {
if bpc != 8 && bpc != 16 {
return nil, fmt.Errorf("reader: predictor: TIFF differencing with /BitsPerComponent %d is not supported", bpc)
}
rowLen, bpp := rowGeometry(colors, bpc, columns)
out := append([]byte{}, data...)
for r := 0; r+rowLen <= len(out); r += rowLen {
row := out[r : r+rowLen]
if bpc == 8 {
for k := bpp; k < rowLen; k++ {
row[k] += row[k-bpp]
}
continue
}
for k := bpp; k+1 < rowLen; k += 2 {
v := uint16(row[k])<<8 | uint16(row[k+1])
p := uint16(row[k-bpp])<<8 | uint16(row[k-bpp+1])
v += p
row[k], row[k+1] = byte(v>>8), byte(v)
}
}
return out, nil
}
+339
View File
@@ -0,0 +1,339 @@
package reader
import (
"bytes"
"cmp"
"fmt"
"slices"
)
// repair rebuilds the cross-reference information by reading the file itself
// rather than what it says about itself: every "N G obj" header in order, the
// later definition of an object number winning, plus whatever trailer or
// catalogue can be found. Damaged files are the normal case for a reader, not
// an exceptional one.
func (d *Document) repair() error {
d.repaired = true
d.cache = map[int]Object{}
d.objStms = map[int]map[int]Object{}
d.pages = nil
d.xref = map[int]xrefEntry{}
for _, h := range scanObjectHeaders(d.buf) {
// A later definition wins, so the entry is overwritten unconditionally
// rather than through setEntry.
d.xref[h.num] = xrefEntry{kind: 'n', offset: int64(h.offset), gen: h.gen}
}
if len(d.xref) == 0 {
return fmt.Errorf("reader: the file holds no indirect objects")
}
// A rebuild reads object streams, and in an encrypted file it cannot read
// one until the file key is known. The trailers a scan can see are where
// /Encrypt is named, so the key is established before the streams are
// indexed rather than after: otherwise a file whose catalogue lives in an
// encrypted object stream is rebuilt from bytes nobody can read, and the
// rebuild fails for a reason that has nothing to do with the file.
if err := d.establishDecryption(); err != nil {
return err
}
// Objects held in object streams are invisible to a header scan; take them
// from every object stream the scan did find.
d.indexObjectStreams()
d.loadRepairedTrailer()
if d.trailer == nil {
return fmt.Errorf("reader: no document catalogue found")
}
return nil
}
// establishDecryption derives the file key from a trailer the scan can see, for
// a rebuild that runs before the cross-reference tables could name /Encrypt.
// A file that is not encrypted, or whose key is already known, needs nothing.
func (d *Document) establishDecryption() error {
if d.decrypt != nil || !bytes.Contains(d.buf, []byte("/Encrypt")) {
// /Encrypt can only be named by a trailer, and no trailer is
// compressed, so a file that does not hold those bytes anywhere is not
// encrypted. Saying so cheaply keeps the scan below off the common path.
return nil
}
previous := d.trailer
for _, tr := range d.trailerCandidates() {
if tr == nil || tr.Get("Encrypt").Kind() == KindNull {
continue
}
d.trailer = tr
err := d.setUpDecryption(d.password)
d.trailer = previous
if err != nil {
return err
}
// Whatever was read while the key was unknown was read wrong.
d.cache = map[int]Object{}
return nil
}
return nil
}
// trailerCandidates lists every dictionary that could carry trailer entries:
// what the tables managed to read, then the file's trailer keywords, then the
// dictionary of each cross-reference stream.
//
// The last of those is not optional. A file written with cross-reference
// streams has no trailer keyword anywhere in it, and /Encrypt then exists only
// in a /Type /XRef stream's dictionary — which is the one stream a PDF never
// encrypts, precisely so that it can be read before the key is known.
//
// The cross-reference streams come highest object number first, an incremental
// update having appended both its new objects and its new table to the file.
func (d *Document) trailerCandidates() []Dict {
out := append([]Dict{d.trailer}, scanTrailers(d.buf)...)
nums := make([]int, 0, len(d.xref))
for num := range d.xref {
nums = append(nums, num)
}
slices.Sort(nums)
var streams []Dict
for _, num := range nums {
// An error here leaves a nil object, which is not a stream; the
// rebuild has already been told about anything unreadable.
o, _ := d.Get(Ref{Num: num})
s, ok := ToStream(o)
if !ok {
continue
}
if t, ok := ToName(s.Dict.Get("Type")); ok && t == "XRef" {
streams = append(streams, s.Dict)
}
}
slices.Reverse(streams)
return append(out, streams...)
}
// loadRepairedTrailer finds a trailer that leads to a catalogue: the file's own
// trailer dictionaries first, newest first; then any object that calls itself a
// catalogue; and failing both, a catalogue built over whatever pages survive.
func (d *Document) loadRepairedTrailer() {
previous := d.trailer
for _, tr := range scanTrailers(d.buf) {
d.trailer = tr
if _, err := d.Catalog(); err == nil {
return
}
}
if previous != nil {
d.trailer = previous
if _, err := d.Catalog(); err == nil {
return
}
}
d.trailer = nil
for _, num := range d.objectsOfType("Catalog") {
d.trailer = Dict{"Root": Ref{Num: num}}
if _, err := d.Catalog(); err == nil {
return
}
d.trailer = nil
}
d.synthesiseCatalogue()
}
// objectsOfType lists, in object-number order, the objects whose /Type is the
// given name.
func (d *Document) objectsOfType(want Name) []int {
// The objects that exist are walked, in order, rather than every number
// up to the largest of them. A file may name any object number it likes:
// a 219-byte one in the wild declares object 2147483647, and counting up
// to that is two thousand million map lookups for three objects — which
// is a quarter of a minute of somebody's afternoon for a file that fits
// in a tweet.
nums := make([]int, 0, len(d.xref))
for num := range d.xref {
nums = append(nums, num)
}
slices.Sort(nums)
var out []int
for _, num := range nums {
o, err := d.Get(Ref{Num: num})
if err != nil {
continue
}
dict, ok := ToDict(o)
if !ok {
continue
}
if t, ok := ToName(dict.Get("Type")); ok && t == want {
out = append(out, num)
}
}
return out
}
// synthesiseCatalogue is the last resort for a file whose catalogue and page
// tree did not survive: build one over whatever objects still call themselves
// pages. A truncated file that kept its pages is worth opening; one that kept
// none is not a document.
func (d *Document) synthesiseCatalogue() {
var kids Array
for _, num := range d.objectsOfType("Page") {
kids = append(kids, Ref{Num: num})
}
if len(kids) == 0 {
return
}
d.trailer = Dict{"Root": Dict{
"Type": Name("Catalog"),
"Pages": Dict{
"Type": Name("Pages"),
"Kids": kids,
"Count": Integer(len(kids)),
},
}}
}
// indexObjectStreams adds the objects held inside every object stream the scan
// found, without overwriting an object written directly in the file.
func (d *Document) indexObjectStreams() {
// Two object streams in a damaged file may both claim the same object
// number — a partial rewrite leaves the old one in place beside the new —
// and the loop below lets the first stream walked define it. Walking
// d.xref in map order therefore made the same file read differently from
// one run to the next.
//
// They are walked latest in the file first, which is the rule the header
// scan above already applies to objects written directly: a later
// definition wins, an incremental update having appended it.
nums := make([]int, 0, len(d.xref))
for num := range d.xref {
nums = append(nums, num)
}
slices.Sort(nums)
type located struct {
offset int64
num int
}
var found []located
for _, num := range nums {
o, err := d.Get(Ref{Num: num})
if err != nil {
continue
}
s, ok := ToStream(o)
if !ok {
continue
}
if t, ok := ToName(s.Dict.Get("Type")); ok && t == "ObjStm" {
found = append(found, located{d.xref[num].offset, num})
}
}
slices.SortFunc(found, func(a, b located) int { return cmp.Compare(b.offset, a.offset) })
streams := make([]int, 0, len(found))
for _, f := range found {
streams = append(streams, f.num)
}
for _, num := range streams {
// The stream object is already cached by the pass above, so this
// cannot fail; an empty result simply contributes nothing.
objs, _ := d.objectStream(num)
for objNum, obj := range objs {
if _, ok := d.xref[objNum]; ok {
continue
}
d.xref[objNum] = xrefEntry{kind: 'o', strmNum: num}
d.cache[objNum] = obj
}
}
}
// An objectHeader is one "N G obj" found by scanning.
type objectHeader struct {
num, gen, offset int
}
// scanObjectHeaders finds every indirect object header in the file, in the
// order they appear.
func scanObjectHeaders(b []byte) []objectHeader {
var out []objectHeader
for i := 0; i+3 <= len(b); {
j := bytes.Index(b[i:], []byte("obj"))
if j < 0 {
return out
}
at := i + j
i = at + 3
if at+3 < len(b) && isRegular(b[at+3]) {
continue
}
h, ok := headerBefore(b, at)
if !ok {
continue
}
out = append(out, h)
}
return out
}
// headerBefore reads the "N G" that must precede an obj keyword at at.
func headerBefore(b []byte, at int) (objectHeader, bool) {
p := skipSpaceBack(b, at)
if p == at {
return objectHeader{}, false
}
gen, p, ok := digitsBack(b, p)
if !ok {
return objectHeader{}, false
}
q := skipSpaceBack(b, p)
if q == p {
return objectHeader{}, false
}
num, p, ok := digitsBack(b, q)
if !ok {
return objectHeader{}, false
}
return objectHeader{num: num, gen: gen, offset: p}, true
}
// skipSpaceBack walks back over white-space and returns the new position.
func skipSpaceBack(b []byte, p int) int {
for p > 0 && isSpace(b[p-1]) {
p--
}
return p
}
// digitsBack reads a decimal number that ends at p, going backwards.
func digitsBack(b []byte, p int) (int, int, bool) {
end := p
for p > 0 && b[p-1] >= '0' && b[p-1] <= '9' {
p--
}
if p == end || end-p > 10 {
return 0, p, false
}
v := 0
for _, c := range b[p:end] {
v = v*10 + int(c-'0')
}
return v, p, true
}
// scanTrailers finds the file's trailer dictionaries, newest first.
func scanTrailers(b []byte) []Dict {
var out []Dict
for i := len(b); i > 0; {
j := bytes.LastIndex(b[:i], []byte("trailer"))
if j < 0 {
break
}
i = j
o, _, err := ParseObject(b[j+len("trailer"):])
if err != nil {
continue
}
if tr, ok := ToDict(o); ok {
out = append(out, tr)
}
}
return out
}
+432
View File
@@ -0,0 +1,432 @@
package reader
import (
"bytes"
"fmt"
"math"
"sort"
"strconv"
)
// AppendObject writes an object in PDF syntax, appending to dst. Dictionary
// keys are written in order, so the same object always produces the same
// bytes — which is what makes a rewritten file comparable with the one it came
// from.
func AppendObject(dst []byte, o Object) []byte {
switch v := o.(type) {
case nil:
return append(dst, "null"...)
case Null:
return append(dst, "null"...)
case Bool:
if v {
return append(dst, "true"...)
}
return append(dst, "false"...)
case Integer:
return strconv.AppendInt(dst, int64(v), 10)
case Real:
return appendReal(dst, float64(v))
case String:
return appendString(dst, v)
case Name:
return appendName(dst, v)
case Ref:
dst = strconv.AppendInt(dst, int64(v.Num), 10)
dst = append(dst, ' ')
dst = strconv.AppendInt(dst, int64(v.Gen), 10)
return append(dst, " R"...)
case Array:
dst = append(dst, '[')
for i, e := range v {
if i > 0 {
dst = append(dst, ' ')
}
dst = AppendObject(dst, e)
}
return append(dst, ']')
case Dict:
return appendDict(dst, v)
case *Stream:
return appendStream(dst, v)
}
return append(dst, "null"...)
}
// FormatObject renders an object in PDF syntax.
func FormatObject(o Object) []byte { return AppendObject(nil, o) }
// appendReal writes a number the way PDF spells one: no exponent, since the
// format has no notation for it, and no infinity or not-a-number, which it
// cannot represent at all.
func appendReal(dst []byte, f float64) []byte {
if math.IsNaN(f) || math.IsInf(f, 0) {
return append(dst, '0')
}
if f == 0 {
// Negative zero is legal and meaningless, and writing it back out
// would be read as the integer zero, so a rewrite would not be a
// fixpoint. Producers do emit it.
return append(dst, '0')
}
return strconv.AppendFloat(dst, f, 'f', -1, 64)
}
// appendString writes a string in whichever of the two forms a PDF allows is
// shorter. A literal string spells an unprintable byte as a four-character
// octal escape, so text in UTF-16 — which is how a PDF says anything that is
// not Latin-1 — comes out four times its length; the hex form costs two
// characters a byte whatever the byte is.
func appendString(dst []byte, s []byte) []byte {
if literalStringCost(s) > 2*len(s)+2 {
return appendHexString(dst, s)
}
return appendLiteralString(dst, s)
}
// literalStringCost is how many characters the literal form would take.
func literalStringCost(s []byte) int {
n := 2
for _, c := range s {
switch {
case c == '(' || c == ')' || c == '\\' || c == '\n' || c == '\r' || c == '\t':
n += 2
case c < 32 || c > 126:
n += 4
default:
n++
}
}
return n
}
// appendHexString writes the <hex> form.
func appendHexString(dst []byte, s []byte) []byte {
const hex = "0123456789ABCDEF"
dst = append(dst, '<')
for _, c := range s {
dst = append(dst, hex[c>>4], hex[c&15])
}
return append(dst, '>')
}
// appendLiteralString writes the (parenthesised) form, escaping only what has
// to be escaped and rendering anything unprintable as an octal escape.
func appendLiteralString(dst []byte, s []byte) []byte {
dst = append(dst, '(')
for _, c := range s {
switch {
case c == '(' || c == ')' || c == '\\':
dst = append(dst, '\\', c)
case c == '\n':
dst = append(dst, '\\', 'n')
case c == '\r':
dst = append(dst, '\\', 'r')
case c == '\t':
dst = append(dst, '\\', 't')
case c < 32 || c > 126:
dst = append(dst, '\\')
dst = append(dst, '0'+c>>6&7, '0'+c>>3&7, '0'+c&7)
default:
dst = append(dst, c)
}
}
return append(dst, ')')
}
// appendName writes a name, escaping every byte that may not appear in one.
func appendName(dst []byte, n Name) []byte {
dst = append(dst, '/')
const hex = "0123456789ABCDEF"
for i := 0; i < len(n); i++ {
c := n[i]
if !isRegular(c) || c == '#' || c < '!' || c > '~' {
dst = append(dst, '#', hex[c>>4], hex[c&15])
continue
}
dst = append(dst, c)
}
return dst
}
// appendDict writes a dictionary with its keys in order.
func appendDict(dst []byte, d Dict) []byte {
keys := make([]string, 0, len(d))
for k := range d {
keys = append(keys, string(k))
}
sort.Strings(keys)
dst = append(dst, "<<"...)
for _, k := range keys {
dst = appendName(dst, Name(k))
dst = append(dst, ' ')
dst = AppendObject(dst, d[Name(k)])
dst = append(dst, ' ')
}
if len(keys) > 0 {
dst = dst[:len(dst)-1]
}
return append(dst, ">>"...)
}
// appendStream writes a stream, its /Length rewritten to the length of the
// data actually being written.
func appendStream(dst []byte, s *Stream) []byte {
d := Dict{}
for k, v := range s.Dict {
d[k] = v
}
d["Length"] = Integer(len(s.Raw))
dst = appendDict(dst, d)
dst = append(dst, "\nstream\n"...)
dst = append(dst, s.Raw...)
return append(dst, "\nendstream"...)
}
// A Writer builds a PDF file out of objects. Numbers are handed out by
// [Writer.Reserve], objects are written with [Writer.Put], and [Writer.Finish]
// adds the cross-reference table and the trailer.
type Writer struct {
buf bytes.Buffer
offsets map[int]int
packed map[int]packedAt
pending []pendingObject
seen map[int]bool
next int
copied map[*Document]map[int]Ref
err error
pack bool
encrypt *encrypter
}
// NewWriter starts a file with the given version in its header, "1.7" when the
// version is empty. Objects are written one after another and the file ends in
// a cross-reference table, which every reader that has ever existed can read.
func NewWriter(version string) *Writer { return newWriter(version, false) }
// NewPackedWriter starts a file that puts what it can into compressed object
// streams and ends in a cross-reference stream. That is smaller — usually by
// a lot, since a file is mostly small dictionaries — and has been readable
// since PDF 1.5, which is 2003; the version in the header is raised to 1.5 if
// it is lower.
func NewPackedWriter(version string) *Writer { return newWriter(version, true) }
// newWriter starts a file of either kind.
func newWriter(version string, pack bool) *Writer {
if version == "" {
version = "1.7"
}
if pack && version < "1.5" {
version = "1.5"
}
w := &Writer{
offsets: map[int]int{},
packed: map[int]packedAt{},
seen: map[int]bool{},
next: 1,
copied: map[*Document]map[int]Ref{},
pack: pack,
}
fmt.Fprintf(&w.buf, "%%PDF-%s\n", version)
// The four bytes above 127 tell every tool downstream that this file is
// not text, which is what the specification asks for.
w.buf.Write([]byte{'%', 0xE2, 0xE3, 0xCF, 0xD3, '\n'})
return w
}
// Reserve hands out an object number that nothing has been written to yet.
func (w *Writer) Reserve() Ref {
r := Ref{Num: w.next}
w.next++
return r
}
// Put writes an object under a reserved number.
func (w *Writer) Put(ref Ref, o Object) {
if w.seen[ref.Num] {
w.note(fmt.Errorf("reader: object %d written twice", ref.Num))
return
}
w.seen[ref.Num] = true
if ref.Num >= w.next {
w.next = ref.Num + 1
}
// A stream cannot go inside another stream, and only generation zero may
// be packed; everything else can wait to be gathered up at the end.
if _, isStream := o.(*Stream); w.pack && !isStream && ref.Gen == 0 {
w.pending = append(w.pending, pendingObject{ref: ref, obj: o})
return
}
w.writeInline(ref, o)
}
// writeInline writes an object where it stands in the file, compressed and
// then encrypted — in that order, since encrypted bytes do not compress.
func (w *Writer) writeInline(ref Ref, o Object) {
o = w.compressStream(o)
if w.encrypt != nil {
o = w.encrypt.apply(ref.Num, ref.Gen, o)
}
w.emit(ref, o)
}
// writeInlineRaw writes an object that must not be encrypted: the /Encrypt
// dictionary itself, and the cross-reference stream, neither of which a
// reader could decrypt, since it needs them to work out how.
func (w *Writer) writeInlineRaw(ref Ref, o Object) {
w.emit(ref, w.compressStream(o))
}
// compressStream deflates a stream that arrives with no filter of its own,
// when the file is being written to be small.
func (w *Writer) compressStream(o Object) Object {
s, ok := o.(*Stream)
if !ok || !w.pack || s.Dict.Get("Filter").Kind() != KindNull {
return o
}
// Nothing this package generated arrives compressed.
out := &Stream{Dict: cloneDict(s.Dict), Raw: flateCompress(s.Raw)}
out.Dict["Filter"] = Name("FlateDecode")
return out
}
// emit writes an object and remembers where it went.
func (w *Writer) emit(ref Ref, o Object) {
w.offsets[ref.Num] = w.buf.Len()
fmt.Fprintf(&w.buf, "%d %d obj\n", ref.Num, ref.Gen)
w.buf.Write(AppendObject(nil, o))
w.buf.WriteString("\nendobj\n")
}
// cloneDict copies a dictionary one level deep.
func cloneDict(d Dict) Dict {
out := Dict{}
for k, v := range d {
out[k] = v
}
return out
}
// Add reserves a number, writes the object under it and returns the reference.
func (w *Writer) Add(o Object) Ref {
ref := w.Reserve()
w.Put(ref, o)
return ref
}
// Err reports the first thing that went wrong while building the file.
func (w *Writer) Err() error { return w.err }
// note keeps the first error.
func (w *Writer) note(err error) {
if w.err == nil {
w.err = err
}
}
// Finish writes the cross-reference table and the trailer and returns the
// file. /Size is filled in; the caller supplies /Root and whatever else the
// trailer needs.
func (w *Writer) Finish(trailer Dict) ([]byte, error) {
// The caller's dictionary is left as it was; what a file needs to say
// about its own encryption is added to a copy.
trailer = cloneDict(trailer)
for k, v := range w.writeEncryptDict() {
trailer[k] = v
}
if w.pack {
return w.finishWithXrefStream(trailer)
}
high := 0
for num := range w.offsets {
if num > high {
high = num
}
}
start := w.buf.Len()
fmt.Fprintf(&w.buf, "xref\n0 %d\n0000000000 65535 f \n", high+1)
for num := 1; num <= high; num++ {
off, ok := w.offsets[num]
if !ok {
w.buf.WriteString("0000000000 65535 f \n")
continue
}
fmt.Fprintf(&w.buf, "%010d 00000 n \n", off)
}
out := Dict{}
for k, v := range trailer {
out[k] = v
}
out["Size"] = Integer(high + 1)
w.buf.WriteString("trailer\n")
w.buf.Write(AppendObject(nil, out))
fmt.Fprintf(&w.buf, "\nstartxref\n%d\n%%%%EOF\n", start)
if w.encrypt != nil && w.encrypt.err != nil {
w.note(w.encrypt.err)
}
if w.err != nil {
return nil, w.err
}
return w.buf.Bytes(), nil
}
// Copy writes an object from a document into this file, following every
// indirect reference it reaches and renumbering as it goes, so that objects
// from several documents can live side by side. An object already copied from
// the same document keeps the number it was given.
func (w *Writer) Copy(d *Document, o Object) Object {
seen, ok := w.copied[d]
if !ok {
seen = map[int]Ref{}
w.copied[d] = seen
}
return w.copyObject(d, seen, o, 0)
}
// copyObject is Copy's recursion.
func (w *Writer) copyObject(d *Document, seen map[int]Ref, o Object, depth int) Object {
if depth > maxCopyDepth {
w.note(fmt.Errorf("reader: an object graph nested deeper than %d was truncated", maxCopyDepth))
return Null{}
}
switch v := o.(type) {
case Ref:
if to, ok := seen[v.Num]; ok {
return to
}
// The number is reserved before the object is read, so a reference
// back to an ancestor finds it rather than recursing.
to := w.Reserve()
seen[v.Num] = to
src, err := d.Get(v)
if err != nil {
w.note(err)
src = Null{}
}
w.Put(to, w.copyObject(d, seen, src, depth+1))
return to
case Array:
out := make(Array, len(v))
for i, e := range v {
out[i] = w.copyObject(d, seen, e, depth+1)
}
return out
case Dict:
out := Dict{}
for k, e := range v {
out[k] = w.copyObject(d, seen, e, depth+1)
}
return out
case *Stream:
out := &Stream{Dict: Dict{}, Raw: v.Raw}
for k, e := range v.Dict {
out.Dict[k] = w.copyObject(d, seen, e, depth+1)
}
return out
}
return o
}
// maxCopyDepth bounds a copy, since a direct object may nest arbitrarily even
// when no reference repeats.
const maxCopyDepth = 256
+303
View File
@@ -0,0 +1,303 @@
package reader
import (
"bytes"
"fmt"
)
// An xrefEntry says where object N lives.
type xrefEntry struct {
kind byte // 'n' at a byte offset, 'o' inside an object stream, 'f' free
offset int64 // kind 'n'
gen int // kind 'n'
strmNum int // kind 'o': the object stream's own number
strmIdx int // kind 'o': the index within it
}
// maxXrefSections bounds a /Prev chain, which a file is free to make a cycle.
const maxXrefSections = 1024
// startxrefWindow is how far back from the end the startxref keyword is looked
// for. The specification allows only a short tail; real files sometimes append
// junk, so the window is generous.
const startxrefWindow = 4096
// findStartxref reads the offset the file's last startxref names.
func (d *Document) findStartxref() (int64, error) {
from := len(d.buf) - startxrefWindow
if from < 0 {
from = 0
}
i := bytes.LastIndex(d.buf[from:], []byte("startxref"))
if i < 0 {
return 0, fmt.Errorf("reader: no startxref in the last %d bytes", startxrefWindow)
}
l := &lexer{buf: d.buf, pos: from + i + len("startxref")}
t, err := l.next()
if err != nil {
return 0, err
}
if t.kind != tokInteger || t.i < 0 || t.i >= int64(len(d.buf)) {
return 0, &SyntaxError{t.pos, "startxref does not name an offset inside the file"}
}
return t.i, nil
}
// loadXref follows the chain of cross-reference sections from the last one
// back through /Prev, taking the newest definition of every object and of
// every trailer entry.
func (d *Document) loadXref() error {
off, err := d.findStartxref()
if err != nil {
return err
}
seen := map[int64]bool{}
for n := 0; off > 0 && n < maxXrefSections; n++ {
if seen[off] {
return fmt.Errorf("reader: cross-reference sections form a cycle at offset %d", off)
}
seen[off] = true
tr, err := d.readXrefSection(off)
if err != nil {
return err
}
d.mergeTrailer(tr)
// A hybrid file keeps a second, stream-shaped section for the objects
// its table deliberately hides from old readers.
if x, ok := ToInt(tr.Get("XRefStm")); ok && x > 0 && !seen[x] {
seen[x] = true
if hy, err := d.readXrefSection(x); err == nil {
d.mergeTrailer(hy)
}
}
p, ok := ToInt(tr.Get("Prev"))
if !ok || p <= 0 {
break
}
off = p
}
if len(d.xref) == 0 {
return fmt.Errorf("reader: the cross-reference table is empty")
}
return nil
}
// mergeTrailer keeps the first value seen for each key, sections being read
// newest first.
func (d *Document) mergeTrailer(tr Dict) {
if d.trailer == nil {
d.trailer = Dict{}
}
for k, v := range tr {
if _, ok := d.trailer[k]; !ok {
d.trailer[k] = v
}
}
}
// setEntry records an entry unless a newer section already defined the object.
func (d *Document) setEntry(num int, e xrefEntry) {
if num < 0 {
return
}
if _, ok := d.xref[num]; !ok {
d.xref[num] = e
}
}
// readXrefSection reads whichever of the two forms is at off and returns its
// trailer dictionary.
func (d *Document) readXrefSection(off int64) (Dict, error) {
if off < 0 || off >= int64(len(d.buf)) {
return nil, fmt.Errorf("reader: cross-reference offset %d is outside the file", off)
}
l := &lexer{buf: d.buf, pos: int(off)}
t, err := l.next()
if err != nil {
return nil, err
}
if t.kind == tokKeyword && string(t.text) == "xref" {
return d.readXrefTable(l)
}
_, obj, _, err := ParseIndirectObject(d.buf[off:], nil)
if err != nil {
return nil, err
}
s, ok := ToStream(obj)
if !ok {
return nil, &SyntaxError{int(off), "neither an xref table nor an xref stream"}
}
return d.readXrefStream(s)
}
// readXrefTable reads the classic subsection form, the xref keyword already
// consumed, up to and including its trailer dictionary.
func (d *Document) readXrefTable(l *lexer) (Dict, error) {
p := &parser{lex: *l}
for {
save := p.lex.pos
t, err := p.lex.next()
if err != nil {
return nil, err
}
if t.kind == tokKeyword && string(t.text) == "trailer" {
o, err := p.parseObject()
if err != nil {
return nil, err
}
tr, ok := ToDict(o)
if !ok {
return nil, &SyntaxError{save, "the trailer is not a dictionary"}
}
return tr, nil
}
if t.kind != tokInteger {
return nil, &SyntaxError{t.pos, "a cross-reference subsection header was expected"}
}
start := int(t.i)
count, err := p.expectUint("a subsection entry count")
if err != nil {
return nil, err
}
for i := 0; i < count; i++ {
if err := d.readXrefTableEntry(p, start+i); err != nil {
return nil, err
}
}
}
}
// readXrefTableEntry reads one "offset generation n|f" line.
func (d *Document) readXrefTableEntry(p *parser, num int) error {
t1, err := p.lex.next()
if err != nil {
return err
}
if t1.kind != tokInteger {
return &SyntaxError{t1.pos, "a cross-reference entry offset was expected"}
}
t2, err := p.lex.next()
if err != nil {
return err
}
if t2.kind != tokInteger {
return &SyntaxError{t2.pos, "a cross-reference entry generation was expected"}
}
t3, err := p.lex.next()
if err != nil {
return err
}
if t3.kind != tokKeyword || len(t3.text) != 1 || (t3.text[0] != 'n' && t3.text[0] != 'f') {
return &SyntaxError{t3.pos, "a cross-reference entry must end in n or f"}
}
if t3.text[0] == 'n' {
d.setEntry(num, xrefEntry{kind: 'n', offset: t1.i, gen: int(t2.i)})
return nil
}
d.setEntry(num, xrefEntry{kind: 'f'})
return nil
}
// readXrefStream reads the /Type /XRef stream form. Its own data is never
// encrypted, so it is decoded without the document's decryptor.
func (d *Document) readXrefStream(s *Stream) (Dict, error) {
data, img, err := Decode(s.Dict, s.Raw, nil)
if err != nil {
return nil, err
}
if img != "" {
return nil, fmt.Errorf("reader: the cross-reference stream is filtered as an image")
}
widths, err := xrefWidths(s.Dict)
if err != nil {
return nil, err
}
index, err := xrefIndex(s.Dict)
if err != nil {
return nil, err
}
row := widths[0] + widths[1] + widths[2]
if row == 0 {
return nil, fmt.Errorf("reader: the cross-reference stream's /W entries are all zero")
}
pos := 0
for i := 0; i+1 < len(index); i += 2 {
for k := 0; k < index[i+1]; k++ {
if pos+row > len(data) {
// A section that claims more rows than it carries still
// contributes the rows it does carry.
return s.Dict, nil
}
f1 := xrefField(data[pos:], widths[0], 1)
f2 := xrefField(data[pos+widths[0]:], widths[1], 0)
f3 := xrefField(data[pos+widths[0]+widths[1]:], widths[2], 0)
pos += row
d.storeXrefStreamEntry(index[i]+k, f1, f2, f3)
}
}
return s.Dict, nil
}
// storeXrefStreamEntry turns one decoded row into an entry.
func (d *Document) storeXrefStreamEntry(num int, f1, f2, f3 int64) {
switch f1 {
case 1:
d.setEntry(num, xrefEntry{kind: 'n', offset: f2, gen: int(f3)})
case 2:
d.setEntry(num, xrefEntry{kind: 'o', strmNum: int(f2), strmIdx: int(f3)})
default:
d.setEntry(num, xrefEntry{kind: 'f'})
}
}
// xrefWidths reads /W, which says how many bytes each of the three fields
// occupies.
func xrefWidths(d Dict) ([3]int, error) {
var w [3]int
arr, ok := ToArray(d.Get("W"))
if !ok || len(arr) < 3 {
return w, fmt.Errorf("reader: the cross-reference stream has no usable /W")
}
for i := 0; i < 3; i++ {
n, ok := ToInt(arr[i])
if !ok || n < 0 || n > 8 {
return w, fmt.Errorf("reader: /W[%d] is not a byte width", i)
}
w[i] = int(n)
}
return w, nil
}
// xrefIndex reads /Index, defaulting to the whole range /Size describes.
func xrefIndex(d Dict) ([]int, error) {
arr, ok := ToArray(d.Get("Index"))
if !ok {
size, ok := ToInt(d.Get("Size"))
if !ok || size < 0 {
return nil, fmt.Errorf("reader: the cross-reference stream has neither /Index nor /Size")
}
return []int{0, int(size)}, nil
}
out := make([]int, 0, len(arr))
for _, e := range arr {
n, ok := ToInt(e)
if !ok || n < 0 {
return nil, fmt.Errorf("reader: /Index holds a value that is not a count")
}
out = append(out, int(n))
}
return out, nil
}
// xrefField reads one big-endian field of the given width, or its default when
// the width is zero.
func xrefField(b []byte, width int, def int64) int64 {
if width == 0 {
return def
}
v := int64(0)
for i := 0; i < width; i++ {
v = v<<8 | int64(b[i])
}
return v
}
+12
View File
@@ -27,6 +27,18 @@ github.com/cespare/xxhash/v2
# github.com/dustin/go-humanize v1.0.1
## explicit; go 1.16
github.com/dustin/go-humanize
# github.com/go-opentype/opentype v0.12.0
## explicit; go 1.26.4
github.com/go-opentype/opentype
# github.com/go-pdfkit/extract v0.3.0
## explicit; go 1.26.4
github.com/go-pdfkit/extract
# github.com/go-pdfkit/pdffont v0.3.0
## explicit; go 1.26.4
github.com/go-pdfkit/pdffont
# github.com/go-pdfkit/reader v0.6.0
## explicit; go 1.26.4
github.com/go-pdfkit/reader
# github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c
## explicit; go 1.20
github.com/go-shiori/dom