font/sfnt: parse the cmap table.
Change-Id: I757d42c9caf419f549696543f0f156cfe3dbfe1a Reviewed-on: https://go-review.googlesource.com/35512 Reviewed-by: David Crawshaw <crawshaw@golang.org>
This commit is contained in:
parent
83686c5479
commit
69afd001f7
198
font/sfnt/cmap.go
Normal file
198
font/sfnt/cmap.go
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
// Copyright 2017 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.
|
||||||
|
|
||||||
|
package sfnt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"golang.org/x/text/encoding/charmap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Platform IDs and Platform Specific IDs as per
|
||||||
|
// https://www.microsoft.com/typography/otspec/name.htm
|
||||||
|
const (
|
||||||
|
pidUnicode = 0
|
||||||
|
pidMacintosh = 1
|
||||||
|
pidWindows = 3
|
||||||
|
|
||||||
|
psidUnicode2BMPOnly = 3
|
||||||
|
psidUnicode2FullRepertoire = 4
|
||||||
|
|
||||||
|
psidMacintoshRoman = 0
|
||||||
|
|
||||||
|
psidWindowsUCS2 = 1
|
||||||
|
psidWindowsUCS4 = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
// platformEncodingWidth returns the number of bytes per character assumed by
|
||||||
|
// the given Platform ID and Platform Specific ID.
|
||||||
|
//
|
||||||
|
// Very old fonts, from before Unicode was widely adopted, assume only 1 byte
|
||||||
|
// per character: a character map.
|
||||||
|
//
|
||||||
|
// Old fonts, from when Unicode meant the Basic Multilingual Plane (BMP),
|
||||||
|
// assume that 2 bytes per character is sufficient.
|
||||||
|
//
|
||||||
|
// Recent fonts naturally support the full range of Unicode code points, which
|
||||||
|
// can take up to 4 bytes per character. Such fonts might still choose one of
|
||||||
|
// the legacy encodings if e.g. their repertoire is limited to the BMP, for
|
||||||
|
// greater compatibility with older software, or because the resultant file
|
||||||
|
// size can be smaller.
|
||||||
|
func platformEncodingWidth(pid, psid uint16) int {
|
||||||
|
switch pid {
|
||||||
|
case pidUnicode:
|
||||||
|
switch psid {
|
||||||
|
case psidUnicode2BMPOnly:
|
||||||
|
return 2
|
||||||
|
case psidUnicode2FullRepertoire:
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
|
||||||
|
case pidMacintosh:
|
||||||
|
switch psid {
|
||||||
|
case psidMacintoshRoman:
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
case pidWindows:
|
||||||
|
switch psid {
|
||||||
|
case psidWindowsUCS2:
|
||||||
|
return 2
|
||||||
|
case psidWindowsUCS4:
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// The various cmap formats are described at
|
||||||
|
// https://www.microsoft.com/typography/otspec/cmap.htm
|
||||||
|
|
||||||
|
var supportedCmapFormat = func(format, pid, psid uint16) bool {
|
||||||
|
switch format {
|
||||||
|
case 0:
|
||||||
|
return pid == pidMacintosh && psid == psidMacintoshRoman
|
||||||
|
case 4:
|
||||||
|
return true
|
||||||
|
case 12:
|
||||||
|
// TODO: implement.
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) makeCachedGlyphIndex(buf []byte, offset, length uint32, format uint16) ([]byte, error) {
|
||||||
|
switch format {
|
||||||
|
case 0:
|
||||||
|
return f.makeCachedGlyphIndexFormat0(buf, offset, length)
|
||||||
|
case 4:
|
||||||
|
return f.makeCachedGlyphIndexFormat4(buf, offset, length)
|
||||||
|
case 12:
|
||||||
|
// TODO: implement, including a cmapEntry32 type (32, not 16).
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) makeCachedGlyphIndexFormat0(buf []byte, offset, length uint32) ([]byte, error) {
|
||||||
|
if length != 6+256 || offset+length > f.cmap.length {
|
||||||
|
return nil, errInvalidCmapTable
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
buf, err = f.src.view(buf, int(f.cmap.offset+offset), int(length))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var table [256]byte
|
||||||
|
copy(table[:], buf[6:])
|
||||||
|
f.cached.glyphIndex = func(f *Font, b *Buffer, r rune) (GlyphIndex, error) {
|
||||||
|
// TODO: for this closure to be goroutine-safe, the
|
||||||
|
// golang.org/x/text/encoding/charmap API needs to allocate a new
|
||||||
|
// Encoder and new []byte buffers, for every call to this closure, even
|
||||||
|
// though all we want to do is to encode one rune as one byte. We could
|
||||||
|
// possibly add some fields in the Buffer struct to re-use these
|
||||||
|
// allocations, but a better solution is to improve the charmap API.
|
||||||
|
var dst, src [utf8.UTFMax]byte
|
||||||
|
n := utf8.EncodeRune(src[:], r)
|
||||||
|
_, _, err = charmap.Macintosh.NewEncoder().Transform(dst[:], src[:n], true)
|
||||||
|
if err != nil {
|
||||||
|
// The source rune r is not representable in the Macintosh-Roman encoding.
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return GlyphIndex(table[dst[0]]), nil
|
||||||
|
}
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) makeCachedGlyphIndexFormat4(buf []byte, offset, length uint32) ([]byte, error) {
|
||||||
|
const headerSize = 14
|
||||||
|
if offset+headerSize > f.cmap.length {
|
||||||
|
return nil, errInvalidCmapTable
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
buf, err = f.src.view(buf, int(f.cmap.offset+offset), headerSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
offset += headerSize
|
||||||
|
|
||||||
|
segCount := u16(buf[6:])
|
||||||
|
if segCount&1 != 0 {
|
||||||
|
return nil, errInvalidCmapTable
|
||||||
|
}
|
||||||
|
segCount /= 2
|
||||||
|
if segCount > maxCmapSegments {
|
||||||
|
return nil, errUnsupportedNumberOfCmapSegments
|
||||||
|
}
|
||||||
|
|
||||||
|
eLength := 8*uint32(segCount) + 2
|
||||||
|
if offset+eLength > f.cmap.length {
|
||||||
|
return nil, errInvalidCmapTable
|
||||||
|
}
|
||||||
|
buf, err = f.src.view(buf, int(f.cmap.offset+offset), int(eLength))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
offset += eLength
|
||||||
|
|
||||||
|
entries := make([]cmapEntry16, segCount)
|
||||||
|
for i := range entries {
|
||||||
|
entries[i] = cmapEntry16{
|
||||||
|
end: u16(buf[0*len(entries)+0+2*i:]),
|
||||||
|
start: u16(buf[2*len(entries)+2+2*i:]),
|
||||||
|
delta: u16(buf[4*len(entries)+2+2*i:]),
|
||||||
|
offset: u16(buf[6*len(entries)+2+2*i:]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.cached.glyphIndex = func(f *Font, b *Buffer, r rune) (GlyphIndex, error) {
|
||||||
|
if uint32(r) > 0xffff {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c := uint16(r)
|
||||||
|
for i, j := 0, len(entries); i < j; {
|
||||||
|
h := i + (j-i)/2
|
||||||
|
entry := &entries[h]
|
||||||
|
if c < entry.start {
|
||||||
|
j = h
|
||||||
|
} else if entry.end < c {
|
||||||
|
i = h + 1
|
||||||
|
} else if entry.offset == 0 {
|
||||||
|
return GlyphIndex(c + entry.delta), nil
|
||||||
|
} else {
|
||||||
|
// TODO: support the glyphIdArray as per
|
||||||
|
// https://www.microsoft.com/typography/OTSPEC/cmap.htm
|
||||||
|
//
|
||||||
|
// This will probably use the *Font and *Buffer arguments.
|
||||||
|
return 0, errUnsupportedCmapFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type cmapEntry16 struct {
|
||||||
|
end, start, delta, offset uint16
|
||||||
|
}
|
|
@ -13,6 +13,9 @@ package sfnt // import "golang.org/x/image/font/sfnt"
|
||||||
//
|
//
|
||||||
// The pyftinspect tool from https://github.com/fonttools/fonttools is useful
|
// The pyftinspect tool from https://github.com/fonttools/fonttools is useful
|
||||||
// for inspecting SFNT fonts.
|
// for inspecting SFNT fonts.
|
||||||
|
//
|
||||||
|
// The ttfdump tool is also useful. For example:
|
||||||
|
// ttfdump -t cmap ../testdata/CFFTest.otf dump.txt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -25,6 +28,7 @@ import (
|
||||||
// These constants are not part of the specifications, but are limitations used
|
// These constants are not part of the specifications, but are limitations used
|
||||||
// by this implementation.
|
// by this implementation.
|
||||||
const (
|
const (
|
||||||
|
maxCmapSegments = 1024
|
||||||
maxGlyphDataLength = 64 * 1024
|
maxGlyphDataLength = 64 * 1024
|
||||||
maxHintBits = 256
|
maxHintBits = 256
|
||||||
maxNumTables = 256
|
maxNumTables = 256
|
||||||
|
@ -41,6 +45,7 @@ var (
|
||||||
|
|
||||||
errInvalidBounds = errors.New("sfnt: invalid bounds")
|
errInvalidBounds = errors.New("sfnt: invalid bounds")
|
||||||
errInvalidCFFTable = errors.New("sfnt: invalid CFF table")
|
errInvalidCFFTable = errors.New("sfnt: invalid CFF table")
|
||||||
|
errInvalidCmapTable = errors.New("sfnt: invalid cmap table")
|
||||||
errInvalidGlyphData = errors.New("sfnt: invalid glyph data")
|
errInvalidGlyphData = errors.New("sfnt: invalid glyph data")
|
||||||
errInvalidHeadTable = errors.New("sfnt: invalid head table")
|
errInvalidHeadTable = errors.New("sfnt: invalid head table")
|
||||||
errInvalidLocaTable = errors.New("sfnt: invalid loca table")
|
errInvalidLocaTable = errors.New("sfnt: invalid loca table")
|
||||||
|
@ -54,9 +59,12 @@ var (
|
||||||
errInvalidVersion = errors.New("sfnt: invalid version")
|
errInvalidVersion = errors.New("sfnt: invalid version")
|
||||||
|
|
||||||
errUnsupportedCFFVersion = errors.New("sfnt: unsupported CFF version")
|
errUnsupportedCFFVersion = errors.New("sfnt: unsupported CFF version")
|
||||||
|
errUnsupportedCmapEncodings = errors.New("sfnt: unsupported cmap encodings")
|
||||||
|
errUnsupportedCmapFormat = errors.New("sfnt: unsupported cmap format")
|
||||||
errUnsupportedCompoundGlyph = errors.New("sfnt: unsupported compound glyph")
|
errUnsupportedCompoundGlyph = errors.New("sfnt: unsupported compound glyph")
|
||||||
errUnsupportedGlyphDataLength = errors.New("sfnt: unsupported glyph data length")
|
errUnsupportedGlyphDataLength = errors.New("sfnt: unsupported glyph data length")
|
||||||
errUnsupportedRealNumberEncoding = errors.New("sfnt: unsupported real number encoding")
|
errUnsupportedRealNumberEncoding = errors.New("sfnt: unsupported real number encoding")
|
||||||
|
errUnsupportedNumberOfCmapSegments = errors.New("sfnt: unsupported number of cmap segments")
|
||||||
errUnsupportedNumberOfHints = errors.New("sfnt: unsupported number of hints")
|
errUnsupportedNumberOfHints = errors.New("sfnt: unsupported number of hints")
|
||||||
errUnsupportedNumberOfTables = errors.New("sfnt: unsupported number of tables")
|
errUnsupportedNumberOfTables = errors.New("sfnt: unsupported number of tables")
|
||||||
errUnsupportedPlatformEncoding = errors.New("sfnt: unsupported platform encoding")
|
errUnsupportedPlatformEncoding = errors.New("sfnt: unsupported platform encoding")
|
||||||
|
@ -107,17 +115,6 @@ const (
|
||||||
// display resolution (DPI) and font size (e.g. a 12 point font).
|
// display resolution (DPI) and font size (e.g. a 12 point font).
|
||||||
type Units int32
|
type Units int32
|
||||||
|
|
||||||
// Platform IDs and Platform Specific IDs as per
|
|
||||||
// https://www.microsoft.com/typography/otspec/name.htm
|
|
||||||
const (
|
|
||||||
pidMacintosh = 1
|
|
||||||
pidWindows = 3
|
|
||||||
|
|
||||||
psidMacintoshRoman = 0
|
|
||||||
|
|
||||||
psidWindowsUCS2 = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
func u16(b []byte) uint16 {
|
func u16(b []byte) uint16 {
|
||||||
_ = b[1] // Bounds check hint to compiler.
|
_ = b[1] // Bounds check hint to compiler.
|
||||||
return uint16(b[0])<<8 | uint16(b[1])<<0
|
return uint16(b[0])<<8 | uint16(b[1])<<0
|
||||||
|
@ -286,6 +283,7 @@ type Font struct {
|
||||||
// TODO: hdmx, kern, vmtx? Others?
|
// TODO: hdmx, kern, vmtx? Others?
|
||||||
|
|
||||||
cached struct {
|
cached struct {
|
||||||
|
glyphIndex func(f *Font, b *Buffer, r rune) (GlyphIndex, error)
|
||||||
indexToLocFormat bool // false means short, true means long.
|
indexToLocFormat bool // false means short, true means long.
|
||||||
isPostScript bool
|
isPostScript bool
|
||||||
unitsPerEm Units
|
unitsPerEm Units
|
||||||
|
@ -306,18 +304,36 @@ func (f *Font) initialize() error {
|
||||||
if !f.src.valid() {
|
if !f.src.valid() {
|
||||||
return errInvalidSourceData
|
return errInvalidSourceData
|
||||||
}
|
}
|
||||||
var buf []byte
|
buf, err := f.initializeTables(nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buf, err = f.parseHead(buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buf, err = f.parseMaxp(buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buf, err = f.parseCmap(buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) initializeTables(buf []byte) ([]byte, error) {
|
||||||
// https://www.microsoft.com/typography/otspec/otff.htm "Organization of an
|
// https://www.microsoft.com/typography/otspec/otff.htm "Organization of an
|
||||||
// OpenType Font" says that "The OpenType font starts with the Offset
|
// OpenType Font" says that "The OpenType font starts with the Offset
|
||||||
// Table", which is 12 bytes.
|
// Table", which is 12 bytes.
|
||||||
buf, err := f.src.view(buf, 0, 12)
|
buf, err := f.src.view(buf, 0, 12)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
switch u32(buf) {
|
switch u32(buf) {
|
||||||
default:
|
default:
|
||||||
return errInvalidVersion
|
return nil, errInvalidVersion
|
||||||
case 0x00010000:
|
case 0x00010000:
|
||||||
// No-op.
|
// No-op.
|
||||||
case 0x4f54544f: // "OTTO".
|
case 0x4f54544f: // "OTTO".
|
||||||
|
@ -325,32 +341,32 @@ func (f *Font) initialize() error {
|
||||||
}
|
}
|
||||||
numTables := int(u16(buf[4:]))
|
numTables := int(u16(buf[4:]))
|
||||||
if numTables > maxNumTables {
|
if numTables > maxNumTables {
|
||||||
return errUnsupportedNumberOfTables
|
return nil, errUnsupportedNumberOfTables
|
||||||
}
|
}
|
||||||
|
|
||||||
// "The Offset Table is followed immediately by the Table Record entries...
|
// "The Offset Table is followed immediately by the Table Record entries...
|
||||||
// sorted in ascending order by tag", 16 bytes each.
|
// sorted in ascending order by tag", 16 bytes each.
|
||||||
buf, err = f.src.view(buf, 12, 16*numTables)
|
buf, err = f.src.view(buf, 12, 16*numTables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
for b, first, prevTag := buf, true, uint32(0); len(b) > 0; b = b[16:] {
|
for b, first, prevTag := buf, true, uint32(0); len(b) > 0; b = b[16:] {
|
||||||
tag := u32(b)
|
tag := u32(b)
|
||||||
if first {
|
if first {
|
||||||
first = false
|
first = false
|
||||||
} else if tag <= prevTag {
|
} else if tag <= prevTag {
|
||||||
return errInvalidTableTagOrder
|
return nil, errInvalidTableTagOrder
|
||||||
}
|
}
|
||||||
prevTag = tag
|
prevTag = tag
|
||||||
|
|
||||||
o, n := u32(b[8:12]), u32(b[12:16])
|
o, n := u32(b[8:12]), u32(b[12:16])
|
||||||
if o > maxTableOffset || n > maxTableLength {
|
if o > maxTableOffset || n > maxTableLength {
|
||||||
return errUnsupportedTableOffsetLength
|
return nil, errUnsupportedTableOffsetLength
|
||||||
}
|
}
|
||||||
// We ignore the checksums, but "all tables must begin on four byte
|
// We ignore the checksums, but "all tables must begin on four byte
|
||||||
// boundries [sic]".
|
// boundries [sic]".
|
||||||
if o&3 != 0 {
|
if o&3 != 0 {
|
||||||
return errInvalidTableOffset
|
return nil, errInvalidTableOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match the 4-byte tag as a uint32. For example, "OS/2" is 0x4f532f32.
|
// Match the 4-byte tag as a uint32. For example, "OS/2" is 0x4f532f32.
|
||||||
|
@ -379,40 +395,109 @@ func (f *Font) initialize() error {
|
||||||
f.post = table{o, n}
|
f.post = table{o, n}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
var u uint16
|
func (f *Font) parseCmap(buf []byte) ([]byte, error) {
|
||||||
|
// https://www.microsoft.com/typography/OTSPEC/cmap.htm
|
||||||
|
|
||||||
// https://www.microsoft.com/typography/otspec/head.htm
|
const headerSize, entrySize = 4, 8
|
||||||
if f.head.length != 54 {
|
if f.cmap.length < headerSize {
|
||||||
return errInvalidHeadTable
|
return nil, errInvalidCmapTable
|
||||||
}
|
}
|
||||||
u, err = f.src.u16(buf, f.head, 18)
|
u, err := f.src.u16(buf, f.cmap, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
|
}
|
||||||
|
numSubtables := int(u)
|
||||||
|
if f.cmap.length < headerSize+entrySize*uint32(numSubtables) {
|
||||||
|
return 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, 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, errInvalidCmapTable
|
||||||
|
}
|
||||||
|
buf, err = f.src.view(buf, int(f.cmap.offset+offset), 4)
|
||||||
|
if err != nil {
|
||||||
|
return 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, errUnsupportedCmapEncodings
|
||||||
|
}
|
||||||
|
return f.makeCachedGlyphIndex(buf, bestOffset, bestLength, bestFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) parseHead(buf []byte) ([]byte, error) {
|
||||||
|
// https://www.microsoft.com/typography/otspec/head.htm
|
||||||
|
|
||||||
|
if f.head.length != 54 {
|
||||||
|
return nil, errInvalidHeadTable
|
||||||
|
}
|
||||||
|
u, err := f.src.u16(buf, f.head, 18)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if u == 0 {
|
if u == 0 {
|
||||||
return errInvalidHeadTable
|
return nil, errInvalidHeadTable
|
||||||
}
|
}
|
||||||
f.cached.unitsPerEm = Units(u)
|
f.cached.unitsPerEm = Units(u)
|
||||||
u, err = f.src.u16(buf, f.head, 50)
|
u, err = f.src.u16(buf, f.head, 50)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
f.cached.indexToLocFormat = u != 0
|
f.cached.indexToLocFormat = u != 0
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) parseMaxp(buf []byte) ([]byte, error) {
|
||||||
// https://www.microsoft.com/typography/otspec/maxp.htm
|
// https://www.microsoft.com/typography/otspec/maxp.htm
|
||||||
|
|
||||||
if f.cached.isPostScript {
|
if f.cached.isPostScript {
|
||||||
if f.maxp.length != 6 {
|
if f.maxp.length != 6 {
|
||||||
return errInvalidMaxpTable
|
return nil, errInvalidMaxpTable
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if f.maxp.length != 32 {
|
if f.maxp.length != 32 {
|
||||||
return errInvalidMaxpTable
|
return nil, errInvalidMaxpTable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
u, err = f.src.u16(buf, f.maxp, 4)
|
u, err := f.src.u16(buf, f.maxp, 4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
numGlyphs := int(u)
|
numGlyphs := int(u)
|
||||||
|
|
||||||
|
@ -425,23 +510,36 @@ func (f *Font) initialize() error {
|
||||||
}
|
}
|
||||||
f.cached.locations, err = p.parse()
|
f.cached.locations, err = p.parse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
f.cached.locations, err = parseLoca(
|
f.cached.locations, err = parseLoca(
|
||||||
&f.src, f.loca, f.glyf.offset, f.cached.indexToLocFormat, numGlyphs)
|
&f.src, f.loca, f.glyf.offset, f.cached.indexToLocFormat, numGlyphs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(f.cached.locations) != numGlyphs+1 {
|
if len(f.cached.locations) != numGlyphs+1 {
|
||||||
return errInvalidLocationData
|
return nil, errInvalidLocationData
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
return buf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: func (f *Font) GlyphIndex(r rune) (x GlyphIndex, ok bool)
|
// TODO: API for looking up glyph variants?? For example, some fonts may
|
||||||
// This will require parsing the cmap table.
|
// provide both slashed and dotted zero glyphs ('0'), or regular and 'old
|
||||||
|
// style' numerals, and users can direct software to choose a variant.
|
||||||
|
|
||||||
|
// 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) ([]byte, error) {
|
func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) ([]byte, error) {
|
||||||
xx := int(x)
|
xx := int(x)
|
||||||
|
@ -512,14 +610,14 @@ func (f *Font) Name(b *Buffer, id NameID) (string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
nSubtables := u16(buf[2:])
|
numSubtables := u16(buf[2:])
|
||||||
if f.name.length < headerSize+entrySize*uint32(nSubtables) {
|
if f.name.length < headerSize+entrySize*uint32(numSubtables) {
|
||||||
return "", errInvalidNameTable
|
return "", errInvalidNameTable
|
||||||
}
|
}
|
||||||
stringOffset := u16(buf[4:])
|
stringOffset := u16(buf[4:])
|
||||||
|
|
||||||
seen := false
|
seen := false
|
||||||
for i, n := 0, int(nSubtables); i < n; i++ {
|
for i, n := 0, int(numSubtables); i < n; i++ {
|
||||||
buf, err := b.view(&f.src, int(f.name.offset)+headerSize+entrySize*i, entrySize)
|
buf, err := b.view(&f.src, int(f.name.offset)+headerSize+entrySize*i, entrySize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
@ -88,6 +88,74 @@ func testTrueType(t *testing.T, f *Font) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGlyphIndex(t *testing.T) {
|
||||||
|
data, err := ioutil.ReadFile(filepath.FromSlash("../testdata/CFFTest.otf"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, format := range []int{-1, 0, 4} {
|
||||||
|
testGlyphIndex(t, data, format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGlyphIndex(t *testing.T, data []byte, cmapFormat int) {
|
||||||
|
if cmapFormat >= 0 {
|
||||||
|
originalSupportedCmapFormat := supportedCmapFormat
|
||||||
|
defer func() {
|
||||||
|
supportedCmapFormat = originalSupportedCmapFormat
|
||||||
|
}()
|
||||||
|
supportedCmapFormat = func(format, pid, psid uint16) bool {
|
||||||
|
return int(format) == cmapFormat && originalSupportedCmapFormat(format, pid, psid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := Parse(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cmapFormat=%d: %v", cmapFormat, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
r rune
|
||||||
|
want GlyphIndex
|
||||||
|
}{
|
||||||
|
{'0', 1},
|
||||||
|
{'1', 2},
|
||||||
|
{'Q', 3},
|
||||||
|
// TODO: add the U+00E0 non-ASCII Latin-1 Supplement rune to
|
||||||
|
// CFFTest.otf and change 0 to something non-zero.
|
||||||
|
{'\u00e0', 0},
|
||||||
|
{'\u4e2d', 4},
|
||||||
|
// TODO: add a rune >= U+00010000 to CFFTest.otf?
|
||||||
|
|
||||||
|
// Glyphs that aren't present in CFFTest.otf.
|
||||||
|
{'?', 0},
|
||||||
|
{'\ufffd', 0},
|
||||||
|
{'\U0001f4a9', 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
var b Buffer
|
||||||
|
for _, tc := range testCases {
|
||||||
|
want := tc.want
|
||||||
|
// cmap format 0, with the Macintosh Roman encoding, can't represent
|
||||||
|
// U+4E2D.
|
||||||
|
if cmapFormat == 0 && tc.r == '\u4e2d' {
|
||||||
|
want = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := f.GlyphIndex(&b, tc.r)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cmapFormat=%d, r=%q: %v", cmapFormat, tc.r, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("cmapFormat=%d, r=%q: got %d, want %d", cmapFormat, tc.r, got, want)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPostScriptSegments(t *testing.T) {
|
func TestPostScriptSegments(t *testing.T) {
|
||||||
// wants' vectors correspond 1-to-1 to what's in the CFFTest.sfd file,
|
// wants' vectors correspond 1-to-1 to what's in the CFFTest.sfd file,
|
||||||
// although OpenType/CFF and FontForge's SFD have reversed orders.
|
// although OpenType/CFF and FontForge's SFD have reversed orders.
|
||||||
|
@ -226,7 +294,7 @@ func TestTrueTypeSegments(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSegments(t *testing.T, filename string, wants [][]Segment) {
|
func testSegments(t *testing.T, filename string, wants [][]Segment) {
|
||||||
data, err := ioutil.ReadFile(filepath.Join("..", "testdata", filename))
|
data, err := ioutil.ReadFile(filepath.FromSlash("../testdata/" + filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user