Skip to content

Commit 7c6958f

Browse files
committed
introduced FontSubsetter interface
1 parent 88b6ab1 commit 7c6958f

File tree

8 files changed

+165
-119
lines changed

8 files changed

+165
-119
lines changed

font.go

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,36 @@ import (
44
"io"
55
"os"
66

7+
"github.com/cdillond/gdf/font"
78
"golang.org/x/image/font/sfnt"
89
"golang.org/x/text/encoding"
910
"golang.org/x/text/encoding/charmap"
1011
)
1112

12-
// A FontSubsetFunc is intended to give the user control over how a Font is subset when it is embedded in the output PDF.
13-
// By default, gdf uses the TTFSubset function from github.com/cdillond/gdf/font, but there are good reasons to choose a different
14-
// implementation. For example, Harfbuzz's hb-subset tool and Microsoft's Win32 CreateFontPackage are robust alternatives
15-
// written in C++ that can be wrapped by a user-defined FontSubsetFunc. If you do not want the embedded font to be subset
16-
// at all, you can define a function that simply returns src and a nil error. A FontSubsetFunc should not alter the glyph
17-
// ID of any rune in the cutset. It must also be sure to include a .notdef glyph.
18-
type FontSubsetFunc func(f *sfnt.Font, src []byte, cutset map[rune]struct{}) ([]byte, error)
13+
// A FontSubsetter is intended to give the user control over how a Font is subset when it is embedded in the output PDF.
14+
// By default, gdf uses the DefaultSubsetter, which is equivalent to the BasicSubsetter type from github.com/cdillond/gdf/font,
15+
// but there are good reasons to choose a different implementation (the gdf/font package provides several).
16+
// For example, Harfbuzz's hb-subset tool and Microsoft's Win32 CreateFontPackage are robust alternatives written in C++
17+
// that can be wrapped by a user-defined FontSubsetter.
18+
// If you do not want the embedded font to be subset at all, you can set the font's FontSubsetter to nil.
19+
// A FontSubsetter should not alter the glyph ID of any rune in the cutset. It must also be sure to include a .notdef glyph.
20+
type FontSubsetter interface {
21+
Init(SFNT *sfnt.Font, src []byte, path string)
22+
Subset(cutset map[rune]struct{}) ([]byte, error)
23+
}
24+
25+
type DefaultSubsetter struct {
26+
font.BasicSubsetter
27+
}
1928

2029
const ppem = 1000
2130

2231
// A Font represents a TrueType/OpenType font. Any given Font struct should be used on at most 1 PDF. To use the same underlying
2332
// font on multiple PDF files, derive a new Font struct from the source font file or bytes for each PDF.
2433
type Font struct {
25-
SFNT *sfnt.Font // The source TrueType or OpenType font.
26-
FontSubsetFunc // If nil, gdf's default is used.
34+
SFNT *sfnt.Font // The source TrueType or OpenType font.
35+
Subsetter FontSubsetter
36+
2737
*simpleFD
2838

2939
refnum int
@@ -38,22 +48,22 @@ type Font struct {
3848
source *stream
3949
buf *sfnt.Buffer
4050
srcb []byte
51+
srcPath string
4152
}
4253

4354
// LoadTrueType returns a *Font object, which can be used for drawing text to a ContentStream or XObject, and an error.
4455
func LoadTrueType(b []byte, flag FontFlag) (*Font, error) {
45-
//b2 := make([]byte, len(b))
46-
//copy(b2, b)
4756
b2 := b
4857
fnt, err := sfnt.Parse(b)
4958
if err != nil {
5059
return nil, err
5160
}
5261
out := &Font{
53-
subtype: "/TrueType",
54-
encName: "/WinAnsiEncoding",
55-
charset: make(map[rune]int),
56-
enc: charmap.Windows1252.NewEncoder(),
62+
Subsetter: new(DefaultSubsetter),
63+
subtype: "/TrueType",
64+
encName: "/WinAnsiEncoding",
65+
charset: make(map[rune]int),
66+
enc: charmap.Windows1252.NewEncoder(),
5767
source: &stream{
5868
Filter: Flate,
5969
},
@@ -79,7 +89,11 @@ func LoadTrueTypeFile(path string, flag FontFlag) (*Font, error) {
7989
if err != nil {
8090
return nil, err
8191
}
82-
return LoadTrueType(b, flag)
92+
f, err := LoadTrueType(b, flag)
93+
if err != nil {
94+
f.srcPath = path
95+
}
96+
return f, err
8397
}
8498

8599
type simpleFD struct {

font/hb.go

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,34 @@ import (
77
"fmt"
88
"os/exec"
99
"strconv"
10-
11-
"golang.org/x/image/font/sfnt"
1210
)
1311

14-
// HBSubsetPath returns a func that can be used as a gdf.FontSubsetFunc. The returned func
15-
// is similar to HBSubset and has the same limitations. The path parameter represents the
16-
// path of the source font.
17-
func HBSubsetPath(path string) func(*sfnt.Font, []byte, map[rune]struct{}) ([]byte, error) {
18-
return func(_ *sfnt.Font, _ []byte, cutset map[rune]struct{}) ([]byte, error) {
19-
u := make([]byte, 0, 512)
20-
for key := range cutset {
21-
u = strconv.AppendInt(u, int64(key), 16)
22-
u = append(u, ',')
23-
}
24-
if len(u) < 1 {
25-
return nil, fmt.Errorf("cutset is too small")
26-
}
27-
cmd := exec.Command("hb-subset",
28-
"--font-file="+path, // must be passed explicitly as an arg
29-
"-u", string(u[:len(u)-1]),
30-
"--retain-gids",
31-
"-o", "/dev/stdout", // ditto for stdout
32-
)
33-
return cmd.Output()
12+
// HBSubset returns the source bytes of a font containing only the characters included in the cutset.
13+
// The path parameter represents the path of the source font. This function has the same limitations as HBSubset.
14+
func HBSubsetPath(path string, cutset map[rune]struct{}) ([]byte, error) {
15+
u := make([]byte, 0, 512)
16+
for key := range cutset {
17+
u = strconv.AppendInt(u, int64(key), 16)
18+
u = append(u, ',')
19+
}
20+
if len(u) < 1 {
21+
return nil, fmt.Errorf("cutset is too small")
3422
}
23+
cmd := exec.Command("hb-subset",
24+
"--font-file="+path, // must be passed explicitly as an arg
25+
"-u", string(u[:len(u)-1]),
26+
"--retain-gids",
27+
"-o", "/dev/stdout", // ditto for stdout
28+
)
29+
return cmd.Output()
3530
}
3631

37-
// HBSubset can be used as a gdf.FontSubsetFunc. For this function to work, the HarfBuzz hb-subset
38-
// tool must be installed. The HBSubset func may handle edge cases that the TTFSubset func does not. hb-subset
32+
// HBSubset returns the source bytes of a font containing only the characters included in the cutset.
33+
// For this function to work, the HarfBuzz hb-subsettool must be installed.
34+
// The HBSubset func may handle edge cases that the TTFSubset func does not. hb-subset
3935
// has a mature, well-tested API and is capable of handling more font formats than TTFSubset.
4036
// However, this approach requires os/exec, so it might not be suitable for all environments.
41-
func HBSubset(_ *sfnt.Font, src []byte, cutset map[rune]struct{}) ([]byte, error) {
37+
func HBSubset(src []byte, cutset map[rune]struct{}) ([]byte, error) {
4238
u := make([]byte, 0, 512)
4339
for key := range cutset {
4440
u = strconv.AppendInt(u, int64(key), 16)

font/hb_windows.go

Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,60 +7,56 @@ import (
77
"os"
88
"os/exec"
99
"strconv"
10-
11-
"golang.org/x/image/font/sfnt"
1210
)
1311

14-
// HBSubsetPath returns a func that can be used as a gdf.FontSubsetFunc. The returned func
15-
// is similar to HBSubset and has the same limitations. The path parameter represents the
16-
// path of the source font.
17-
func HBSubsetPath(path string) func(*sfnt.Font, []byte, map[rune]struct{}) ([]byte, error) {
18-
return func(_ *sfnt.Font, _ []byte, cutset map[rune]struct{}) ([]byte, error) {
19-
var out *os.File
20-
var err error
21-
outPath := "tmp-font-out"
12+
// HBSubset returns the source bytes of a font containing only the characters included in the cutset.
13+
// The path parameter represents the path of the source font. This function has the same limitations as HBSubset.
14+
func HBSubsetPath(path string, cutset map[rune]struct{}) ([]byte, error) {
15+
var out *os.File
16+
var err error
17+
outPath := "tmp-font-out"
2218

23-
// make sure we're not overwriting anything
24-
var i int
25-
for _, err = os.Stat(outPath); err == nil; i++ {
26-
outPath += strconv.Itoa(i)
27-
}
28-
// set up output file
29-
if out, err = os.Create(outPath); err != nil {
30-
return nil, err
31-
}
32-
defer os.Remove(outPath)
33-
if err = out.Close(); err != nil {
34-
return nil, err
35-
}
19+
// make sure we're not overwriting anything
20+
var i int
21+
for _, err = os.Stat(outPath); err == nil; i++ {
22+
outPath += strconv.Itoa(i)
23+
}
24+
// set up output file
25+
if out, err = os.Create(outPath); err != nil {
26+
return nil, err
27+
}
28+
defer os.Remove(outPath)
29+
if err = out.Close(); err != nil {
30+
return nil, err
31+
}
3632

37-
u := make([]byte, 0, 512)
38-
for key := range cutset {
39-
u = strconv.AppendInt(u, int64(key), 16)
40-
u = append(u, ',')
41-
}
42-
if len(u) < 1 {
43-
return nil, fmt.Errorf("cutset is too small")
44-
}
45-
cmd := exec.Command("hb-subset",
46-
"--font-file="+path, // must be passed explicitly as an arg
47-
"-u", string(u[:len(u)-1]),
48-
"--retain-gids",
49-
"-o", outPath, // ditto for output
50-
)
51-
if err = cmd.Run(); err != nil {
52-
return nil, err
53-
}
54-
return os.ReadFile(outPath)
33+
u := make([]byte, 0, 512)
34+
for key := range cutset {
35+
u = strconv.AppendInt(u, int64(key), 16)
36+
u = append(u, ',')
37+
}
38+
if len(u) < 1 {
39+
return nil, fmt.Errorf("cutset is too small")
40+
}
41+
cmd := exec.Command("hb-subset",
42+
"--font-file="+path, // must be passed explicitly as an arg
43+
"-u", string(u[:len(u)-1]),
44+
"--retain-gids",
45+
"-o", outPath, // ditto for output
46+
)
47+
if err = cmd.Run(); err != nil {
48+
return nil, err
5549
}
50+
return os.ReadFile(outPath)
51+
5652
}
5753

58-
// HBSubset can be used as a gdf.FontSubsetFunc.
59-
// For this function to work, the HarfBuzz hb-subset tool must be installed.
54+
// HBSubset returns the source bytes of a font containing only the characters included in the cutset.
55+
// For this function to work, the HarfBuzz hb-subsettool must be installed.
6056
// The HBSubset func may handle edge cases that the TTFSubset func does not. hb-subset
6157
// has a mature, well-tested API and is capable of handling more font formats than TTFSubset.
62-
// However, this approach requires os/exec, so it might not be suitable for all environments.
63-
func HBSubset(_ *sfnt.Font, src []byte, cutset map[rune]struct{}) ([]byte, error) {
58+
// However, this approach requires os/exec, so it might not be suitable for all environments..
59+
func HBSubset(src []byte, cutset map[rune]struct{}) ([]byte, error) {
6460
// Instead of using /dev/stdin and /dev/stdout, on Windows, this function creates temp files.
6561
// It's unclear if this is necessary, but the hb-subset tool is finnicky.
6662
var in, out *os.File

font/hbc.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@
22

33
package font
44

5-
import (
6-
"golang.org/x/image/font/sfnt"
7-
)
5+
import "fmt"
86

9-
// HBSubsetC can be used as a gdf.FontSubsetFunc. It calls functions in libharfbuzz and libharfbuzz-subset via CGo. In order
10-
// for this function to work, CGo must be enabled, HarfBuzz v>=2.9.0 must be installed on your system, and `hbsubsetc` must be passed
11-
// as a build tag to the Go compiler. If these conditions are not met, the function instead calls HBSubset (which may fail if hb-subset)
12-
// has not been installed.
13-
func HBSubsetC(_ *sfnt.Font, src []byte, charset map[rune]struct{}) ([]byte, error) {
14-
return HBSubset(nil, src, charset)
7+
// HBSubsetC calls functions in libharfbuzz and libharfbuzz-subset via CGo and returns the source bytes of a font containing only the
8+
// characters included in the cutset. In order for this function to work, CGo must be enabled, HarfBuzz v>=2.9.0 must be installed on
9+
// your system, and `hbsubsetc` must be passed to the Go compiler as a build tag.
10+
func HBSubsetC(src []byte, charset map[rune]struct{}) ([]byte, error) {
11+
return nil, fmt.Errorf("HBSubetC is not enabled; use the build tag hbsubsetc and consult github.com/cdillond/gdf/font for further information")
1512
}

font/hbc_cgo.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,15 @@ import "C"
4848
import (
4949
"fmt"
5050
"unsafe"
51-
52-
"golang.org/x/image/font/sfnt"
5351
)
5452

55-
// HBSubsetC can be used as a gdf.FontSubsetFunc. It calls functions in libharfbuzz and libharfbuzz-subset via CGo. In order
56-
// for this function to work, CGo must be enabled, HarfBuzz v>=2.9.0 must be installed on your system, and `hbsubsetc` must be passed
57-
// as a build tag to the Go compiler. If these conditions are not met, the function instead calls HBSubset (which may fail if hb-subset)
58-
// has not been installed.
59-
func HBSubsetC(_ *sfnt.Font, src []byte, charset map[rune]struct{}) ([]byte, error) {
53+
// HBSubsetC calls functions in libharfbuzz and libharfbuzz-subset via CGo and returns the source bytes of a font containing only the
54+
// characters included in the cutset. In order for this function to work, CGo must be enabled, HarfBuzz v>=2.9.0 must be installed on
55+
// your system, and `hbsubsetc` must be passed to the Go compiler as a build tag.
56+
func HBSubsetC(src []byte, cutset map[rune]struct{}) ([]byte, error) {
6057
// convert runes to uint32_t chars readable by hb-subset
61-
charset_u32 := make([]uint32, len(charset))
62-
for char := range charset {
58+
charset_u32 := make([]uint32, len(cutset))
59+
for char := range cutset {
6360
charset_u32 = append(charset_u32, uint32(char))
6461
}
6562
// allocate at least as much as the current file size

font/subsetter.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package font
2+
3+
import (
4+
"fmt"
5+
6+
"golang.org/x/image/font/sfnt"
7+
)
8+
9+
type BasicSubsetter struct {
10+
sFNT *sfnt.Font
11+
src []byte
12+
}
13+
14+
func (b *BasicSubsetter) Subset(cutset map[rune]struct{}) ([]byte, error) {
15+
return TTFSubset(b.sFNT, b.src, cutset)
16+
}
17+
func (b *BasicSubsetter) Init(SFNT *sfnt.Font, src []byte, _ string) {
18+
b.sFNT = SFNT
19+
b.src = src
20+
}
21+
22+
type HarfBuzzSubsetter struct {
23+
path string // Path to the font file; may be empty if Src is non-nil
24+
src []byte // Font source bytes; may be nil if Path is not empty
25+
}
26+
27+
func (h *HarfBuzzSubsetter) Subset(cutset map[rune]struct{}) ([]byte, error) {
28+
if h.path != "" {
29+
return HBSubsetPath(h.path, cutset)
30+
}
31+
if h.src != nil {
32+
return HBSubset(h.src, cutset)
33+
}
34+
return nil, fmt.Errorf("no font source data provided")
35+
}
36+
37+
func (h *HarfBuzzSubsetter) Init(_ *sfnt.Font, src []byte, path string) {
38+
h.path = path
39+
h.src = src
40+
}
41+
42+
type HarfBuzzCGoSubsetter struct {
43+
src []byte // Font source bytes
44+
}
45+
46+
func (h *HarfBuzzCGoSubsetter) Subset(cutset map[rune]struct{}) ([]byte, error) {
47+
return HBSubsetC(h.src, cutset)
48+
}
49+
50+
func (h *HarfBuzzCGoSubsetter) Init(_ *sfnt.Font, src []byte, _ string) {
51+
h.src = src
52+
}

font/subset.go renamed to font/ttf_subset.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,6 @@ var pdfTables = [...]TableTag{
3636
//1886352244, // post
3737
}
3838

39-
// NoSubset can be used as a gdf.FontSubsetFunc when you wish to avoid subsetting a given
40-
// font. Beware: this can negatively impact the output PDF file size.
41-
func NoSubset(_ *sfnt.Font, src []byte, _ map[rune]struct{}) ([]byte, error) { return src, nil }
42-
4339
// TTFSubset is something of a poor man's subsetting function. It works - for TrueType fonts with 'glyf' tables only - by zeroing out
4440
// the outlines of all glyphs not corresponding to or directly referenced by f's glyphs for the runes in cutset,
4541
// truncating f's glyf and loca tables, and then writing only the required tables to the returned byte slice. The final subset font

gdf.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package gdf
22

33
import (
44
"io"
5-
6-
"github.com/cdillond/gdf/font"
75
)
86

97
type PDF struct {
@@ -69,17 +67,17 @@ func includeChildren(pdf *PDF, o obj) error {
6967
for key := range f.charset {
7068
tmp[key] = struct{}{}
7169
}
72-
73-
if f.FontSubsetFunc == nil {
74-
f.FontSubsetFunc = font.TTFSubset
75-
}
76-
b, err := f.FontSubsetFunc(f.SFNT, f.srcb, tmp)
77-
if err != nil {
78-
return err // log.Println(err.Error())
70+
if f.Subsetter == nil {
71+
f.source.buf = f.srcb
7972
} else {
80-
f.source.buf = b
73+
f.Subsetter.Init(f.SFNT, f.srcb, f.srcPath)
74+
b, err := f.Subsetter.Subset(tmp)
75+
if err != nil {
76+
return err
77+
} else {
78+
f.source.buf = b
79+
}
8180
}
82-
8381
}
8482
includeObj(pdf, child)
8583
if err := includeChildren(pdf, child); err != nil {

0 commit comments

Comments
 (0)