golang-image/font/sfnt/sfnt.go
Nigel Tao d835a09709 font/sfnt: add ErrColoredGlyph.
Also add tests for the Noto proprietary fonts. Prior to this commit,
NotoColorEmoji.ttf was unsupported. It's still not well supported, but
the error message returned is now more informative.

Change-Id: I61a3301d7f2458a4b838eb1de7a73d6472e3486f
Reviewed-on: https://go-review.googlesource.com/43694
Reviewed-by: David Crawshaw <crawshaw@golang.org>
2017-05-23 02:17:49 +00:00

1504 lines
46 KiB
Go

// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate go run gen.go
// Package sfnt implements a decoder for SFNT font file formats, including
// TrueType and OpenType.
package sfnt // import "golang.org/x/image/font/sfnt"
// This implementation was written primarily to the
// https://www.microsoft.com/en-us/Typography/OpenTypeSpecification.aspx
// specification. Additional documentation is at
// http://developer.apple.com/fonts/TTRefMan/
//
// The pyftinspect tool from https://github.com/fonttools/fonttools is useful
// for inspecting SFNT fonts.
//
// The ttfdump tool is also useful. For example:
// ttfdump -t cmap ../testdata/CFFTest.otf dump.txt
import (
"errors"
"io"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
"golang.org/x/text/encoding/charmap"
)
// These constants are not part of the specifications, but are limitations used
// by this implementation.
const (
// This value is arbitrary, but defends against parsing malicious font
// files causing excessive memory allocations. For reference, Adobe's
// SourceHanSansSC-Regular.otf has 65535 glyphs and:
// - its format-4 cmap table has 1581 segments.
// - its format-12 cmap table has 16498 segments.
//
// TODO: eliminate this constraint? If the cmap table is very large, load
// some or all of it lazily (at the time Font.GlyphIndex is called) instead
// of all of it eagerly (at the time Font.initialize is called), while
// keeping an upper bound on the memory used? This will make the code in
// cmap.go more complicated, considering that all of the Font methods are
// safe to call concurrently, as long as each call has a different *Buffer.
maxCmapSegments = 20000
// TODO: similarly, load subroutine locations lazily. Adobe's
// SourceHanSansSC-Regular.otf has up to 30000 subroutines.
maxNumSubroutines = 40000
maxCompoundRecursionDepth = 8
maxCompoundStackSize = 64
maxGlyphDataLength = 64 * 1024
maxHintBits = 256
maxNumFontDicts = 256
maxNumFonts = 256
maxNumTables = 256
maxRealNumberStrLen = 64 // Maximum length in bytes of the "-123.456E-7" representation.
// (maxTableOffset + maxTableLength) will not overflow an int32.
maxTableLength = 1 << 29
maxTableOffset = 1 << 29
)
var (
// ErrColoredGlyph indicates that the requested glyph is not a monochrome
// vector glyph, such as a colored (bitmap or vector) emoji glyph.
ErrColoredGlyph = errors.New("sfnt: colored glyph")
// ErrNotFound indicates that the requested value was not found.
ErrNotFound = errors.New("sfnt: not found")
errInvalidBounds = errors.New("sfnt: invalid bounds")
errInvalidCFFTable = errors.New("sfnt: invalid CFF table")
errInvalidCmapTable = errors.New("sfnt: invalid cmap table")
errInvalidDfont = errors.New("sfnt: invalid dfont")
errInvalidFont = errors.New("sfnt: invalid font")
errInvalidFontCollection = errors.New("sfnt: invalid font collection")
errInvalidGlyphData = errors.New("sfnt: invalid glyph data")
errInvalidGlyphDataLength = errors.New("sfnt: invalid glyph data length")
errInvalidHeadTable = errors.New("sfnt: invalid head table")
errInvalidHheaTable = errors.New("sfnt: invalid hhea table")
errInvalidHmtxTable = errors.New("sfnt: invalid hmtx table")
errInvalidKernTable = errors.New("sfnt: invalid kern table")
errInvalidLocaTable = errors.New("sfnt: invalid loca table")
errInvalidLocationData = errors.New("sfnt: invalid location data")
errInvalidMaxpTable = errors.New("sfnt: invalid maxp table")
errInvalidNameTable = errors.New("sfnt: invalid name table")
errInvalidPostTable = errors.New("sfnt: invalid post table")
errInvalidSingleFont = errors.New("sfnt: invalid single font (data is a font collection)")
errInvalidSourceData = errors.New("sfnt: invalid source data")
errInvalidTableOffset = errors.New("sfnt: invalid table offset")
errInvalidTableTagOrder = errors.New("sfnt: invalid table tag order")
errInvalidUCS2String = errors.New("sfnt: invalid UCS-2 string")
errUnsupportedCFFFDSelectTable = errors.New("sfnt: unsupported CFF FDSelect table")
errUnsupportedCFFVersion = errors.New("sfnt: unsupported CFF version")
errUnsupportedCmapEncodings = errors.New("sfnt: unsupported cmap encodings")
errUnsupportedCompoundGlyph = errors.New("sfnt: unsupported compound glyph")
errUnsupportedGlyphDataLength = errors.New("sfnt: unsupported glyph data length")
errUnsupportedKernTable = errors.New("sfnt: unsupported kern table")
errUnsupportedRealNumberEncoding = errors.New("sfnt: unsupported real number encoding")
errUnsupportedNumberOfCmapSegments = errors.New("sfnt: unsupported number of cmap segments")
errUnsupportedNumberOfFontDicts = errors.New("sfnt: unsupported number of font dicts")
errUnsupportedNumberOfFonts = errors.New("sfnt: unsupported number of fonts")
errUnsupportedNumberOfHints = errors.New("sfnt: unsupported number of hints")
errUnsupportedNumberOfSubroutines = errors.New("sfnt: unsupported number of subroutines")
errUnsupportedNumberOfTables = errors.New("sfnt: unsupported number of tables")
errUnsupportedPlatformEncoding = errors.New("sfnt: unsupported platform encoding")
errUnsupportedPostTable = errors.New("sfnt: unsupported post table")
errUnsupportedTableOffsetLength = errors.New("sfnt: unsupported table offset or length")
errUnsupportedType2Charstring = errors.New("sfnt: unsupported Type 2 Charstring")
)
// GlyphIndex is a glyph index in a Font.
type GlyphIndex uint16
// NameID identifies a name table entry.
//
// See the "Name IDs" section of
// https://www.microsoft.com/typography/otspec/name.htm
type NameID uint16
const (
NameIDCopyright NameID = 0
NameIDFamily = 1
NameIDSubfamily = 2
NameIDUniqueIdentifier = 3
NameIDFull = 4
NameIDVersion = 5
NameIDPostScript = 6
NameIDTrademark = 7
NameIDManufacturer = 8
NameIDDesigner = 9
NameIDDescription = 10
NameIDVendorURL = 11
NameIDDesignerURL = 12
NameIDLicense = 13
NameIDLicenseURL = 14
NameIDTypographicFamily = 16
NameIDTypographicSubfamily = 17
NameIDCompatibleFull = 18
NameIDSampleText = 19
NameIDPostScriptCID = 20
NameIDWWSFamily = 21
NameIDWWSSubfamily = 22
NameIDLightBackgroundPalette = 23
NameIDDarkBackgroundPalette = 24
NameIDVariationsPostScriptPrefix = 25
)
// Units are an integral number of abstract, scalable "font units". The em
// square is typically 1000 or 2048 "font units". This would map to a certain
// number (e.g. 30 pixels) of physical pixels, depending on things like the
// display resolution (DPI) and font size (e.g. a 12 point font).
type Units int32
// scale returns x divided by unitsPerEm, rounded to the nearest fixed.Int26_6
// value (1/64th of a pixel).
func scale(x fixed.Int26_6, unitsPerEm Units) fixed.Int26_6 {
if x >= 0 {
x += fixed.Int26_6(unitsPerEm) / 2
} else {
x -= fixed.Int26_6(unitsPerEm) / 2
}
return x / fixed.Int26_6(unitsPerEm)
}
func u16(b []byte) uint16 {
_ = b[1] // Bounds check hint to compiler.
return uint16(b[0])<<8 | uint16(b[1])<<0
}
func u32(b []byte) uint32 {
_ = b[3] // Bounds check hint to compiler.
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])<<0
}
// source is a source of byte data. Conceptually, it is like an io.ReaderAt,
// except that a common source of SFNT font data is in-memory instead of
// on-disk: a []byte containing the entire data, either as a global variable
// (e.g. "goregular.TTF") or the result of an ioutil.ReadFile call. In such
// cases, as an optimization, we skip the io.Reader / io.ReaderAt model of
// copying from the source to a caller-supplied buffer, and instead provide
// direct access to the underlying []byte data.
type source struct {
b []byte
r io.ReaderAt
// TODO: add a caching layer, if we're using the io.ReaderAt? Note that
// this might make a source no longer safe to use concurrently.
}
// valid returns whether exactly one of s.b and s.r is nil.
func (s *source) valid() bool {
return (s.b == nil) != (s.r == nil)
}
// viewBufferWritable returns whether the []byte returned by source.view can be
// written to by the caller, including by passing it to the same method
// (source.view) on other receivers (i.e. different sources).
//
// In other words, it returns whether the source's underlying data is an
// io.ReaderAt, not a []byte.
func (s *source) viewBufferWritable() bool {
return s.b == nil
}
// view returns the length bytes at the given offset. buf is an optional
// scratch buffer to reduce allocations when calling view multiple times. A nil
// buf is valid. The []byte returned may be a sub-slice of buf[:cap(buf)], or
// it may be an unrelated slice. In any case, the caller should not modify the
// contents of the returned []byte, other than passing that []byte back to this
// method on the same source s.
func (s *source) view(buf []byte, offset, length int) ([]byte, error) {
if 0 > offset || offset > offset+length {
return nil, errInvalidBounds
}
// Try reading from the []byte.
if s.b != nil {
if offset+length > len(s.b) {
return nil, errInvalidBounds
}
return s.b[offset : offset+length], nil
}
// Read from the io.ReaderAt.
if length <= cap(buf) {
buf = buf[:length]
} else {
// Round length up to the nearest KiB. The slack can lead to fewer
// allocations if the buffer is re-used for multiple source.view calls.
n := length
n += 1023
n &^= 1023
buf = make([]byte, length, n)
}
if n, err := s.r.ReadAt(buf, int64(offset)); n != length {
return nil, err
}
return buf, nil
}
// u16 returns the uint16 in the table t at the relative offset i.
//
// buf is an optional scratch buffer as per the source.view method.
func (s *source) u16(buf []byte, t table, i int) (uint16, error) {
if i < 0 || uint(t.length) < uint(i+2) {
return 0, errInvalidBounds
}
buf, err := s.view(buf, int(t.offset)+i, 2)
if err != nil {
return 0, err
}
return u16(buf), nil
}
// u32 returns the uint32 in the table t at the relative offset i.
//
// buf is an optional scratch buffer as per the source.view method.
func (s *source) u32(buf []byte, t table, i int) (uint32, error) {
if i < 0 || uint(t.length) < uint(i+4) {
return 0, errInvalidBounds
}
buf, err := s.view(buf, int(t.offset)+i, 4)
if err != nil {
return 0, err
}
return u32(buf), nil
}
// table is a section of the font data.
type table struct {
offset, length uint32
}
// ParseCollection parses an SFNT font collection, such as TTC or OTC data,
// from a []byte data source.
//
// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, it
// will return a collection containing 1 font.
func ParseCollection(src []byte) (*Collection, error) {
c := &Collection{src: source{b: src}}
if err := c.initialize(); err != nil {
return nil, err
}
return c, nil
}
// ParseCollectionReaderAt parses an SFNT collection, such as TTC or OTC data,
// from an io.ReaderAt data source.
//
// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, it
// will return a collection containing 1 font.
func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) {
c := &Collection{src: source{r: src}}
if err := c.initialize(); err != nil {
return nil, err
}
return c, nil
}
// Collection is a collection of one or more fonts.
//
// All of the Collection methods are safe to call concurrently.
type Collection struct {
src source
offsets []uint32
isDfont bool
}
// NumFonts returns the number of fonts in the collection.
func (c *Collection) NumFonts() int { return len(c.offsets) }
func (c *Collection) initialize() error {
// The https://www.microsoft.com/typography/otspec/otff.htm "Font
// Collections" section describes the TTC header.
//
// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
// describes the dfont header.
//
// 16 is the maximum of sizeof(TTCHeader) and sizeof(DfontHeader).
buf, err := c.src.view(nil, 0, 16)
if err != nil {
return err
}
// These cases match the switch statement in Font.initializeTables.
switch u32(buf) {
default:
return errInvalidFontCollection
case dfontResourceDataOffset:
return c.parseDfont(buf, u32(buf[4:]), u32(buf[12:]))
case 0x00010000, 0x4f54544f:
// Try parsing it as a single font instead of a collection.
c.offsets = []uint32{0}
case 0x74746366: // "ttcf".
numFonts := u32(buf[8:])
if numFonts == 0 || numFonts > maxNumFonts {
return errUnsupportedNumberOfFonts
}
buf, err = c.src.view(nil, 12, int(4*numFonts))
if err != nil {
return err
}
c.offsets = make([]uint32, numFonts)
for i := range c.offsets {
o := u32(buf[4*i:])
if o > maxTableOffset {
return errUnsupportedTableOffsetLength
}
c.offsets[i] = o
}
}
return nil
}
// dfontResourceDataOffset is the assumed value of a dfont file's resource data
// offset.
//
// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
// says that "A Mac OS resource file... [starts with an] offset from start of
// file to start of resource data section... [usually] 0x0100". In theory,
// 0x00000100 isn't always a magic number for identifying dfont files. In
// practice, it seems to work.
const dfontResourceDataOffset = 0x00000100
// parseDfont parses a dfont resource map, as per
// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
//
// That unofficial wiki page lists all of its fields as *signed* integers,
// which looks unusual. The actual file format might use *unsigned* integers in
// various places, but until we have either an official specification or an
// actual dfont file where this matters, we'll use signed integers and treat
// negative values as invalid.
func (c *Collection) parseDfont(buf []byte, resourceMapOffset, resourceMapLength uint32) error {
if resourceMapOffset > maxTableOffset || resourceMapLength > maxTableLength {
return errUnsupportedTableOffsetLength
}
const headerSize = 28
if resourceMapLength < headerSize {
return errInvalidDfont
}
buf, err := c.src.view(buf, int(resourceMapOffset+24), 2)
if err != nil {
return err
}
typeListOffset := int(int16(u16(buf)))
if typeListOffset < headerSize || resourceMapLength < uint32(typeListOffset)+2 {
return errInvalidDfont
}
buf, err = c.src.view(buf, int(resourceMapOffset)+typeListOffset, 2)
if err != nil {
return err
}
typeCount := int(int16(u16(buf)))
const tSize = 8
if typeCount < 0 || tSize*uint32(typeCount) > resourceMapLength-uint32(typeListOffset)-2 {
return errInvalidDfont
}
buf, err = c.src.view(buf, int(resourceMapOffset)+typeListOffset+2, tSize*typeCount)
if err != nil {
return err
}
resourceCount, resourceListOffset := 0, 0
for i := 0; i < typeCount; i++ {
if u32(buf[tSize*i:]) != 0x73666e74 { // "sfnt".
continue
}
resourceCount = int(int16(u16(buf[tSize*i+4:])))
if resourceCount < 0 {
return errInvalidDfont
}
// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
// says that the value in the wire format is "the number of
// resources of this type, minus one."
resourceCount++
resourceListOffset = int(int16(u16(buf[tSize*i+6:])))
if resourceListOffset < 0 {
return errInvalidDfont
}
break
}
if resourceCount == 0 {
return errInvalidDfont
}
if resourceCount > maxNumFonts {
return errUnsupportedNumberOfFonts
}
const rSize = 12
if o, n := uint32(typeListOffset+resourceListOffset), rSize*uint32(resourceCount); o > resourceMapLength || n > resourceMapLength-o {
return errInvalidDfont
} else {
buf, err = c.src.view(buf, int(resourceMapOffset+o), int(n))
if err != nil {
return err
}
}
c.offsets = make([]uint32, resourceCount)
for i := range c.offsets {
o := 0xffffff & u32(buf[rSize*i+4:])
// Offsets are relative to the resource data start, not the file start.
// A particular resource's data also starts with a 4-byte length, which
// we skip.
o += dfontResourceDataOffset + 4
if o > maxTableOffset {
return errUnsupportedTableOffsetLength
}
c.offsets[i] = o
}
c.isDfont = true
return nil
}
// Font returns the i'th font in the collection.
func (c *Collection) Font(i int) (*Font, error) {
if i < 0 || len(c.offsets) <= i {
return nil, ErrNotFound
}
f := &Font{src: c.src}
if err := f.initialize(int(c.offsets[i]), c.isDfont); err != nil {
return nil, err
}
return f, nil
}
// Parse parses an SFNT font, such as TTF or OTF data, from a []byte data
// source.
func Parse(src []byte) (*Font, error) {
f := &Font{src: source{b: src}}
if err := f.initialize(0, false); err != nil {
return nil, err
}
return f, nil
}
// ParseReaderAt parses an SFNT font, such as TTF or OTF data, from an
// io.ReaderAt data source.
func ParseReaderAt(src io.ReaderAt) (*Font, error) {
f := &Font{src: source{r: src}}
if err := f.initialize(0, false); err != nil {
return nil, err
}
return f, nil
}
// Font is an SFNT font.
//
// Many of its methods take a *Buffer argument, as re-using buffers can reduce
// the total memory allocation of repeated Font method calls, such as measuring
// and rasterizing every unique glyph in a string of text. If efficiency is not
// a concern, passing a nil *Buffer is valid, and implies using a temporary
// buffer for a single call.
//
// It is valid to re-use a *Buffer with multiple Font method calls, even with
// different *Font receivers, as long as they are not concurrent calls.
//
// All of the Font methods are safe to call concurrently, as long as each call
// has a different *Buffer (or nil).
//
// The Font methods that don't take a *Buffer argument are always safe to call
// concurrently.
//
// Some methods provide lengths or coordinates, e.g. bounds, font metrics and
// control points. All of these methods take a ppem parameter, which is the
// number of pixels in 1 em, expressed as a 26.6 fixed point value. For
// example, if 1 em is 10 pixels then ppem is fixed.I(10), which equals
// fixed.Int26_6(10 << 6).
//
// To get those lengths or coordinates in terms of font units instead of
// pixels, use ppem = fixed.Int26_6(f.UnitsPerEm()) and if those methods take a
// font.Hinting parameter, use font.HintingNone. The return values will have
// type fixed.Int26_6, but those numbers can be converted back to Units with no
// further scaling necessary.
type Font struct {
src source
// https://www.microsoft.com/typography/otspec/otff.htm#otttables
// "Required Tables".
cmap table
head table
hhea table
hmtx table
maxp table
name table
os2 table
post table
// https://www.microsoft.com/typography/otspec/otff.htm#otttables
// "Tables Related to TrueType Outlines".
//
// This implementation does not support hinting, so it does not read the
// cvt, fpgm gasp or prep tables.
glyf table
loca table
// https://www.microsoft.com/typography/otspec/otff.htm#otttables
// "Tables Related to PostScript Outlines".
//
// TODO: cff2, vorg?
cff table
// https://www.microsoft.com/typography/otspec/otff.htm#otttables
// "Tables Related to Bitmap Glyphs".
//
// TODO: Others?
cblc table
// https://www.microsoft.com/typography/otspec/otff.htm#otttables
// "Advanced Typographic Tables".
//
// TODO: base, gdef, gpos, gsub, jstf, math?
// https://www.microsoft.com/typography/otspec/otff.htm#otttables
// "Other OpenType Tables".
//
// TODO: hdmx, vmtx? Others?
kern table
cached struct {
glyphData glyphData
glyphIndex glyphIndexFunc
bounds [4]int16
indexToLocFormat bool // false means short, true means long.
isColorBitmap bool
isPostScript bool
kernNumPairs int32
kernOffset int32
numHMetrics int32
postTableVersion uint32
unitsPerEm Units
}
}
// NumGlyphs returns the number of glyphs in f.
func (f *Font) NumGlyphs() int { return len(f.cached.glyphData.locations) - 1 }
// UnitsPerEm returns the number of units per em for f.
func (f *Font) UnitsPerEm() Units { return f.cached.unitsPerEm }
func (f *Font) initialize(offset int, isDfont bool) error {
if !f.src.valid() {
return errInvalidSourceData
}
buf, isPostScript, err := f.initializeTables(offset, isDfont)
if err != nil {
return err
}
// The order of these parseXxx calls matters. Later calls may depend on
// information parsed by earlier calls, such as the maxp table's numGlyphs.
// To enforce these dependencies, such information is passed and returned
// explicitly, and the f.cached fields are only set afterwards.
//
// When implementing new parseXxx methods, take care not to call methods
// such as Font.NumGlyphs that implicitly depend on f.cached fields.
buf, bounds, indexToLocFormat, unitsPerEm, err := f.parseHead(buf)
if err != nil {
return err
}
buf, numGlyphs, err := f.parseMaxp(buf, isPostScript)
if err != nil {
return err
}
buf, glyphData, isColorBitmap, err := f.parseGlyphData(buf, numGlyphs, indexToLocFormat, isPostScript)
if err != nil {
return err
}
buf, glyphIndex, err := f.parseCmap(buf)
if err != nil {
return err
}
buf, kernNumPairs, kernOffset, err := f.parseKern(buf)
if err != nil {
return err
}
buf, numHMetrics, err := f.parseHhea(buf, numGlyphs)
if err != nil {
return err
}
buf, err = f.parseHmtx(buf, numGlyphs, numHMetrics)
if err != nil {
return err
}
buf, postTableVersion, err := f.parsePost(buf, numGlyphs)
if err != nil {
return err
}
f.cached.glyphData = glyphData
f.cached.glyphIndex = glyphIndex
f.cached.bounds = bounds
f.cached.indexToLocFormat = indexToLocFormat
f.cached.isColorBitmap = isColorBitmap
f.cached.isPostScript = isPostScript
f.cached.kernNumPairs = kernNumPairs
f.cached.kernOffset = kernOffset
f.cached.numHMetrics = numHMetrics
f.cached.postTableVersion = postTableVersion
f.cached.unitsPerEm = unitsPerEm
return nil
}
func (f *Font) initializeTables(offset int, isDfont bool) (buf1 []byte, isPostScript bool, err error) {
// https://www.microsoft.com/typography/otspec/otff.htm "Organization of an
// OpenType Font" says that "The OpenType font starts with the Offset
// Table", which is 12 bytes.
buf, err := f.src.view(nil, offset, 12)
if err != nil {
return nil, false, err
}
// When updating the cases in this switch statement, also update the
// Collection.initialize method.
switch u32(buf) {
default:
return nil, false, errInvalidFont
case dfontResourceDataOffset:
return nil, false, errInvalidSingleFont
case 0x00010000:
// No-op.
case 0x4f54544f: // "OTTO".
isPostScript = true
case 0x74746366: // "ttcf".
return nil, false, errInvalidSingleFont
}
numTables := int(u16(buf[4:]))
if numTables > maxNumTables {
return nil, false, errUnsupportedNumberOfTables
}
// "The Offset Table is followed immediately by the Table Record entries...
// sorted in ascending order by tag", 16 bytes each.
buf, err = f.src.view(buf, offset+12, 16*numTables)
if err != nil {
return nil, false, err
}
for b, first, prevTag := buf, true, uint32(0); len(b) > 0; b = b[16:] {
tag := u32(b)
if first {
first = false
} else if tag <= prevTag {
return nil, false, errInvalidTableTagOrder
}
prevTag = tag
o, n := u32(b[8:12]), u32(b[12:16])
// For dfont files, the offset is relative to the resource, not the
// file.
if isDfont {
origO := o
o += uint32(offset)
if o < origO {
return nil, false, errUnsupportedTableOffsetLength
}
}
if o > maxTableOffset || n > maxTableLength {
return nil, false, errUnsupportedTableOffsetLength
}
// We ignore the checksums, but "all tables must begin on four byte
// boundries [sic]".
if o&3 != 0 {
return nil, false, errInvalidTableOffset
}
// Match the 4-byte tag as a uint32. For example, "OS/2" is 0x4f532f32.
switch tag {
case 0x43424c43:
f.cblc = table{o, n}
case 0x43464620:
f.cff = table{o, n}
case 0x4f532f32:
f.os2 = table{o, n}
case 0x636d6170:
f.cmap = table{o, n}
case 0x676c7966:
f.glyf = table{o, n}
case 0x68656164:
f.head = table{o, n}
case 0x68686561:
f.hhea = table{o, n}
case 0x686d7478:
f.hmtx = table{o, n}
case 0x6b65726e:
f.kern = table{o, n}
case 0x6c6f6361:
f.loca = table{o, n}
case 0x6d617870:
f.maxp = table{o, n}
case 0x6e616d65:
f.name = table{o, n}
case 0x706f7374:
f.post = table{o, n}
}
}
return buf, isPostScript, nil
}
func (f *Font) parseCmap(buf []byte) (buf1 []byte, glyphIndex glyphIndexFunc, err error) {
// https://www.microsoft.com/typography/OTSPEC/cmap.htm
const headerSize, entrySize = 4, 8
if f.cmap.length < headerSize {
return nil, nil, errInvalidCmapTable
}
u, err := f.src.u16(buf, f.cmap, 2)
if err != nil {
return nil, nil, err
}
numSubtables := int(u)
if f.cmap.length < headerSize+entrySize*uint32(numSubtables) {
return nil, nil, errInvalidCmapTable
}
var (
bestWidth int
bestOffset uint32
bestLength uint32
bestFormat uint16
)
// Scan all of the subtables, picking the widest supported one. See the
// platformEncodingWidth comment for more discussion of width.
for i := 0; i < numSubtables; i++ {
buf, err = f.src.view(buf, int(f.cmap.offset)+headerSize+entrySize*i, entrySize)
if err != nil {
return nil, nil, err
}
pid := u16(buf)
psid := u16(buf[2:])
width := platformEncodingWidth(pid, psid)
if width <= bestWidth {
continue
}
offset := u32(buf[4:])
if offset > f.cmap.length-4 {
return nil, nil, errInvalidCmapTable
}
buf, err = f.src.view(buf, int(f.cmap.offset+offset), 4)
if err != nil {
return nil, nil, err
}
format := u16(buf)
if !supportedCmapFormat(format, pid, psid) {
continue
}
length := uint32(u16(buf[2:]))
bestWidth = width
bestOffset = offset
bestLength = length
bestFormat = format
}
if bestWidth == 0 {
return nil, nil, errUnsupportedCmapEncodings
}
return f.makeCachedGlyphIndex(buf, bestOffset, bestLength, bestFormat)
}
func (f *Font) parseHead(buf []byte) (buf1 []byte, bounds [4]int16, indexToLocFormat bool, unitsPerEm Units, err error) {
// https://www.microsoft.com/typography/otspec/head.htm
if f.head.length != 54 {
return nil, [4]int16{}, false, 0, errInvalidHeadTable
}
u, err := f.src.u16(buf, f.head, 18)
if err != nil {
return nil, [4]int16{}, false, 0, err
}
if u == 0 {
return nil, [4]int16{}, false, 0, errInvalidHeadTable
}
unitsPerEm = Units(u)
for i := range bounds {
u, err := f.src.u16(buf, f.head, 36+2*i)
if err != nil {
return nil, [4]int16{}, false, 0, err
}
bounds[i] = int16(u)
}
u, err = f.src.u16(buf, f.head, 50)
if err != nil {
return nil, [4]int16{}, false, 0, err
}
indexToLocFormat = u != 0
return buf, bounds, indexToLocFormat, unitsPerEm, nil
}
func (f *Font) parseHhea(buf []byte, numGlyphs int32) (buf1 []byte, numHMetrics int32, err error) {
// https://www.microsoft.com/typography/OTSPEC/hhea.htm
if f.hhea.length != 36 {
return nil, 0, errInvalidHheaTable
}
u, err := f.src.u16(buf, f.hhea, 34)
if err != nil {
return nil, 0, err
}
if int32(u) > numGlyphs || u == 0 {
return nil, 0, errInvalidHheaTable
}
return buf, int32(u), nil
}
func (f *Font) parseHmtx(buf []byte, numGlyphs, numHMetrics int32) (buf1 []byte, err error) {
// https://www.microsoft.com/typography/OTSPEC/hmtx.htm
if f.hmtx.length != uint32(2*numGlyphs+2*numHMetrics) {
return nil, errInvalidHmtxTable
}
return buf, nil
}
func (f *Font) parseKern(buf []byte) (buf1 []byte, kernNumPairs, kernOffset int32, err error) {
// https://www.microsoft.com/typography/otspec/kern.htm
if f.kern.length == 0 {
return buf, 0, 0, nil
}
const headerSize = 4
if f.kern.length < headerSize {
return nil, 0, 0, errInvalidKernTable
}
buf, err = f.src.view(buf, int(f.kern.offset), headerSize)
if err != nil {
return nil, 0, 0, err
}
offset := int(f.kern.offset) + headerSize
length := int(f.kern.length) - headerSize
switch version := u16(buf); version {
case 0:
if numTables := int(u16(buf[2:])); numTables == 0 {
return buf, 0, 0, nil
} else if numTables > 1 {
// TODO: support multiple subtables. For now, fall through and use
// only the first one.
}
return f.parseKernVersion0(buf, offset, length)
case 1:
if buf[2] != 0 || buf[3] != 0 {
return nil, 0, 0, errUnsupportedKernTable
}
// Microsoft's https://www.microsoft.com/typography/otspec/kern.htm
// says that "Apple has extended the definition of the 'kern' table to
// provide additional functionality. The Apple extensions are not
// supported on Windows."
//
// The format is relatively complicated, including encoding a state
// machine, but rarely seen. We follow Microsoft's and FreeType's
// behavior and simply ignore it. Theoretically, we could follow
// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html
// but it doesn't seem worth the effort.
return buf, 0, 0, nil
}
return nil, 0, 0, errUnsupportedKernTable
}
func (f *Font) parseKernVersion0(buf []byte, offset, length int) (buf1 []byte, kernNumPairs, kernOffset int32, err error) {
const headerSize = 6
if length < headerSize {
return nil, 0, 0, errInvalidKernTable
}
buf, err = f.src.view(buf, offset, headerSize)
if err != nil {
return nil, 0, 0, err
}
if version := u16(buf); version != 0 {
return nil, 0, 0, errUnsupportedKernTable
}
subtableLength := int(u16(buf[2:]))
if subtableLength < headerSize || length < subtableLength {
return nil, 0, 0, errInvalidKernTable
}
if coverageBits := buf[5]; coverageBits != 0x01 {
// We only support horizontal kerning.
return nil, 0, 0, errUnsupportedKernTable
}
offset += headerSize
length -= headerSize
subtableLength -= headerSize
switch format := buf[4]; format {
case 0:
return f.parseKernFormat0(buf, offset, subtableLength)
case 2:
// If we could find such a font, we could write code to support it, but
// a comment in the equivalent FreeType code (sfnt/ttkern.c) says that
// they've never seen such a font.
}
return nil, 0, 0, errUnsupportedKernTable
}
func (f *Font) parseKernFormat0(buf []byte, offset, length int) (buf1 []byte, kernNumPairs, kernOffset int32, err error) {
const headerSize, entrySize = 8, 6
if length < headerSize {
return nil, 0, 0, errInvalidKernTable
}
buf, err = f.src.view(buf, offset, headerSize)
if err != nil {
return nil, 0, 0, err
}
kernNumPairs = int32(u16(buf))
if length != headerSize+entrySize*int(kernNumPairs) {
return nil, 0, 0, errInvalidKernTable
}
return buf, kernNumPairs, int32(offset) + headerSize, nil
}
func (f *Font) parseMaxp(buf []byte, isPostScript bool) (buf1 []byte, numGlyphs int32, err error) {
// https://www.microsoft.com/typography/otspec/maxp.htm
if isPostScript {
if f.maxp.length != 6 {
return nil, 0, errInvalidMaxpTable
}
} else {
if f.maxp.length != 32 {
return nil, 0, errInvalidMaxpTable
}
}
u, err := f.src.u16(buf, f.maxp, 4)
if err != nil {
return nil, 0, err
}
return buf, int32(u), nil
}
type glyphData struct {
// The glyph data for the i'th glyph index is in
// src[locations[i+0]:locations[i+1]].
//
// The slice length equals 1 plus the number of glyphs.
locations []uint32
// For PostScript fonts, the bytecode for the i'th global or local
// subroutine is in src[x[i+0]:x[i+1]].
//
// The []uint32 slice length equals 1 plus the number of subroutines
gsubrs []uint32
singleSubrs []uint32
multiSubrs [][]uint32
fdSelect fdSelect
}
func (f *Font) parseGlyphData(buf []byte, numGlyphs int32, indexToLocFormat, isPostScript bool) (buf1 []byte, ret glyphData, isColorBitmap bool, err error) {
if isPostScript {
p := cffParser{
src: &f.src,
base: int(f.cff.offset),
offset: int(f.cff.offset),
end: int(f.cff.offset + f.cff.length),
}
ret, err = p.parse(numGlyphs)
if err != nil {
return nil, glyphData{}, false, err
}
} else if f.loca.length != 0 {
ret.locations, err = parseLoca(&f.src, f.loca, f.glyf.offset, indexToLocFormat, numGlyphs)
if err != nil {
return nil, glyphData{}, false, err
}
} else if f.cblc.length != 0 {
isColorBitmap = true
// TODO: parse the CBLC (and CBDT) tables. For now, we return a font
// with empty glyphs.
ret.locations = make([]uint32, numGlyphs+1)
}
if len(ret.locations) != int(numGlyphs+1) {
return nil, glyphData{}, false, errInvalidLocationData
}
return buf, ret, isColorBitmap, nil
}
func (f *Font) parsePost(buf []byte, numGlyphs int32) (buf1 []byte, postTableVersion uint32, err error) {
// https://www.microsoft.com/typography/otspec/post.htm
const headerSize = 32
if f.post.length < headerSize {
return nil, 0, errInvalidPostTable
}
u, err := f.src.u32(buf, f.post, 0)
if err != nil {
return nil, 0, err
}
switch u {
case 0x20000:
if f.post.length < headerSize+2+2*uint32(numGlyphs) {
return nil, 0, errInvalidPostTable
}
case 0x30000:
// No-op.
default:
return nil, 0, errUnsupportedPostTable
}
return buf, u, nil
}
// Bounds returns the union of a Font's glyphs' bounds.
//
// In the returned Rectangle26_6's (x, y) coordinates, the Y axis increases
// down.
func (f *Font) Bounds(b *Buffer, ppem fixed.Int26_6, h font.Hinting) (fixed.Rectangle26_6, error) {
// The 0, 3, 2, 1 indices are to flip the Y coordinates. OpenType's Y axis
// increases up. Go's standard graphics libraries' Y axis increases down.
r := fixed.Rectangle26_6{
Min: fixed.Point26_6{
X: +scale(fixed.Int26_6(f.cached.bounds[0])*ppem, f.cached.unitsPerEm),
Y: -scale(fixed.Int26_6(f.cached.bounds[3])*ppem, f.cached.unitsPerEm),
},
Max: fixed.Point26_6{
X: +scale(fixed.Int26_6(f.cached.bounds[2])*ppem, f.cached.unitsPerEm),
Y: -scale(fixed.Int26_6(f.cached.bounds[1])*ppem, f.cached.unitsPerEm),
},
}
if h == font.HintingFull {
// Quantize the Min down and Max up to a whole pixel.
r.Min.X = (r.Min.X + 0) &^ 63
r.Min.Y = (r.Min.Y + 0) &^ 63
r.Max.X = (r.Max.X + 63) &^ 63
r.Max.Y = (r.Max.Y + 63) &^ 63
}
return r, nil
}
// TODO: API for looking up glyph variants?? For example, some fonts may
// provide both slashed and dotted zero glyphs ('0'), or regular and 'old
// style' numerals, and users can direct software to choose a variant.
type glyphIndexFunc func(f *Font, b *Buffer, r rune) (GlyphIndex, error)
// GlyphIndex returns the glyph index for the given rune.
//
// It returns (0, nil) if there is no glyph for r.
// https://www.microsoft.com/typography/OTSPEC/cmap.htm says that "Character
// codes that do not correspond to any glyph in the font should be mapped to
// glyph index 0. The glyph at this location must be a special glyph
// representing a missing character, commonly known as .notdef."
func (f *Font) GlyphIndex(b *Buffer, r rune) (GlyphIndex, error) {
return f.cached.glyphIndex(f, b, r)
}
func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
xx := int(x)
if f.NumGlyphs() <= xx {
return nil, 0, 0, ErrNotFound
}
i := f.cached.glyphData.locations[xx+0]
j := f.cached.glyphData.locations[xx+1]
if j < i {
return nil, 0, 0, errInvalidGlyphDataLength
}
if j-i > maxGlyphDataLength {
return nil, 0, 0, errUnsupportedGlyphDataLength
}
buf, err = b.view(&f.src, int(i), int(j-i))
return buf, i, j - i, err
}
// LoadGlyphOptions are the options to the Font.LoadGlyph method.
type LoadGlyphOptions struct {
// TODO: transform / hinting.
}
// LoadGlyph returns the vector segments for the x'th glyph. ppem is the number
// of pixels in 1 em.
//
// If b is non-nil, the segments become invalid to use once b is re-used.
//
// In the returned Segments' (x, y) coordinates, the Y axis increases down.
//
// It returns ErrNotFound if the glyph index is out of range. It returns
// ErrColoredGlyph if the glyph is not a monochrome vector glyph, such as a
// colored (bitmap or vector) emoji glyph.
func (f *Font) LoadGlyph(b *Buffer, x GlyphIndex, ppem fixed.Int26_6, opts *LoadGlyphOptions) ([]Segment, error) {
if b == nil {
b = &Buffer{}
}
b.segments = b.segments[:0]
if f.cached.isColorBitmap {
return nil, ErrColoredGlyph
}
if f.cached.isPostScript {
buf, offset, length, err := f.viewGlyphData(b, x)
if err != nil {
return nil, err
}
b.psi.type2Charstrings.initialize(f, b, x)
if err := b.psi.run(psContextType2Charstring, buf, offset, length); err != nil {
return nil, err
}
if !b.psi.type2Charstrings.ended {
return nil, errInvalidCFFTable
}
} else if err := loadGlyf(f, b, x, 0, 0); err != nil {
return nil, err
}
// Scale the segments. If we want to support hinting, we'll have to push
// the scaling computation into the PostScript / TrueType specific glyph
// loading code, such as the appendGlyfSegments body, since TrueType
// hinting bytecode works on the scaled glyph vectors. For now, though,
// it's simpler to scale as a post-processing step.
//
// We also flip the Y coordinates. OpenType's Y axis increases up. Go's
// standard graphics libraries' Y axis increases down.
for i := range b.segments {
a := &b.segments[i].Args
for j := range a {
a[j].X = +scale(a[j].X*ppem, f.cached.unitsPerEm)
a[j].Y = -scale(a[j].Y*ppem, f.cached.unitsPerEm)
}
}
// TODO: look at opts to transform / hint the Buffer.segments.
return b.segments, nil
}
// GlyphName returns the name of the x'th glyph.
//
// Not every font contains glyph names. If not present, GlyphName will return
// ("", nil).
//
// If present, the glyph name, provided by the font, is assumed to follow the
// Adobe Glyph List Specification:
// https://github.com/adobe-type-tools/agl-specification/blob/master/README.md
//
// This is also known as the "Adobe Glyph Naming convention", the "Adobe
// document [for] Unicode and Glyph Names" or "PostScript glyph names".
//
// It returns ErrNotFound if the glyph index is out of range.
func (f *Font) GlyphName(b *Buffer, x GlyphIndex) (string, error) {
if int(x) >= f.NumGlyphs() {
return "", ErrNotFound
}
if f.cached.postTableVersion != 0x20000 {
return "", nil
}
if b == nil {
b = &Buffer{}
}
// The wire format for a Version 2 post table is documented at:
// https://www.microsoft.com/typography/otspec/post.htm
const glyphNameIndexOffset = 34
buf, err := b.view(&f.src, int(f.post.offset)+glyphNameIndexOffset+2*int(x), 2)
if err != nil {
return "", err
}
u := u16(buf)
if u < numBuiltInPostNames {
i := builtInPostNamesOffsets[u+0]
j := builtInPostNamesOffsets[u+1]
return builtInPostNamesData[i:j], nil
}
// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html
// says that "32768 through 65535 are reserved for future use".
if u > 32767 {
return "", errUnsupportedPostTable
}
u -= numBuiltInPostNames
// Iterate through the list of Pascal-formatted strings. A linear scan is
// clearly O(u), which isn't great (as the obvious loop, calling
// Font.GlyphName, to get all of the glyph names in a font has quadratic
// complexity), but the wire format doesn't suggest a better alternative.
offset := glyphNameIndexOffset + 2*f.NumGlyphs()
buf, err = b.view(&f.src, int(f.post.offset)+offset, int(f.post.length)-offset)
if err != nil {
return "", err
}
for {
if len(buf) == 0 {
return "", errInvalidPostTable
}
n := 1 + int(buf[0])
if len(buf) < n {
return "", errInvalidPostTable
}
if u == 0 {
return string(buf[1:n]), nil
}
buf = buf[n:]
u--
}
}
// GlyphAdvance returns the advance width for the x'th glyph. ppem is the
// number of pixels in 1 em.
//
// It returns ErrNotFound if the glyph index is out of range.
func (f *Font) GlyphAdvance(b *Buffer, x GlyphIndex, ppem fixed.Int26_6, h font.Hinting) (fixed.Int26_6, error) {
if int(x) >= f.NumGlyphs() {
return 0, ErrNotFound
}
if b == nil {
b = &Buffer{}
}
// https://www.microsoft.com/typography/OTSPEC/hmtx.htm says that "As an
// optimization, the number of records can be less than the number of
// glyphs, in which case the advance width value of the last record applies
// to all remaining glyph IDs."
if n := GlyphIndex(f.cached.numHMetrics - 1); x > n {
x = n
}
buf, err := b.view(&f.src, int(f.hmtx.offset)+int(4*x), 2)
if err != nil {
return 0, err
}
adv := fixed.Int26_6(u16(buf))
adv = scale(adv*ppem, f.cached.unitsPerEm)
if h == font.HintingFull {
// Quantize the fixed.Int26_6 value to the nearest pixel.
adv = (adv + 32) &^ 63
}
return adv, nil
}
// Kern returns the horizontal adjustment for the kerning pair (x0, x1). A
// positive kern means to move the glyphs further apart. ppem is the number of
// pixels in 1 em.
//
// It returns ErrNotFound if either glyph index is out of range.
func (f *Font) Kern(b *Buffer, x0, x1 GlyphIndex, ppem fixed.Int26_6, h font.Hinting) (fixed.Int26_6, error) {
// TODO: how should this work with the GPOS table and CFF fonts?
// https://www.microsoft.com/typography/otspec/kern.htm says that
// "OpenType™ fonts containing CFF outlines are not supported by the 'kern'
// table and must use the 'GPOS' OpenType Layout table."
if n := f.NumGlyphs(); int(x0) >= n || int(x1) >= n {
return 0, ErrNotFound
}
// Not every font has a kern table. If it doesn't, or if that table is
// ignored, there's no need to allocate a Buffer.
if f.cached.kernNumPairs == 0 {
return 0, nil
}
if b == nil {
b = &Buffer{}
}
key := uint32(x0)<<16 | uint32(x1)
lo, hi := int32(0), f.cached.kernNumPairs
for lo < hi {
i := (lo + hi) / 2
// TODO: this view call inside the inner loop can lead to many small
// reads instead of fewer larger reads, which can be expensive. We
// should be able to do better, although we don't want to make (one)
// arbitrarily large read. Perhaps we should round up reads to 4K or 8K
// chunks. For reference, Arial.ttf's kern table is 5472 bytes.
// Times_New_Roman.ttf's kern table is 5220 bytes.
const entrySize = 6
buf, err := b.view(&f.src, int(f.cached.kernOffset+i*entrySize), entrySize)
if err != nil {
return 0, err
}
k := u32(buf)
if k < key {
lo = i + 1
} else if k > key {
hi = i
} else {
kern := fixed.Int26_6(int16(u16(buf[4:])))
kern = scale(kern*ppem, f.cached.unitsPerEm)
if h == font.HintingFull {
// Quantize the fixed.Int26_6 value to the nearest pixel.
kern = (kern + 32) &^ 63
}
return kern, nil
}
}
return 0, nil
}
// Name returns the name value keyed by the given NameID.
//
// It returns ErrNotFound if there is no value for that key.
func (f *Font) Name(b *Buffer, id NameID) (string, error) {
if b == nil {
b = &Buffer{}
}
const headerSize, entrySize = 6, 12
if f.name.length < headerSize {
return "", errInvalidNameTable
}
buf, err := b.view(&f.src, int(f.name.offset), headerSize)
if err != nil {
return "", err
}
numSubtables := u16(buf[2:])
if f.name.length < headerSize+entrySize*uint32(numSubtables) {
return "", errInvalidNameTable
}
stringOffset := u16(buf[4:])
seen := false
for i, n := 0, int(numSubtables); i < n; i++ {
buf, err := b.view(&f.src, int(f.name.offset)+headerSize+entrySize*i, entrySize)
if err != nil {
return "", err
}
if u16(buf[6:]) != uint16(id) {
continue
}
seen = true
var stringify func([]byte) (string, error)
switch u32(buf) {
default:
continue
case pidMacintosh<<16 | psidMacintoshRoman:
stringify = stringifyMacintosh
case pidWindows<<16 | psidWindowsUCS2:
stringify = stringifyUCS2
}
nameLength := u16(buf[8:])
nameOffset := u16(buf[10:])
buf, err = b.view(&f.src, int(f.name.offset)+int(nameOffset)+int(stringOffset), int(nameLength))
if err != nil {
return "", err
}
return stringify(buf)
}
if seen {
return "", errUnsupportedPlatformEncoding
}
return "", ErrNotFound
}
func stringifyMacintosh(b []byte) (string, error) {
for _, c := range b {
if c >= 0x80 {
// b contains some non-ASCII bytes.
s, _ := charmap.Macintosh.NewDecoder().Bytes(b)
return string(s), nil
}
}
// b contains only ASCII bytes.
return string(b), nil
}
func stringifyUCS2(b []byte) (string, error) {
if len(b)&1 != 0 {
return "", errInvalidUCS2String
}
r := make([]rune, len(b)/2)
for i := range r {
r[i] = rune(u16(b))
b = b[2:]
}
return string(r), nil
}
// Buffer holds re-usable buffers that can reduce the total memory allocation
// of repeated Font method calls.
//
// See the Font type's documentation comment for more details.
type Buffer struct {
// buf is a byte buffer for when a Font's source is an io.ReaderAt.
buf []byte
// segments holds glyph vector path segments.
segments []Segment
// compoundStack holds the components of a TrueType compound glyph.
compoundStack [maxCompoundStackSize]struct {
glyphIndex GlyphIndex
dx, dy int16
hasTransform bool
transformXX int16
transformXY int16
transformYX int16
transformYY int16
}
// psi is a PostScript interpreter for when the Font is an OpenType/CFF
// font.
psi psInterpreter
}
func (b *Buffer) view(src *source, offset, length int) ([]byte, error) {
buf, err := src.view(b.buf, offset, length)
if err != nil {
return nil, err
}
// Only update b.buf if it is safe to re-use buf.
if src.viewBufferWritable() {
b.buf = buf
}
return buf, nil
}
// Segment is a segment of a vector path.
type Segment struct {
// Op is the operator.
Op SegmentOp
// Args is up to three (x, y) coordinates. The Y axis increases down.
Args [3]fixed.Point26_6
}
// SegmentOp is a vector path segment's operator.
type SegmentOp uint32
const (
SegmentOpMoveTo SegmentOp = iota
SegmentOpLineTo
SegmentOpQuadTo
SegmentOpCubeTo
)
// translateArgs applies a translation to args.
func translateArgs(args *[3]fixed.Point26_6, dx, dy fixed.Int26_6) {
args[0].X += dx
args[0].Y += dy
args[1].X += dx
args[1].Y += dy
args[2].X += dx
args[2].Y += dy
}
// transformArgs applies an affine transformation to args. The t?? arguments
// are 2.14 fixed point values.
func transformArgs(args *[3]fixed.Point26_6, txx, txy, tyx, tyy int16, dx, dy fixed.Int26_6) {
args[0] = tform(txx, txy, tyx, tyy, dx, dy, args[0])
args[1] = tform(txx, txy, tyx, tyy, dx, dy, args[1])
args[2] = tform(txx, txy, tyx, tyy, dx, dy, args[2])
}
func tform(txx, txy, tyx, tyy int16, dx, dy fixed.Int26_6, p fixed.Point26_6) fixed.Point26_6 {
const half = 1 << 13
return fixed.Point26_6{
X: dx +
fixed.Int26_6((int64(p.X)*int64(txx)+half)>>14) +
fixed.Int26_6((int64(p.Y)*int64(tyx)+half)>>14),
Y: dy +
fixed.Int26_6((int64(p.X)*int64(txy)+half)>>14) +
fixed.Int26_6((int64(p.Y)*int64(tyy)+half)>>14),
}
}