font/sfnt: add Font.WriteSourceTo
See https://groups.google.com/g/golang-nuts/c/bilOyOz_SCc/m/kih7w0x4AgAJ
"x/image/font: Font serialization" for the motivation.
Change-Id: Iba05b4251f1dc91783eb3c43d78fb3ba79b0d912
Reviewed-on: https://go-review.googlesource.com/c/image/+/291149
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Nigel Tao <nigeltao@golang.org>
Trust: Nigel Tao <nigeltao@golang.org>
diff --git a/font/sfnt/sfnt.go b/font/sfnt/sfnt.go
index 2ee621f..c668b3f 100644
--- a/font/sfnt/sfnt.go
+++ b/font/sfnt/sfnt.go
@@ -106,6 +106,7 @@
errUnsupportedCFFVersion = errors.New("sfnt: unsupported CFF version")
errUnsupportedClassDefFormat = errors.New("sfnt: unsupported class definition format")
errUnsupportedCmapEncodings = errors.New("sfnt: unsupported cmap encodings")
+ errUnsupportedCollection = errors.New("sfnt: unsupported collection")
errUnsupportedCompoundGlyph = errors.New("sfnt: unsupported compound glyph")
errUnsupportedCoverageFormat = errors.New("sfnt: unsupported coverage format")
errUnsupportedExtensionPosFormat = errors.New("sfnt: unsupported extension positioning format")
@@ -320,6 +321,9 @@
//
// 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.
+//
+// The caller should not modify src while the Collection or its Fonts remain in
+// use.
func ParseCollection(src []byte) (*Collection, error) {
c := &Collection{src: source{b: src}}
if err := c.initialize(); err != nil {
@@ -333,6 +337,9 @@
//
// 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.
+//
+// The caller should not modify or close src while the Collection or its Fonts
+// remain in use.
func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) {
c := &Collection{src: source{r: src}}
if err := c.initialize(); err != nil {
@@ -512,6 +519,8 @@
// Parse parses an SFNT font, such as TTF or OTF data, from a []byte data
// source.
+//
+// The caller should not modify src while the Font remains in use.
func Parse(src []byte) (*Font, error) {
f := &Font{src: source{b: src}}
if err := f.initialize(0, false); err != nil {
@@ -522,6 +531,8 @@
// ParseReaderAt parses an SFNT font, such as TTF or OTF data, from an
// io.ReaderAt data source.
+//
+// The caller should not modify or close src while the Font remains in use.
func ParseReaderAt(src io.ReaderAt) (*Font, error) {
f := &Font{src: source{r: src}}
if err := f.initialize(0, false); err != nil {
@@ -561,6 +572,10 @@
type Font struct {
src source
+ // initialOffset is the file offset of the start of the font. This may be
+ // non-zero for fonts within a font collection.
+ initialOffset int32
+
// https://www.microsoft.com/typography/otspec/otff.htm#otttables
// "Required Tables".
cmap table
@@ -607,6 +622,7 @@
cached struct {
ascent int32
capHeight int32
+ finalTableOffset int32
glyphData glyphData
glyphIndex glyphIndexFunc
bounds [4]int16
@@ -636,7 +652,7 @@
if !f.src.valid() {
return errInvalidSourceData
}
- buf, isPostScript, err := f.initializeTables(offset, isDfont)
+ buf, finalTableOffset, isPostScript, err := f.initializeTables(offset, isDfont)
if err != nil {
return err
}
@@ -692,6 +708,7 @@
f.cached.ascent = ascent
f.cached.capHeight = capHeight
+ f.cached.finalTableOffset = finalTableOffset
f.cached.glyphData = glyphData
f.cached.glyphIndex = glyphIndex
f.cached.bounds = bounds
@@ -721,21 +738,25 @@
return nil
}
-func (f *Font) initializeTables(offset int, isDfont bool) (buf1 []byte, isPostScript bool, err error) {
+func (f *Font) initializeTables(offset int, isDfont bool) (buf1 []byte, finalTableOffset int32, isPostScript bool, err error) {
+ f.initialOffset = int32(offset)
+ if int(f.initialOffset) != offset {
+ return nil, 0, false, errUnsupportedTableOffsetLength
+ }
// 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
+ return nil, 0, false, err
}
// When updating the cases in this switch statement, also update the
// Collection.initialize method.
switch u32(buf) {
default:
- return nil, false, errInvalidFont
+ return nil, 0, false, errInvalidFont
case dfontResourceDataOffset:
- return nil, false, errInvalidSingleFont
+ return nil, 0, false, errInvalidSingleFont
case 0x00010000:
// No-op.
case 0x4f54544f: // "OTTO".
@@ -743,25 +764,25 @@
case 0x74727565: // "true"
// No-op.
case 0x74746366: // "ttcf".
- return nil, false, errInvalidSingleFont
+ return nil, 0, false, errInvalidSingleFont
}
numTables := int(u16(buf[4:]))
if numTables > maxNumTables {
- return nil, false, errUnsupportedNumberOfTables
+ return nil, 0, 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
+ return nil, 0, 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
+ return nil, 0, false, errInvalidTableTagOrder
}
prevTag = tag
@@ -772,16 +793,19 @@
origO := o
o += uint32(offset)
if o < origO {
- return nil, false, errUnsupportedTableOffsetLength
+ return nil, 0, false, errUnsupportedTableOffsetLength
}
}
if o > maxTableOffset || n > maxTableLength {
- return nil, false, errUnsupportedTableOffsetLength
+ return nil, 0, false, errUnsupportedTableOffsetLength
}
// We ignore the checksums, but "all tables must begin on four byte
// boundries [sic]".
if o&3 != 0 {
- return nil, false, errInvalidTableOffset
+ return nil, 0, false, errInvalidTableOffset
+ }
+ if finalTableOffset < int32(o+n) {
+ finalTableOffset = int32(o + n)
}
// Match the 4-byte tag as a uint32. For example, "OS/2" is 0x4f532f32.
@@ -816,7 +840,11 @@
f.post = table{o, n}
}
}
- return buf, isPostScript, nil
+
+ if (f.src.b != nil) && (int(finalTableOffset) > len(f.src.b)) {
+ return nil, 0, false, errInvalidSourceData
+ }
+ return buf, finalTableOffset, isPostScript, nil
}
func (f *Font) parseCmap(buf []byte) (buf1 []byte, glyphIndex glyphIndexFunc, err error) {
@@ -1689,6 +1717,63 @@
return m, nil
}
+// WriteSourceTo writes the source data (the []byte or io.ReaderAt passed to
+// Parse or ParseReaderAt) to w.
+//
+// It returns the number of bytes written. On success, this is the final offset
+// of the furthest SFNT table in the source. This may be less than the length
+// of the []byte or io.ReaderAt originally passed.
+func (f *Font) WriteSourceTo(b *Buffer, w io.Writer) (int64, error) {
+ if f.initialOffset != 0 {
+ // TODO: when extracting a single font (i.e. TTF) out of a font
+ // collection (i.e. TTC), write only the i'th font and not the (i-1)
+ // previous fonts. Subtly, in the file format, table offsets may be
+ // relative to the start of the resource (for dfont collections) or the
+ // start of the file (otherwise). If we were to extract a single font
+ // here, we might need to dynamically patch the table offsets, bearing
+ // in mind that f.src.b is conceptually a 'read-only' slice of bytes.
+ return 0, errUnsupportedCollection
+ }
+
+ if f.src.b != nil {
+ n, err := w.Write(f.src.b[:f.cached.finalTableOffset])
+ return int64(n), err
+ }
+
+ // We have an io.ReaderAt source, not a []byte. It is tempting to see if
+ // the io.ReaderAt optionally implements the io.WriterTo interface, but we
+ // don't for two reasons:
+ // - We want to write exactly f.cached.finalTableOffset bytes, even if the
+ // underlying 'file' is larger, to be consistent with the []byte flavor.
+ // - We document that "Font methods are safe to call concurrently" and
+ // while io.ReaderAt is stateless (the offset is an argument), the
+ // io.Reader / io.Writer abstractions are stateful (the current position
+ // is a field) and mutable state generally isn't concurrent-safe.
+
+ if b == nil {
+ b = &Buffer{}
+ }
+ finalTableOffset := int(f.cached.finalTableOffset)
+ numBytesWritten := int64(0)
+ for offset := 0; offset < finalTableOffset; {
+ length := finalTableOffset - offset
+ if length > 4096 {
+ length = 4096
+ }
+ view, err := b.view(&f.src, offset, length)
+ if err != nil {
+ return numBytesWritten, err
+ }
+ n, err := w.Write(view)
+ numBytesWritten += int64(n)
+ if err != nil {
+ return numBytesWritten, err
+ }
+ offset += length
+ }
+ return numBytesWritten, nil
+}
+
// Name returns the name value keyed by the given NameID.
//
// It returns ErrNotFound if there is no value for that key.
diff --git a/font/sfnt/sfnt_test.go b/font/sfnt/sfnt_test.go
index 315ab16..a8efa49 100644
--- a/font/sfnt/sfnt_test.go
+++ b/font/sfnt/sfnt_test.go
@@ -125,7 +125,7 @@
if err != nil {
t.Fatalf("Parse: %v", err)
}
- testTrueType(t, f)
+ testTrueType(t, f, goregular.TTF)
}
func TestTrueTypeParseReaderAt(t *testing.T) {
@@ -133,10 +133,10 @@
if err != nil {
t.Fatalf("ParseReaderAt: %v", err)
}
- testTrueType(t, f)
+ testTrueType(t, f, goregular.TTF)
}
-func testTrueType(t *testing.T, f *Font) {
+func testTrueType(t *testing.T, f *Font, wantSrc []byte) {
if got, want := f.UnitsPerEm(), Units(2048); got != want {
t.Errorf("UnitsPerEm: got %d, want %d", got, want)
}
@@ -146,6 +146,14 @@
if got, want := f.NumGlyphs(), 650; got <= want {
t.Errorf("NumGlyphs: got %d, want > %d", got, want)
}
+ buf := &bytes.Buffer{}
+ if n, err := f.WriteSourceTo(nil, buf); err != nil {
+ t.Fatalf("WriteSourceTo: %v", err)
+ } else if n != int64(len(wantSrc)) {
+ t.Fatalf("WriteSourceTo: got %d, want %d", n, len(wantSrc))
+ } else if gotSrc := buf.Bytes(); !bytes.Equal(gotSrc, wantSrc) {
+ t.Fatalf("WriteSourceTo: contents differ")
+ }
}
func fontData(name string) []byte {