├── .gitattributes ├── .github └── workflows │ └── go.yml ├── .gitignore ├── ACKNOWLEDGEMENTS.md ├── LICENSE.md ├── README.md ├── TODO.md ├── byte_reader.go ├── byte_writer.go ├── const.go ├── doc.go ├── encoding.go ├── encoding_test.go ├── export.go ├── font.go ├── font_test.go ├── go.mod ├── go.sum ├── internal └── strutils │ ├── encoding.go │ ├── encoding_test.go │ ├── pdfdocenc.go │ └── pdfdocenc_test.go ├── io_test.go ├── table_cmap.go ├── table_cmap_test.go ├── table_cvt.go ├── table_fpgm.go ├── table_glyf.go ├── table_glyf_test.go ├── table_head.go ├── table_hhea.go ├── table_hmtx.go ├── table_hmtx_test.go ├── table_loca.go ├── table_maxp.go ├── table_maxp_test.go ├── table_name.go ├── table_name_test.go ├── table_offset.go ├── table_offset_test.go ├── table_os2.go ├── table_post.go ├── table_prep.go ├── table_record.go ├── table_record_test.go ├── testdata ├── FreeSans.ttf ├── roboto │ ├── COPYRIGHT.txt │ ├── DESCRIPTION.en_us.html │ ├── LICENSE.txt │ ├── METADATA.pb │ ├── Roboto-Black.ttf │ ├── Roboto-BlackItalic.ttf │ ├── Roboto-Bold.ttf │ ├── Roboto-BoldItalic.ttf │ ├── Roboto-Italic.ttf │ ├── Roboto-Light#1.ttx │ ├── Roboto-Light.ttf │ ├── Roboto-Light.ttx │ ├── Roboto-LightItalic.ttf │ ├── Roboto-Medium.ttf │ ├── Roboto-MediumItalic.ttf │ ├── Roboto-Regular#1.ttx │ ├── Roboto-Regular.ttf │ ├── Roboto-Regular.ttx │ ├── Roboto-Thin.ttf │ └── Roboto-ThinItalic.ttf ├── table_gdef.go ├── table_glyf_test.go ├── utils.go └── wts11.ttf ├── truecli ├── cmd │ ├── info.go │ ├── readwrite.go │ ├── root.go │ ├── subset.go │ ├── subset_gids.go │ ├── subset_runes.go │ ├── subset_simple.go │ └── validate.go ├── go.mod ├── go.sum └── main.go ├── type.go ├── type_test.go ├── validate.go └── validate_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | * -crlf 2 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: Build Go ${{ matrix.go }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | go: [ '1.22','1.21','1.20','1.19','1.18' ] 16 | steps: 17 | - name: Check out code into the Go module directory 18 | uses: actions/checkout@v2 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v1 22 | with: 23 | go-version: ${{ matrix.go }} 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | 29 | - name: Test 30 | run: go test ./... 31 | 32 | - name: Coverage 33 | run: | 34 | go test -race . -coverprofile=coverage.txt -covermode=atomic 35 | 36 | - name: Build 37 | run: cd truecli && go build -v -o truecli 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /ACKNOWLEDGEMENTS.md: -------------------------------------------------------------------------------- 1 | Acknowledgements 2 | ---------------- 3 | 4 | UniType contains code from or depends on the following open source projects: 5 | 6 | * [The standard Go library](https://golang.org/pkg/#stdlib), 7 | 8 | ``` 9 | Copyright (c) 2009 The Go Authors. All rights reserved. 10 | 11 | Redistribution and use in source and binary forms, with or without 12 | modification, are permitted provided that the following conditions are 13 | met: 14 | 15 | * Redistributions of source code must retain the above copyright 16 | notice, this list of conditions and the following disclaimer. 17 | * Redistributions in binary form must reproduce the above 18 | copyright notice, this list of conditions and the following disclaimer 19 | in the documentation and/or other materials provided with the 20 | distribution. 21 | * Neither the name of Google Inc. nor the names of its 22 | contributors may be used to endorse or promote products derived from 23 | this software without specific prior written permission. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 27 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 28 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 29 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 30 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 31 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 32 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 33 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 34 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | ``` 37 | 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## Licensing Information 2 | 3 | The use of this software package and source code is governed by the end-user 4 | license agreement (EULA) available at: https://unidoc.io/eula/ 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UniType - truetype font library for golang. 2 | This library is designed for parsing and editing truetype fonts. 3 | Useful along with UniPDF for subsetting fonts for use in PDF files. 4 | 5 | Contains a CLI for useful operations: 6 | ```bash 7 | $ ./truecli 8 | truecli - TrueCLI 9 | 10 | Usage: 11 | truecli [command] 12 | 13 | Available Commands: 14 | help Help about any command 15 | info Get font file info 16 | readwrite Read and write font file 17 | subset Subset font 18 | validate Validate font file 19 | 20 | Flags: 21 | -h, --help help for truecli 22 | --loglevel string Log level 'debug' and 'trace' give debug information 23 | 24 | Use "truecli [command] --help" for more information about a command. 25 | ``` 26 | 27 | for example: 28 | ```bash 29 | $ $ ./truecli info --trec myfnt.ttf 30 | trec: present with 22 table records 31 | DSIG: 6.78 kB 32 | GSUB: 368 B 33 | LTSH: 28.53 kB 34 | OS/2: 96 B 35 | VDMX: 1.47 kB 36 | cmap: 124.93 kB 37 | cvt: 672 B 38 | fpgm: 2.33 kB 39 | gasp: 16 B 40 | glyf: 13.12 MB 41 | hdmx: 484.97 kB 42 | head: 54 B 43 | hhea: 36 B 44 | hmtx: 114.09 kB 45 | kern: 3.43 kB 46 | loca: 114.09 kB 47 | maxp: 32 B 48 | name: 2.54 kB 49 | post: 283.82 kB 50 | prep: 676 B 51 | vhea: 36 B 52 | vmtx: 114.09 kB 53 | ``` 54 | 55 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## Discussion 4 | 1. Define exactly the purpose of the package. Its harder to make a fully generic package. For example its one thing 5 | to load the outer data structures (tables etc) and another one to fully parse them down to detail. 6 | 7 | - If subsetting is the goal then its enough to know the glyph indices and where the data is kept for each index. 8 | Then if subsetting is simply selecting a subset of glyph indices (and mapping to a new smaller set), then its 9 | enough to know where the data for each glyph is kept and how its references and then simply dropping the data 10 | for certain glyphs is easier than reconstructing the whole thing from memory (where we know what each value means). 11 | 12 | - Potential use cases: 13 | - Subsetting - reducing font size 14 | - Validation - handy for developers and debugging 15 | - Extraction - Identifying glyph index to rune mapping and for use in PDF text extraction 16 | - Rendering - Rendering glyphs to image 17 | 18 | Validation requires loading and analyzing the font tables and both their individual content and how it 19 | fits together. Subsetting requires locating content for input glyph indices and regenerating font with 20 | only data for the desires glyphs (and updated tables) with the output being a new valid font file and 21 | an updated glyph index map mapping new desired glyph indices to their new index. 22 | Extraction requires mapping unicode runes to glyph index (understanding the cmap tables mostly, and potentially 23 | OCR in cases where the cmaps are unreliable). 24 | Rendering requires quickly going from glyph index to rendering data for glyph (and caching for performance), 25 | it does require having a detailed representation of some of the inner maps. 26 | 27 | A performant true type package would have two levels, one quick loading where the data for a table is loaded 28 | and then a detailed. Lazy loading is ideal, i.e. only loading what is needed for a given application. 29 | When regenerating a font, if tables have not been changed it makes sense that their original binary data 30 | can be written out almost instantaneously. No need to parse the details into data models and regenerate, 31 | that only slows down and is not necessary, although it is a great way to test the parsing and regenerating 32 | detail (so its good to have it as an option for development). 33 | 34 | Regeneration: When loading data from tables and storing in a model, the way that the data is stored is not 35 | necessarily the most practical way for using it. For example cmaps have quite many types, some for compatibility 36 | so its not necessarily the best way to output the data. Cmap data is basically map: rune -> glyph index, and 37 | there are a few preferred tables, so it might make sense to use the most common tables when outputing. 38 | For speed, it might make sense to use the same tables as in the original and only take the data needed 39 | for the new glyph set. 40 | 41 | It makes sense to only export functions for the needed use cases. This allows more flexibility in changing 42 | the internals going forward. 43 | 44 | Currently the package is somewhere in between, i.e. it loads a bunch of stuff, probably more than is needed. 45 | 46 | The key should be to get the needed use cases to work, then it can be refactored for better performance 47 | later. 48 | 49 | ## List 50 | 51 | - Lazy loading of tables. See font.go. Only parse tables when they are called. Example: 52 | GetHead() function that returns the head table: parses it if not already loaded, returns cached version if loaded. 53 | 54 | - glyf table: Code for parsing the glyf table contents is commented out. Need to parsing it on demand? 55 | Related to the above point. 56 | Key is we dont want to do this parsing except when using this table. 57 | Also ideally we only want to parse the ones we need to use, not every single one, unless 58 | it can be avoided or using for full validation/checking. 59 | 60 | - glyf table extended parsing needs more test cases too looks like. 61 | 62 | - CLI utility - would be nice to have a cli to play around with for checking and working with fonts 63 | truecli tables fnt.ttf should output: 64 | number of tables: 5 65 | name: offset / size MB 66 | 67 | trucli fnt.ttf should output some summary about the font 68 | like number of glyphs 69 | and some basic infos (version, creator, basic dimensions etc) 70 | 71 | trucli tablename fnt.ttf should output serialized info for that specific table. 72 | 73 | 74 | - pdf side: 75 | 76 | 77 | +simple fonts: 78 | subsetting ttf font that is using only simple fonts... we can probably 79 | generate a font with 255 glyphs that are only those needed, and maybe empty ones in between, 80 | if needed 81 | subset name: EOODIA+Poetica : arbitrary 6 uppercase letters + postscript name 82 | or just take the 0-255 entries... which should be pretty easy, order unchanged. 83 | 84 | +composite fonts: 85 | create a CIDToGIDMap: stream/name 86 | default is identity (1/1), 87 | If the value is a stream, the bytes in the stream 88 | shall contain the mapping from CIDs to glyph indices: the glyph index 89 | for a particular CID value c shall be a 2-byte value stored in bytes 90 | 2 × c and 2 × c + 1, where the first byte shall be the high-order byte. 91 | If the value of CIDToGIDMap is a name, it shall be Identity, 92 | indicating that the mapping between CIDs and glyph indices is the 93 | identity mapping. Default value: Identity. 94 | 95 | The generation of the CIDToGIDMap assumes that the original CID:GID mapping was 1:1 96 | and the deviation is due to changes in the glyph indices, so each entry will 97 | be CID:newGID value 98 | 0:1:2:3:4:5:6... 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /byte_reader.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "bufio" 10 | "encoding/binary" 11 | "io" 12 | 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // byteReader encapsulates io.ReadSeeker with buffering and provides methods to read binary data as 17 | // needed for truetype fonts. The buffered reader is used to enhance the performance when reading 18 | // binary data types one at a time. 19 | type byteReader struct { 20 | rs io.ReadSeeker 21 | reader *bufio.Reader 22 | } 23 | 24 | func newByteReader(rs io.ReadSeeker) *byteReader { 25 | return &byteReader{ 26 | rs: rs, 27 | reader: bufio.NewReader(rs), 28 | } 29 | } 30 | 31 | // Offset returns current offset position of `r`. 32 | func (r byteReader) Offset() int64 { 33 | offset, _ := r.rs.Seek(0, io.SeekCurrent) 34 | offset -= int64(r.reader.Buffered()) 35 | return offset 36 | } 37 | 38 | // SeekTo seeks to offset. 39 | func (r *byteReader) SeekTo(offset int64) error { 40 | _, err := r.rs.Seek(offset, io.SeekStart) 41 | if err != nil { 42 | return err 43 | } 44 | r.reader = bufio.NewReader(r.rs) 45 | return nil 46 | } 47 | 48 | // Skip skips over `n` bytes. 49 | func (r *byteReader) Skip(n int) error { 50 | _, err := r.reader.Discard(n) 51 | return err 52 | } 53 | 54 | // readBytes reads bytes straight from `r`. 55 | func (r *byteReader) readBytes(bp *[]byte, length int) error { 56 | *bp = make([]byte, length) 57 | _, err := io.ReadFull(r.reader, *bp) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // readSlice reads a series of values into `slice` from `r` (big endian). 66 | func (r *byteReader) readSlice(slice interface{}, length int) error { 67 | switch t := slice.(type) { 68 | case *[]uint8: 69 | for i := 0; i < length; i++ { 70 | val, err := r.readUint8() 71 | if err != nil { 72 | return err 73 | } 74 | *t = append(*t, val) 75 | } 76 | case *[]uint16: 77 | for i := 0; i < length; i++ { 78 | val, err := r.readUint16() 79 | if err != nil { 80 | return err 81 | } 82 | *t = append(*t, val) 83 | } 84 | case *[]int16: 85 | for i := 0; i < length; i++ { 86 | val, err := r.readInt16() 87 | if err != nil { 88 | return err 89 | } 90 | *t = append(*t, val) 91 | } 92 | case *[]offset16: 93 | for i := 0; i < length; i++ { 94 | val, err := r.readOffset16() 95 | if err != nil { 96 | return err 97 | } 98 | *t = append(*t, val) 99 | } 100 | case *[]offset32: 101 | for i := 0; i < length; i++ { 102 | val, err := r.readOffset32() 103 | if err != nil { 104 | return err 105 | } 106 | *t = append(*t, val) 107 | } 108 | 109 | default: 110 | logrus.Errorf("Unsupported type: %T (readSlice)", t) 111 | return errTypeCheck 112 | } 113 | return nil 114 | } 115 | 116 | // read reads a series of fields from `r`. 117 | func (r byteReader) read(fields ...interface{}) error { 118 | for _, f := range fields { 119 | switch t := f.(type) { 120 | case **f2dot14: 121 | val, err := r.readF2dot14() 122 | if err != nil { 123 | return err 124 | } 125 | *t = &val 126 | case *f2dot14: 127 | val, err := r.readF2dot14() 128 | if err != nil { 129 | return err 130 | } 131 | *t = val 132 | case *fixed: 133 | val, err := r.readFixed() 134 | if err != nil { 135 | return err 136 | } 137 | *t = val 138 | case *fword: 139 | val, err := r.readFword() 140 | if err != nil { 141 | return err 142 | } 143 | *t = val 144 | case *int8: 145 | val, err := r.readInt8() 146 | if err != nil { 147 | return err 148 | } 149 | *t = val 150 | case *int16: 151 | val, err := r.readInt16() 152 | if err != nil { 153 | return err 154 | } 155 | *t = val 156 | case *int32: 157 | val, err := r.readInt32() 158 | if err != nil { 159 | return err 160 | } 161 | *t = val 162 | case *longdatetime: 163 | val, err := r.readLongdatetime() 164 | if err != nil { 165 | return err 166 | } 167 | *t = val 168 | case *offset16: 169 | val, err := r.readOffset16() 170 | if err != nil { 171 | return err 172 | } 173 | *t = val 174 | case *offset32: 175 | val, err := r.readOffset32() 176 | if err != nil { 177 | return err 178 | } 179 | *t = val 180 | case *ufword: 181 | val, err := r.readUfword() 182 | if err != nil { 183 | return err 184 | } 185 | *t = val 186 | case *uint8: 187 | val, err := r.readUint8() 188 | if err != nil { 189 | return err 190 | } 191 | *t = val 192 | case *uint16: 193 | val, err := r.readUint16() 194 | if err != nil { 195 | return err 196 | } 197 | *t = val 198 | case *tag: 199 | val, err := r.readTag() 200 | if err != nil { 201 | return err 202 | } 203 | *t = val 204 | case *uint32: 205 | val, err := r.readUint32() 206 | if err != nil { 207 | return err 208 | } 209 | *t = val 210 | 211 | default: 212 | logrus.Errorf("Unsupported type: %T (read)", t) 213 | return errTypeCheck 214 | } 215 | } 216 | return nil 217 | } 218 | 219 | func (r byteReader) readF2dot14() (f2dot14, error) { 220 | b := make([]byte, 2) 221 | _, err := io.ReadFull(r.reader, b) 222 | if err != nil { 223 | return 0, err 224 | } 225 | u16 := binary.BigEndian.Uint16(b) 226 | return f2dot14(u16), nil 227 | } 228 | 229 | func (r byteReader) readFixed() (fixed, error) { 230 | var val fixed 231 | err := binary.Read(r.reader, binary.BigEndian, &val) 232 | return val, err 233 | } 234 | 235 | func (r byteReader) readFword() (fword, error) { 236 | var val fword 237 | err := binary.Read(r.reader, binary.BigEndian, &val) 238 | return val, err 239 | } 240 | 241 | func (r byteReader) readUint8() (uint8, error) { 242 | var val uint8 243 | err := binary.Read(r.reader, binary.BigEndian, &val) 244 | return val, err 245 | } 246 | 247 | func (r byteReader) readUint16() (uint16, error) { 248 | var val uint16 249 | err := binary.Read(r.reader, binary.BigEndian, &val) 250 | return val, err 251 | } 252 | 253 | func (r byteReader) readInt8() (int8, error) { 254 | var val int8 255 | err := binary.Read(r.reader, binary.BigEndian, &val) 256 | return val, err 257 | } 258 | 259 | func (r byteReader) readInt16() (int16, error) { 260 | var val int16 261 | err := binary.Read(r.reader, binary.BigEndian, &val) 262 | return val, err 263 | } 264 | 265 | func (r byteReader) readInt32() (int32, error) { 266 | var val int32 267 | err := binary.Read(r.reader, binary.BigEndian, &val) 268 | return val, err 269 | } 270 | 271 | func (r byteReader) readUint32() (uint32, error) { 272 | var val uint32 273 | err := binary.Read(r.reader, binary.BigEndian, &val) 274 | return val, err 275 | } 276 | 277 | func (r byteReader) readTag() (tag, error) { 278 | var val tag 279 | err := binary.Read(r.reader, binary.BigEndian, &val) 280 | return val, err 281 | } 282 | 283 | func (r byteReader) readUfword() (ufword, error) { 284 | var val ufword 285 | err := binary.Read(r.reader, binary.BigEndian, &val) 286 | return val, err 287 | } 288 | 289 | func (r byteReader) readLongdatetime() (longdatetime, error) { 290 | var val longdatetime 291 | err := binary.Read(r.reader, binary.BigEndian, &val) 292 | return val, err 293 | } 294 | 295 | func (r byteReader) readOffset16() (offset16, error) { 296 | var val offset16 297 | err := binary.Read(r.reader, binary.BigEndian, &val) 298 | return val, err 299 | } 300 | 301 | func (r byteReader) readOffset32() (offset32, error) { 302 | var val offset32 303 | err := binary.Read(r.reader, binary.BigEndian, &val) 304 | return val, err 305 | } 306 | -------------------------------------------------------------------------------- /byte_writer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "bytes" 10 | "encoding/binary" 11 | "io" 12 | 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // byteWriter encapsulates io.Writer and provides methods to write binary data as fit for truetype fonts. 17 | // Writes are buffered until flushed. Provides methods to calculate checksum of the current buffer. 18 | type byteWriter struct { 19 | w io.Writer 20 | len int64 21 | flushedLen int64 // total length that has been flushed (written to w). 22 | 23 | buffer bytes.Buffer 24 | } 25 | 26 | func newByteWriter(w io.Writer) *byteWriter { 27 | return &byteWriter{ 28 | w: w, 29 | } 30 | } 31 | 32 | func (w *byteWriter) flush() error { 33 | b := w.buffer.Bytes() 34 | n, err := w.w.Write(b) 35 | if err != nil { 36 | return err 37 | } 38 | w.flushedLen += int64(n) 39 | 40 | w.buffer.Reset() 41 | return nil 42 | } 43 | 44 | // bufferedLen returns the length of the current buffer. 45 | func (w *byteWriter) bufferedLen() int { 46 | return w.buffer.Len() 47 | } 48 | 49 | // checksum returns the checksum of the current buffer. 50 | func (w *byteWriter) checksum() uint32 { 51 | var sum uint32 52 | 53 | data := w.buffer.Bytes() 54 | 55 | if len(data) < 60 { 56 | logrus.Debugf("Data: % X", data) 57 | } 58 | logrus.Debugf("Data length: %d", len(data)) 59 | sum = 0 60 | 61 | for i := 0; i < len(data); i += 4 { 62 | a := i 63 | b := i + 4 64 | if b > len(data) { 65 | b = len(data) 66 | } 67 | 68 | dup := make([]byte, 4) 69 | copy(dup, data[a:b]) 70 | 71 | if b-a < 4 { 72 | for j := 0; j < b-a; j++ { 73 | dup = append(dup, 0) // 74 | } 75 | } 76 | 77 | val := binary.BigEndian.Uint32(dup) 78 | sum += val 79 | } 80 | 81 | return sum 82 | } 83 | 84 | // writeBytes writes the bytes straight to the buffer. 85 | func (w *byteWriter) writeBytes(b []byte) error { 86 | n, err := w.buffer.Write(b) 87 | w.len += int64(n) 88 | return err 89 | } 90 | 91 | func (w *byteWriter) writeSlice(slice interface{}) error { 92 | switch t := slice.(type) { 93 | case *[]uint8: 94 | slice = *t 95 | case *[]uint16: 96 | slice = *t 97 | case *[]offset32: 98 | slice = *t 99 | } 100 | 101 | switch t := slice.(type) { 102 | case []uint8: 103 | for _, val := range t { 104 | err := w.writeUint8(val) 105 | if err != nil { 106 | return err 107 | } 108 | } 109 | case []uint16: 110 | for _, val := range t { 111 | err := w.writeUint16(val) 112 | if err != nil { 113 | return err 114 | } 115 | } 116 | case []int16: 117 | for _, val := range t { 118 | err := w.writeInt16(val) 119 | if err != nil { 120 | return err 121 | } 122 | } 123 | case []offset16: 124 | for _, val := range t { 125 | err := w.writeOffset16(val) 126 | if err != nil { 127 | return err 128 | } 129 | } 130 | case []offset32: 131 | for _, val := range t { 132 | err := w.writeOffset32(val) 133 | if err != nil { 134 | return err 135 | } 136 | } 137 | 138 | default: 139 | logrus.Errorf("Write type check error: %T (slice)", t) 140 | return errTypeCheck 141 | } 142 | return nil 143 | } 144 | 145 | // Write a series of values to `w`. 146 | func (w *byteWriter) write(fields ...interface{}) error { 147 | for _, f := range fields { 148 | switch t := f.(type) { 149 | case fixed: 150 | err := w.writeFixed(t) 151 | if err != nil { 152 | return err 153 | } 154 | case fword: 155 | err := w.writeFword(t) 156 | if err != nil { 157 | return err 158 | } 159 | case longdatetime: 160 | err := w.writeLongdatetime(t) 161 | if err != nil { 162 | return err 163 | } 164 | case offset16: 165 | err := w.writeOffset16(t) 166 | if err != nil { 167 | return err 168 | } 169 | case offset32: 170 | err := w.writeOffset32(t) 171 | if err != nil { 172 | return err 173 | } 174 | case ufword: 175 | err := w.writeUfword(t) 176 | if err != nil { 177 | return err 178 | } 179 | case uint8: 180 | err := w.writeUint8(t) 181 | if err != nil { 182 | return err 183 | } 184 | case uint16: 185 | err := w.writeUint16(t) 186 | if err != nil { 187 | return err 188 | } 189 | case int16: 190 | err := w.writeInt16(t) 191 | if err != nil { 192 | return err 193 | } 194 | case uint32: 195 | err := w.writeUint32(t) 196 | if err != nil { 197 | return err 198 | } 199 | case tag: 200 | err := w.writeTag(t) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | default: 206 | logrus.Errorf("Write type check error: %T", t) 207 | return errTypeCheck 208 | } 209 | } 210 | 211 | return nil 212 | } 213 | 214 | func (w *byteWriter) writeFixed(val fixed) error { 215 | err := binary.Write(&w.buffer, binary.BigEndian, val) 216 | if err != nil { 217 | return err 218 | } 219 | w.len += 4 220 | return nil 221 | } 222 | 223 | func (w *byteWriter) writeFword(val fword) error { 224 | err := binary.Write(&w.buffer, binary.BigEndian, val) 225 | if err != nil { 226 | return err 227 | } 228 | w.len += 2 229 | return nil 230 | } 231 | 232 | func (w *byteWriter) writeLongdatetime(val longdatetime) error { 233 | err := binary.Write(&w.buffer, binary.BigEndian, val) 234 | if err != nil { 235 | return err 236 | } 237 | w.len += 8 238 | return nil 239 | } 240 | 241 | func (w *byteWriter) writeUint8(vals ...uint8) error { 242 | err := binary.Write(&w.buffer, binary.BigEndian, vals) 243 | if err != nil { 244 | return err 245 | } 246 | w.len++ 247 | return nil 248 | } 249 | 250 | func (w *byteWriter) writeUint16(vals ...uint16) error { 251 | err := binary.Write(&w.buffer, binary.BigEndian, vals) 252 | if err != nil { 253 | return err 254 | } 255 | w.len += 2 256 | return nil 257 | } 258 | 259 | func (w *byteWriter) writeUfword(val ufword) error { 260 | err := binary.Write(&w.buffer, binary.BigEndian, val) 261 | if err != nil { 262 | return err 263 | } 264 | w.len += 2 265 | return nil 266 | } 267 | 268 | func (w *byteWriter) writeInt16(vals ...int16) error { 269 | err := binary.Write(&w.buffer, binary.BigEndian, vals) 270 | if err != nil { 271 | return err 272 | } 273 | w.len += 2 274 | return nil 275 | } 276 | 277 | func (w *byteWriter) writeUint32(val uint32) error { 278 | err := binary.Write(&w.buffer, binary.BigEndian, val) 279 | if err != nil { 280 | return err 281 | } 282 | w.len += 4 283 | return nil 284 | } 285 | 286 | func (w *byteWriter) writeTag(val tag) error { 287 | err := binary.Write(&w.buffer, binary.BigEndian, val) 288 | if err != nil { 289 | return err 290 | } 291 | w.len += 4 292 | return nil 293 | } 294 | 295 | func (w *byteWriter) writeOffset16(val offset16) error { 296 | err := binary.Write(&w.buffer, binary.BigEndian, val) 297 | if err != nil { 298 | return err 299 | } 300 | w.len += 2 301 | return nil 302 | } 303 | 304 | func (w *byteWriter) writeOffset32(val offset32) error { 305 | err := binary.Write(&w.buffer, binary.BigEndian, val) 306 | if err != nil { 307 | return err 308 | } 309 | w.len += 4 310 | return nil 311 | } 312 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import "errors" 9 | 10 | var ( 11 | errTypeCheck = errors.New("type check error") 12 | errRangeCheck = errors.New("range check error") 13 | errInvalidContext = errors.New("invalid context") 14 | errRequiredField = errors.New("required field missing") 15 | errNilReceiver = errors.New("receiver pointer not initialized") 16 | ) 17 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | // Package unitype supports loading and writing truetype fonts. Specifically intended for font validation, 7 | // repairing, subsetting for use in PDF. 8 | package unitype 9 | -------------------------------------------------------------------------------- /encoding.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "encoding/binary" 10 | "unicode/utf8" 11 | 12 | "github.com/sirupsen/logrus" 13 | 14 | "golang.org/x/text/encoding" 15 | "golang.org/x/text/encoding/charmap" 16 | "golang.org/x/text/encoding/japanese" 17 | "golang.org/x/text/encoding/simplifiedchinese" 18 | "golang.org/x/text/encoding/traditionalchinese" 19 | "golang.org/x/text/encoding/unicode" 20 | "golang.org/x/text/encoding/unicode/utf32" 21 | ) 22 | 23 | // The 258 standard mac glyph names used in 'post' format 1 and 2. 24 | // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html 25 | var macGlyphNames = []GlyphName{ 26 | ".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl", 27 | "numbersign", "dollar", "percent", "ampersand", "quotesingle", 28 | "parenleft", "parenright", "asterisk", "plus", "comma", "hyphen", 29 | "period", "slash", "zero", "one", "two", "three", "four", "five", 30 | "six", "seven", "eight", "nine", "colon", "semicolon", "less", 31 | "equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", 32 | "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", 33 | "T", "U", "V", "W", "X", "Y", "Z", "bracketleft", "backslash", 34 | "bracketright", "asciicircum", "underscore", "grave", "a", "b", 35 | "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", 36 | "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", 37 | "bar", "braceright", "asciitilde", "Adieresis", "Aring", 38 | "Ccedilla", "Eacute", "Ntilde", "Odieresis", "Udieresis", "aacute", 39 | "agrave", "acircumflex", "adieresis", "atilde", "aring", 40 | "ccedilla", "eacute", "egrave", "ecircumflex", "edieresis", 41 | "iacute", "igrave", "icircumflex", "idieresis", "ntilde", "oacute", 42 | "ograve", "ocircumflex", "odieresis", "otilde", "uacute", "ugrave", 43 | "ucircumflex", "udieresis", "dagger", "degree", "cent", "sterling", 44 | "section", "bullet", "paragraph", "germandbls", "registered", 45 | "copyright", "trademark", "acute", "dieresis", "notequal", "AE", 46 | "Oslash", "infinity", "plusminus", "lessequal", "greaterequal", 47 | "yen", "mu", "partialdiff", "summation", "product", "pi", 48 | "integral", "ordfeminine", "ordmasculine", "Omega", "ae", "oslash", 49 | "questiondown", "exclamdown", "logicalnot", "radical", "florin", 50 | "approxequal", "Delta", "guillemotleft", "guillemotright", 51 | "ellipsis", "nonbreakingspace", "Agrave", "Atilde", "Otilde", "OE", 52 | "oe", "endash", "emdash", "quotedblleft", "quotedblright", 53 | "quoteleft", "quoteright", "divide", "lozenge", "ydieresis", 54 | "Ydieresis", "fraction", "currency", "guilsinglleft", 55 | "guilsinglright", "fi", "fl", "daggerdbl", "periodcentered", 56 | "quotesinglbase", "quotedblbase", "perthousand", "Acircumflex", 57 | "Ecircumflex", "Aacute", "Edieresis", "Egrave", "Iacute", 58 | "Icircumflex", "Idieresis", "Igrave", "Oacute", "Ocircumflex", 59 | "apple", "Ograve", "Uacute", "Ucircumflex", "Ugrave", "dotlessi", 60 | "circumflex", "tilde", "macron", "breve", "dotaccent", "ring", 61 | "cedilla", "hungarumlaut", "ogonek", "caron", "Lslash", "lslash", 62 | "Scaron", "scaron", "Zcaron", "zcaron", "brokenbar", "Eth", "eth", 63 | "Yacute", "yacute", "Thorn", "thorn", "minus", "multiply", 64 | "onesuperior", "twosuperior", "threesuperior", "onehalf", 65 | "onequarter", "threequarters", "franc", "Gbreve", "gbreve", 66 | "Idotaccent", "Scedilla", "scedilla", "Cacute", "cacute", "Ccaron", 67 | "ccaron", "dcroat", 68 | } 69 | 70 | const ( 71 | platformIDUnicode int = 0 72 | platformIDMacintosh = 1 73 | platformIDWindows = 3 74 | ) 75 | 76 | // getCmapEncoding returns the cmapEncoding for the specified `platformID` and platform-specific `encodingID`. 77 | func getCmapEncoding(platformID, encodingID int) cmapEncoding { 78 | switch platformID { 79 | case platformIDUnicode: 80 | return cmapEncodingUCS2 81 | case platformIDMacintosh: 82 | return cmapEncodingMacRoman 83 | case platformIDWindows: 84 | switch encodingID { 85 | case 0: // Symbol 86 | // TODO(gunnsth): Is this correct for symbol? 87 | return cmapEncodingUCS2 88 | case 1: // Unicode BMP-only (UCS-2) 89 | return cmapEncodingUCS2 90 | case 2: // Shift-JIS 91 | return cmapEncodingShiftJIS 92 | case 3: // PRC 93 | return cmapEncodingPRC 94 | case 4: // BigFive 95 | return cmapEncodingBig5 96 | case 5: // Johab. 97 | return cmapEncodingJohab 98 | case 10: // Unicode UCS-4. 99 | return cmapEncodingUCS4 100 | } 101 | } 102 | logrus.Debugf("Unsupported: PlatformID=%d, EncodingID=%d", platformID, encodingID) 103 | 104 | return cmapEncodingUnsupported 105 | } 106 | 107 | type cmapEncoding int 108 | 109 | const ( 110 | cmapEncodingUCS2 cmapEncoding = iota 111 | cmapEncodingUCS4 112 | cmapEncodingMacRoman 113 | cmapEncodingShiftJIS 114 | cmapEncodingPRC 115 | cmapEncodingBig5 116 | cmapEncodingJohab 117 | cmapEncodingUnsupported 118 | ) 119 | 120 | // GetRuneDecoder returns a rune decoder for the given cmapEncoding. 121 | // TODO(gunnsth): Combine this and getCmapEncoding into a single function? 122 | func (e cmapEncoding) GetRuneDecoder() runeDecoder { 123 | var d *encoding.Decoder 124 | var charcodeBytes int 125 | 126 | switch e { 127 | case cmapEncodingUCS2: 128 | // UCS2 is a subset of UTF16. 129 | // Typically is big endian, although it is not specified explicitly in the specifications. 130 | d = unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder() 131 | charcodeBytes = 2 132 | case cmapEncodingUCS4: 133 | // UCS4 is a subset of UTF32. 134 | d = utf32.UTF32(utf32.BigEndian, utf32.IgnoreBOM).NewDecoder() 135 | charcodeBytes = 4 136 | case cmapEncodingMacRoman: 137 | d = charmap.Macintosh.NewDecoder() 138 | charcodeBytes = 1 139 | case cmapEncodingShiftJIS: 140 | d = japanese.ShiftJIS.NewDecoder() 141 | charcodeBytes = 2 142 | case cmapEncodingPRC: 143 | d = simplifiedchinese.GBK.NewDecoder() 144 | charcodeBytes = 2 145 | case cmapEncodingBig5: 146 | d = traditionalchinese.Big5.NewDecoder() 147 | charcodeBytes = 2 148 | } 149 | 150 | if d == nil { 151 | logrus.Debugf("ERROR: Unsupported encoding (%d) - returning charcodes as runes", e) 152 | d = unicode.UTF8.NewDecoder() 153 | charcodeBytes = 1 154 | } 155 | 156 | return runeDecoder{ 157 | Decoder: d, 158 | charcodeBytes: charcodeBytes, 159 | } 160 | } 161 | 162 | // runeDecoder decodes runes from encoded byte data. 163 | type runeDecoder struct { 164 | *encoding.Decoder 165 | charcodeBytes int // number of bytes per charcode in TTF data. 166 | } 167 | 168 | // ToBytes encodes `charcode` into bytes as represented in TTF data. 169 | func (d runeDecoder) ToBytes(charcode uint32) []byte { 170 | b := make([]byte, d.charcodeBytes) 171 | 172 | switch d.charcodeBytes { 173 | case 1: 174 | b[0] = byte(charcode) 175 | case 2: 176 | binary.BigEndian.PutUint16(b, uint16(charcode)) 177 | case 4: 178 | binary.BigEndian.PutUint32(b, charcode) 179 | default: 180 | logrus.Debugf("ERROR: Unsupported number of bytes per charcode: %d", d.charcodeBytes) 181 | return []byte{0} 182 | } 183 | 184 | return b 185 | } 186 | 187 | // DecodeRune decodes character codes in `b` and returns the decode rune. 188 | func (d runeDecoder) DecodeRune(b []byte) rune { 189 | // Get decoded bytes (the decoder decodes to UTF8 byte format). 190 | decoded, err := d.Bytes(b) 191 | if err != nil { 192 | logrus.Debugf("Decoding error: %v", err) 193 | } 194 | 195 | // TODO(gunnsth): Benchmark utf8.DecodeRune vs string(). 196 | r, _ := utf8.DecodeRune(decoded) 197 | return r 198 | } 199 | -------------------------------------------------------------------------------- /encoding_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestMacEncoding(t *testing.T) { 15 | // Spot checks based on: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html 16 | assert.Equal(t, 258, len(macGlyphNames)) 17 | assert.Equal(t, GlyphName(".notdef"), macGlyphNames[0]) 18 | assert.Equal(t, GlyphName("space"), macGlyphNames[3]) 19 | assert.Equal(t, GlyphName("comma"), macGlyphNames[15]) 20 | assert.Equal(t, GlyphName("a"), macGlyphNames[68]) 21 | assert.Equal(t, GlyphName("z"), macGlyphNames[93]) 22 | assert.Equal(t, GlyphName("dcroat"), macGlyphNames[257]) 23 | } 24 | -------------------------------------------------------------------------------- /export.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "bytes" 10 | "errors" 11 | "io" 12 | "math" 13 | "os" 14 | "sort" 15 | 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | // Font wraps font for outside access. 20 | type Font struct { 21 | br *byteReader 22 | *font 23 | } 24 | 25 | // Parse parses the truetype font from `rs` and returns a new Font. 26 | func Parse(rs io.ReadSeeker) (*Font, error) { 27 | r := newByteReader(rs) 28 | 29 | fnt, err := parseFont(r) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return &Font{ 35 | br: r, 36 | font: fnt, 37 | }, nil 38 | } 39 | 40 | // ParseFile parses the truetype font from file given by path. 41 | func ParseFile(filePath string) (*Font, error) { 42 | f, err := os.Open(filePath) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | defer f.Close() 48 | return Parse(f) 49 | } 50 | 51 | // ValidateBytes validates the turetype font represented by the byte stream. 52 | func ValidateBytes(b []byte) error { 53 | r := bytes.NewReader(b) 54 | br := newByteReader(r) 55 | fnt, err := parseFont(br) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | return fnt.validate(br) 61 | } 62 | 63 | // ValidateFile validates the truetype font given by `filePath`. 64 | func ValidateFile(filePath string) error { 65 | f, err := os.Open(filePath) 66 | if err != nil { 67 | return err 68 | } 69 | defer f.Close() 70 | 71 | br := newByteReader(f) 72 | fnt, err := parseFont(br) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | return fnt.validate(br) 78 | } 79 | 80 | // GetCmap returns the specific cmap specified by `platformID` and platform-specific `encodingID`. 81 | // If not available, nil is returned. Used in PDF for decoding. 82 | func (f *Font) GetCmap(platformID, encodingID int) map[rune]GlyphIndex { 83 | if f.cmap == nil { 84 | return nil 85 | } 86 | 87 | for _, subt := range f.cmap.subtables { 88 | if subt.platformID == platformID && subt.encodingID == encodingID { 89 | return subt.cmap 90 | } 91 | } 92 | 93 | return nil 94 | } 95 | 96 | // LookupRunes looks up each rune in `rune` and returns a matching slice of glyph indices. 97 | // When a rune is not found, a GID of 0 is used (notdef). 98 | func (f *Font) LookupRunes(runes []rune) []GlyphIndex { 99 | var maps []map[rune]GlyphIndex 100 | // Search order (3,1), (1,0), (0,3), (3,10). 101 | maps = append(maps, 102 | f.GetCmap(3, 1), 103 | f.GetCmap(1, 0), 104 | f.GetCmap(0, 3), 105 | f.GetCmap(3, 10), 106 | ) 107 | 108 | var indices []GlyphIndex 109 | for _, r := range runes { 110 | index := GlyphIndex(0) 111 | for _, cmap := range maps { 112 | ind, has := cmap[r] 113 | if has { 114 | index = ind 115 | break 116 | } 117 | } 118 | indices = append(indices, index) 119 | } 120 | logrus.Debugf("Runes: %+v %s", runes, string(runes)) 121 | logrus.Debugf("GIDs: %+v", indices) 122 | return indices 123 | } 124 | 125 | // SubsetKeepRunes prunes data for all GIDs except the ones corresponding to `runes`. The GIDs are 126 | // maintained. Typically reduces glyf table size significantly. 127 | func (f *Font) SubsetKeepRunes(runes []rune) (*Font, error) { 128 | indices := f.LookupRunes(runes) 129 | return f.SubsetKeepIndices(indices) 130 | } 131 | 132 | // SubsetKeepIndices prunes data for all GIDs outside of `indices`. The GIDs are maintained. 133 | // This typically works well and is a simple way to prune most of the unnecessary data as the 134 | // glyf table is usually the biggest by far. 135 | func (f *Font) SubsetKeepIndices(indices []GlyphIndex) (*Font, error) { 136 | newfnt := font{} 137 | 138 | // Expand the set of indices if any of the indices are composite 139 | // glyphs depending on other glyphs. 140 | gidIncludedMap := make(map[GlyphIndex]struct{}, len(indices)) 141 | for _, gid := range indices { 142 | gidIncludedMap[gid] = struct{}{} 143 | } 144 | 145 | toscan := make([]GlyphIndex, 0, len(gidIncludedMap)) 146 | for gid := range gidIncludedMap { 147 | toscan = append(toscan, gid) 148 | } 149 | 150 | // Find dependencies of core sets of glyph, and expand until have all relations. 151 | for len(toscan) > 0 { 152 | var newgids []GlyphIndex 153 | for _, gid := range toscan { 154 | components, err := f.glyf.GetComponents(gid) 155 | if err != nil { 156 | logrus.Debugf("Error getting components for %d", gid) 157 | return nil, err 158 | } 159 | for _, gid := range components { 160 | if _, has := gidIncludedMap[gid]; !has { 161 | gidIncludedMap[gid] = struct{}{} 162 | newgids = append(newgids, gid) 163 | } 164 | } 165 | } 166 | toscan = newgids 167 | } 168 | 169 | newfnt.ot = &offsetTable{} 170 | *newfnt.ot = *f.font.ot 171 | 172 | newfnt.trec = &tableRecords{} 173 | *newfnt.trec = *f.font.trec 174 | 175 | if f.font.head != nil { 176 | newfnt.head = &headTable{} 177 | *newfnt.head = *f.font.head 178 | } 179 | 180 | if f.font.maxp != nil { 181 | newfnt.maxp = &maxpTable{} 182 | *newfnt.maxp = *f.font.maxp 183 | } 184 | 185 | if f.font.hhea != nil { 186 | newfnt.hhea = &hheaTable{} 187 | *newfnt.hhea = *f.font.hhea 188 | } 189 | 190 | if f.font.hmtx != nil { 191 | newfnt.hmtx = &hmtxTable{} 192 | *newfnt.hmtx = *f.font.hmtx 193 | newfnt.optimizeHmtx() 194 | } 195 | 196 | if f.font.glyf != nil && f.font.loca != nil { 197 | newfnt.loca = &locaTable{} 198 | newfnt.glyf = &glyfTable{} 199 | *newfnt.glyf = *f.font.glyf 200 | 201 | // Empty glyf contents for non-included glyphs. 202 | for i := range newfnt.glyf.descs { 203 | if _, has := gidIncludedMap[GlyphIndex(i)]; has { 204 | continue 205 | } 206 | 207 | newfnt.glyf.descs[i].raw = nil 208 | } 209 | 210 | // Update loca offsets. 211 | isShort := f.font.head.indexToLocFormat == 0 212 | if isShort { 213 | newfnt.loca.offsetsShort = make([]offset16, len(newfnt.glyf.descs)+1) 214 | newfnt.loca.offsetsShort[0] = f.font.loca.offsetsShort[0] 215 | } else { 216 | newfnt.loca.offsetsLong = make([]offset32, len(newfnt.glyf.descs)+1) 217 | newfnt.loca.offsetsLong[0] = f.font.loca.offsetsLong[0] 218 | } 219 | for i, desc := range newfnt.glyf.descs { 220 | if isShort { 221 | newfnt.loca.offsetsShort[i+1] = newfnt.loca.offsetsShort[i] + offset16(len(desc.raw))/2 222 | } else { 223 | newfnt.loca.offsetsLong[i+1] = newfnt.loca.offsetsLong[i] + offset32(len(desc.raw)) 224 | } 225 | } 226 | } 227 | 228 | if f.font.prep != nil { 229 | newfnt.prep = &prepTable{} 230 | *newfnt.prep = *f.font.prep 231 | } 232 | 233 | if f.font.cvt != nil { 234 | newfnt.cvt = &cvtTable{} 235 | *newfnt.cvt = *f.font.cvt 236 | } 237 | 238 | if f.font.fpgm != nil { 239 | newfnt.fpgm = &fpgmTable{} 240 | *newfnt.fpgm = *f.font.fpgm 241 | } 242 | 243 | if f.font.name != nil { 244 | newfnt.name = &nameTable{} 245 | *newfnt.name = *f.font.name 246 | } 247 | 248 | if f.font.os2 != nil { 249 | newfnt.os2 = &os2Table{} 250 | *newfnt.os2 = *f.font.os2 251 | } 252 | 253 | if f.font.post != nil { 254 | newfnt.post = &postTable{} 255 | *newfnt.post = *f.font.post 256 | } 257 | 258 | if f.font.cmap != nil { 259 | newfnt.cmap = &cmapTable{} 260 | *newfnt.cmap = *f.font.cmap 261 | } 262 | 263 | subfnt := &Font{ 264 | br: nil, 265 | font: &newfnt, 266 | } 267 | 268 | // Trim down to the first fonts. 269 | var maxgid GlyphIndex 270 | for gid := range gidIncludedMap { 271 | if gid > maxgid { 272 | maxgid = gid 273 | } 274 | } 275 | // Trim font down to only maximum needed glyphs without changing order. 276 | maxNeededNum := int(maxgid) + 1 277 | return subfnt.SubsetFirst(maxNeededNum) 278 | } 279 | 280 | // SubsetFirst creates a subset of `f` limited to only the first `numGlyphs` glyphs. 281 | // Prunes out the glyphs from the previous font beyond that number. 282 | // NOTE: If any of the first numGlyphs depend on later glyphs, it can lead to incorrect rendering. 283 | func (f *Font) SubsetFirst(numGlyphs int) (*Font, error) { 284 | if int(f.maxp.numGlyphs) <= numGlyphs { 285 | logrus.Debugf("Attempting to subset font with same number of glyphs - Ignoring, returning same back") 286 | return f, nil 287 | } 288 | newfnt := font{} 289 | 290 | newfnt.ot = &offsetTable{} 291 | *newfnt.ot = *f.font.ot 292 | 293 | newfnt.trec = &tableRecords{} 294 | *newfnt.trec = *f.font.trec 295 | 296 | if f.font.head != nil { 297 | newfnt.head = &headTable{} 298 | *newfnt.head = *f.font.head 299 | } 300 | 301 | if f.font.maxp != nil { 302 | newfnt.maxp = &maxpTable{} 303 | *newfnt.maxp = *f.font.maxp 304 | newfnt.maxp.numGlyphs = uint16(numGlyphs) 305 | } 306 | if f.font.hhea != nil { 307 | newfnt.hhea = &hheaTable{} 308 | *newfnt.hhea = *f.font.hhea 309 | 310 | if newfnt.hhea.numberOfHMetrics > uint16(numGlyphs) { 311 | newfnt.hhea.numberOfHMetrics = uint16(numGlyphs) 312 | } 313 | } 314 | 315 | if f.font.hmtx != nil { 316 | newfnt.hmtx = &hmtxTable{} 317 | *newfnt.hmtx = *f.font.hmtx 318 | 319 | if len(newfnt.hmtx.hMetrics) > numGlyphs { 320 | newfnt.hmtx.hMetrics = newfnt.hmtx.hMetrics[0:numGlyphs] 321 | newfnt.hmtx.leftSideBearings = nil 322 | } else { 323 | numKeep := numGlyphs - len(newfnt.hmtx.hMetrics) 324 | if numKeep > len(newfnt.hmtx.leftSideBearings) { 325 | numKeep = len(newfnt.hmtx.leftSideBearings) 326 | } 327 | newfnt.hmtx.leftSideBearings = newfnt.hmtx.leftSideBearings[0:numKeep] 328 | } 329 | newfnt.optimizeHmtx() 330 | } 331 | 332 | if f.font.glyf != nil && f.font.loca != nil { 333 | newfnt.loca = &locaTable{} 334 | newfnt.glyf = &glyfTable{ 335 | descs: f.font.glyf.descs[0:numGlyphs], 336 | } 337 | // Update loca offsets. 338 | isShort := f.font.head.indexToLocFormat == 0 339 | if isShort { 340 | newfnt.loca.offsetsShort = make([]offset16, numGlyphs+1) 341 | newfnt.loca.offsetsShort[0] = f.font.loca.offsetsShort[0] 342 | } else { 343 | newfnt.loca.offsetsLong = make([]offset32, numGlyphs+1) 344 | newfnt.loca.offsetsLong[0] = f.font.loca.offsetsLong[0] 345 | } 346 | for i, desc := range newfnt.glyf.descs { 347 | if isShort { 348 | newfnt.loca.offsetsShort[i+1] = newfnt.loca.offsetsShort[i] + offset16(len(desc.raw))/2 349 | } else { 350 | newfnt.loca.offsetsLong[i+1] = newfnt.loca.offsetsLong[i] + offset32(len(desc.raw)) 351 | } 352 | } 353 | } 354 | 355 | if f.font.prep != nil { 356 | newfnt.prep = &prepTable{} 357 | *newfnt.prep = *f.font.prep 358 | } 359 | 360 | if f.font.cvt != nil { 361 | newfnt.cvt = &cvtTable{} 362 | *newfnt.cvt = *f.font.cvt 363 | } 364 | 365 | if f.font.fpgm != nil { 366 | newfnt.fpgm = &fpgmTable{} 367 | *newfnt.fpgm = *f.font.fpgm 368 | } 369 | 370 | if f.font.name != nil { 371 | newfnt.name = &nameTable{} 372 | *newfnt.name = *f.font.name 373 | } 374 | 375 | if f.font.os2 != nil { 376 | newfnt.os2 = &os2Table{} 377 | *newfnt.os2 = *f.font.os2 378 | } 379 | 380 | if f.font.post != nil { 381 | newfnt.post = &postTable{} 382 | *newfnt.post = *f.font.post 383 | 384 | if newfnt.post.numGlyphs > 0 { 385 | newfnt.post.numGlyphs = uint16(numGlyphs) 386 | } 387 | if len(newfnt.post.glyphNameIndex) > numGlyphs { 388 | newfnt.post.glyphNameIndex = newfnt.post.glyphNameIndex[0:numGlyphs] 389 | } 390 | if len(newfnt.post.offsets) > numGlyphs { 391 | newfnt.post.offsets = newfnt.post.offsets[0:numGlyphs] 392 | } 393 | if len(newfnt.post.glyphNames) > numGlyphs { 394 | newfnt.post.glyphNames = newfnt.post.glyphNames[0:numGlyphs] 395 | } 396 | } 397 | 398 | if f.font.cmap != nil { 399 | newfnt.cmap = &cmapTable{ 400 | version: f.cmap.version, 401 | subtables: map[string]*cmapSubtable{}, 402 | } 403 | 404 | for _, name := range f.cmap.subtableKeys { 405 | subt := f.cmap.subtables[name] 406 | switch t := subt.ctx.(type) { 407 | case cmapSubtableFormat0: 408 | for i := range t.glyphIDArray { 409 | if i > numGlyphs { 410 | t.glyphIDArray[i] = 0 411 | } 412 | } 413 | case cmapSubtableFormat4: 414 | newt := cmapSubtableFormat4{} 415 | // Generates a new table: going from glyph index 0 up to numGlyphs. 416 | // Makes continous entries with deltas. 417 | // Does not use glyphIDData, but only the deltas. Can lead to many segments, but should not 418 | // be too bad (especially since subsetting). 419 | charcodes := make([]CharCode, 0, len(subt.charcodeToGID)) 420 | for cc, gid := range subt.charcodeToGID { 421 | if int(gid) >= numGlyphs { 422 | continue 423 | } 424 | charcodes = append(charcodes, cc) 425 | } 426 | sort.Slice(charcodes, func(i, j int) bool { 427 | return charcodes[i] < charcodes[j] 428 | }) 429 | 430 | segments := 0 431 | i := 0 432 | for i < len(charcodes) { 433 | j := i + 1 434 | for ; j < len(charcodes); j++ { 435 | if int(charcodes[j]-charcodes[i]) != j-i || 436 | int(subt.charcodeToGID[charcodes[j]]-subt.charcodeToGID[charcodes[i]]) != j-i { 437 | break 438 | } 439 | } 440 | // from i:j-1 maps to subt.charcodes[i]:subt.charcodes[i]+j-i-1 441 | startCode := uint16(charcodes[i]) 442 | endCode := uint16(charcodes[i]) + uint16(j-i-1) 443 | idDelta := uint16(subt.charcodeToGID[charcodes[i]]) - uint16(charcodes[i]) 444 | 445 | newt.startCode = append(newt.startCode, startCode) 446 | newt.endCode = append(newt.endCode, endCode) 447 | newt.idDelta = append(newt.idDelta, idDelta) 448 | newt.idRangeOffset = append(newt.idRangeOffset, 0) 449 | segments++ 450 | i = j 451 | } 452 | 453 | if segments > 0 && newt.endCode[segments-1] < 65535 { 454 | newt.endCode = append(newt.endCode, 65535) 455 | newt.startCode = append(newt.startCode, 65535) 456 | newt.idDelta = append(newt.idDelta, 1) 457 | newt.idRangeOffset = append(newt.idRangeOffset, 0) 458 | segments++ 459 | } 460 | 461 | newt.length = uint16(2*8 + 2*4*segments) 462 | newt.language = t.language 463 | newt.segCountX2 = uint16(segments * 2) 464 | newt.searchRange = 2 * uint16(math.Pow(2, math.Floor(math.Log2(float64(segments))))) 465 | newt.entrySelector = uint16(math.Log2(float64(newt.searchRange) / 2.0)) 466 | newt.rangeShift = uint16(segments*2) - newt.searchRange 467 | subt.ctx = newt 468 | case cmapSubtableFormat6: 469 | for i := range t.glyphIDArray { 470 | if int(t.glyphIDArray[i]) >= numGlyphs { 471 | t.glyphIDArray[i] = 0 472 | } 473 | } 474 | case cmapSubtableFormat12: 475 | newt := cmapSubtableFormat12{} 476 | groups := 0 477 | 478 | charcodes := make([]CharCode, 0, len(subt.charcodeToGID)) 479 | for cc, gid := range subt.charcodeToGID { 480 | if int(gid) >= numGlyphs { 481 | continue 482 | } 483 | charcodes = append(charcodes, cc) 484 | } 485 | sort.Slice(charcodes, func(i, j int) bool { 486 | return charcodes[i] < charcodes[j] 487 | }) 488 | 489 | i := 0 490 | for i < len(charcodes) { 491 | j := i + 1 492 | for ; j < len(charcodes); j++ { 493 | if int(charcodes[j]-charcodes[i]) != j-i || 494 | int(subt.charcodeToGID[charcodes[j]]-subt.charcodeToGID[charcodes[i]]) != j-i { 495 | break 496 | } 497 | } 498 | // from i:j-1 maps to subt.charcodes[i]:subt.charcodes[i]+j-i-1 499 | startCharCode := uint32(charcodes[i]) 500 | endCharCode := uint32(charcodes[i]) + uint32(j-i-1) 501 | startGlyphID := uint32(subt.charcodeToGID[charcodes[i]]) 502 | 503 | group := sequentialMapGroup{ 504 | startCharCode: startCharCode, 505 | endCharCode: endCharCode, 506 | startGlyphID: startGlyphID, 507 | } 508 | newt.groups = append(newt.groups, group) 509 | groups++ 510 | i = j 511 | } 512 | 513 | newt.length = uint32(2*2 + 3*4 + groups*3*4) 514 | newt.language = t.language 515 | newt.numGroups = uint32(groups) 516 | subt.ctx = newt 517 | } 518 | 519 | newfnt.cmap.subtableKeys = append(newfnt.cmap.subtableKeys, name) 520 | newfnt.cmap.subtables[name] = subt 521 | } 522 | newfnt.cmap.numTables = uint16(len(newfnt.cmap.subtables)) 523 | } 524 | 525 | subfnt := &Font{ 526 | br: nil, 527 | font: &newfnt, 528 | } 529 | return subfnt, nil 530 | } 531 | 532 | // Subset creates a subset of `f` including only glyph indices specified by `indices`. 533 | // Returns the new subsetted font, a map of old to new GlyphIndex to GlyphIndex as the removal 534 | // of glyphs requires reordering. 535 | func (f *Font) Subset(indices []GlyphIndex) (newf *Font, oldnew map[GlyphIndex]GlyphIndex, err error) { 536 | // TODO: 537 | // 1. Make the new cmap for `runes` if `cmap` is nil, using the cmap table and make a []GlyphIndex 538 | // with the glyph indices to keep (index prior to subsetting). 539 | // 2. Go through each table and leave only data for the glyph indices to be kept. 540 | return nil, nil, errors.New("not implemented yet") 541 | } 542 | 543 | // PruneTables prunes font tables `tables` by name from font. 544 | // Currently supports: "cmap", "post", "name". 545 | func (f *Font) PruneTables(tables ...string) error { 546 | for _, table := range tables { 547 | switch table { 548 | case "cmap": 549 | f.cmap = nil 550 | case "post": 551 | f.post = nil 552 | case "name": 553 | f.name = nil 554 | } 555 | } 556 | return nil 557 | } 558 | 559 | // Optimize does some optimization such as reducing hmtx table. 560 | func (f *Font) Optimize() error { 561 | f.optimizeHmtx() 562 | return nil 563 | } 564 | 565 | // Write writes the font to `w`. 566 | func (f *Font) Write(w io.Writer) error { 567 | bw := newByteWriter(w) 568 | err := f.font.write(bw) 569 | if err != nil { 570 | return err 571 | } 572 | return bw.flush() 573 | } 574 | 575 | // WriteFile writes the font to `outPath`. 576 | func (f *Font) WriteFile(outPath string) error { 577 | of, err := os.Create(outPath) 578 | if err != nil { 579 | return err 580 | } 581 | defer of.Close() 582 | 583 | return f.Write(of) 584 | } 585 | -------------------------------------------------------------------------------- /font.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "bytes" 10 | "encoding/binary" 11 | "fmt" 12 | "io" 13 | 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // Export what UniPDF needs. 18 | // font flags: 19 | // IsFixedPitch, Serif, etc (Table 123 PDF32000_2008 - font flags) 20 | // FixedPitch() bool 21 | // Serif() bool 22 | // Symbolic() bool 23 | // Script() bool 24 | // Nonsymbolic() bool 25 | // Italic() bool 26 | // AllCap() bool 27 | // SmallCap() bool 28 | // ForceBold() bool 29 | // Need to be able to derive the font flags from the font to build a font descriptor 30 | // 31 | // Required table according to PDF32000_2008 (9.9 Embedded font programs - p. 299): 32 | // “head”, “hhea”, “loca”, “maxp”, “cvt”, “prep”, “glyf”, “hmtx”, and “fpgm”. If used with a simple 33 | // font dictionary, the font program shall additionally contain a cmap table defining one or more 34 | // encodings, as discussed in 9.6.6.4, "Encodings for TrueType Fonts". If used with a CIDFont 35 | // dictionary, the cmap table is not needed and shall not be present, since the mapping from 36 | // character codes to glyph descriptions is provided separately. 37 | // 38 | 39 | // font is a data model for truetype fonts with basic access methods. 40 | type font struct { 41 | strict bool 42 | incompatibilities []string 43 | 44 | ot *offsetTable 45 | trec *tableRecords // table records (references other tables). 46 | head *headTable 47 | hhea *hheaTable 48 | loca *locaTable 49 | maxp *maxpTable 50 | cvt *cvtTable 51 | fpgm *fpgmTable 52 | prep *prepTable 53 | glyf *glyfTable 54 | hmtx *hmtxTable 55 | name *nameTable 56 | os2 *os2Table 57 | post *postTable 58 | cmap *cmapTable 59 | } 60 | 61 | // Returns an error in strict mode, otherwise adds the incompatibility to a list of noted incompatibilities. 62 | func (f *font) recordIncompatibilityf(fmtstr string, a ...interface{}) error { 63 | str := fmt.Sprintf(fmtstr, a...) 64 | if f.strict { 65 | return fmt.Errorf("incompatibility: %s", str) 66 | } 67 | f.incompatibilities = append(f.incompatibilities, str) 68 | return nil 69 | } 70 | 71 | func (f font) numTables() int { 72 | return int(f.ot.numTables) 73 | } 74 | 75 | func parseFont(r *byteReader) (*font, error) { 76 | f := &font{} 77 | 78 | var err error 79 | 80 | // Load table offsets and records. 81 | f.ot, err = f.parseOffsetTable(r) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | f.trec, err = f.parseTableRecords(r) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | f.head, err = f.parseHead(r) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | f.maxp, err = f.parseMaxp(r) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | f.hhea, err = f.parseHhea(r) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | f.hmtx, err = f.parseHmtx(r) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | f.loca, err = f.parseLoca(r) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | f.glyf, err = f.parseGlyf(r) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | f.prep, err = f.parsePrep(r) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | f.name, err = f.parseNameTable(r) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | f.os2, err = f.parseOS2Table(r) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | f.post, err = f.parsePost(r) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | f.cmap, err = f.parseCmap(r) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | f.cvt, err = f.parseCvt(r) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | f.fpgm, err = f.parseFpgm(r) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | return f, nil 157 | } 158 | 159 | // numTablesToWrite returns the number of tables in `f`. 160 | // Calculates based on the number of tables will be written out. 161 | // NOTE that not all tables that are loaded are written out. 162 | func (f *font) numTablesToWrite() int { 163 | var num int 164 | 165 | if f.head != nil { 166 | num++ 167 | } 168 | if f.maxp != nil { 169 | num++ 170 | } 171 | if f.hhea != nil { 172 | num++ 173 | } 174 | if f.hmtx != nil { 175 | num++ 176 | } 177 | if f.loca != nil { 178 | num++ 179 | } 180 | if f.glyf != nil { 181 | num++ 182 | } 183 | if f.cvt != nil { 184 | num++ 185 | } 186 | if f.fpgm != nil { 187 | num++ 188 | } 189 | if f.prep != nil { 190 | num++ 191 | } 192 | if f.name != nil { 193 | num++ 194 | } 195 | if f.os2 != nil { 196 | num++ 197 | } 198 | if f.post != nil { 199 | num++ 200 | } 201 | if f.cmap != nil { 202 | num++ 203 | } 204 | return num 205 | } 206 | 207 | func (f *font) write(w *byteWriter) error { 208 | logrus.Debug("Writing font") 209 | numTables := f.numTablesToWrite() 210 | otTable := &offsetTable{ 211 | sfntVersion: f.ot.sfntVersion, 212 | numTables: uint16(numTables), 213 | searchRange: f.ot.searchRange, 214 | entrySelector: f.ot.entrySelector, 215 | rangeShift: f.ot.rangeShift, 216 | } 217 | trec := &tableRecords{} 218 | 219 | f.ot.numTables = uint16(numTables) 220 | 221 | // Starting offset after offset table and table records. 222 | startOffset := int64(12 + numTables*16) 223 | 224 | logrus.Tracef("==== write\nnumTables: %d\nstartOffset: %d", numTables, startOffset) 225 | logrus.Trace("Write 2") 226 | // Writing is two phases and is done in a few steps: 227 | // 1. Write the content tables: head, hhea, etc in the expected order and keep track of the length, checksum for each. 228 | // 2. Generate the table records based on the information. 229 | // 3. Write out in final order: offset table, table records, head, ... 230 | // 4. Set checkAdjustment of head table based on checksumof entire file 231 | // 5. Write the final output 232 | 233 | // Write to buffer to get offsets. 234 | var buf bytes.Buffer 235 | var headChecksum uint32 236 | { 237 | bufw := newByteWriter(&buf) 238 | 239 | // head. 240 | f.head.checksumAdjustment = 0 241 | offset := startOffset 242 | err := f.writeHead(bufw) 243 | if err != nil { 244 | return err 245 | } 246 | headChecksum = bufw.checksum() 247 | trec.Set("head", offset, bufw.bufferedLen(), headChecksum) 248 | err = bufw.flush() 249 | if err != nil { 250 | return err 251 | } 252 | 253 | // maxp. 254 | offset = startOffset + bufw.flushedLen 255 | err = f.writeMaxp(bufw) 256 | if err != nil { 257 | return err 258 | } 259 | trec.Set("maxp", offset, bufw.bufferedLen(), bufw.checksum()) 260 | err = bufw.flush() 261 | if err != nil { 262 | return err 263 | } 264 | 265 | // hhea. 266 | if f.hhea != nil { 267 | offset = startOffset + bufw.flushedLen 268 | err = f.writeHhea(bufw) 269 | if err != nil { 270 | return err 271 | } 272 | trec.Set("hhea", offset, bufw.bufferedLen(), bufw.checksum()) 273 | err = bufw.flush() 274 | if err != nil { 275 | return err 276 | } 277 | } 278 | 279 | // hmtx. 280 | if f.hmtx != nil { 281 | offset = startOffset + bufw.flushedLen 282 | err = f.writeHmtx(bufw) 283 | if err != nil { 284 | return err 285 | } 286 | trec.Set("hmtx", offset, bufw.bufferedLen(), bufw.checksum()) 287 | err = bufw.flush() 288 | if err != nil { 289 | return err 290 | } 291 | } 292 | 293 | // loca. 294 | if f.loca != nil { 295 | offset = startOffset + bufw.flushedLen 296 | err = f.writeLoca(bufw) 297 | if err != nil { 298 | return err 299 | } 300 | trec.Set("loca", offset, bufw.bufferedLen(), bufw.checksum()) 301 | err = bufw.flush() 302 | if err != nil { 303 | return err 304 | } 305 | } 306 | 307 | // glyf. 308 | if f.glyf != nil { 309 | offset = startOffset + bufw.flushedLen 310 | err = f.writeGlyf(bufw) 311 | if err != nil { 312 | return err 313 | } 314 | trec.Set("glyf", offset, bufw.bufferedLen(), bufw.checksum()) 315 | err = bufw.flush() 316 | if err != nil { 317 | return err 318 | } 319 | } 320 | 321 | // prep. 322 | if f.prep != nil { 323 | offset = startOffset + bufw.flushedLen 324 | err = f.writePrep(bufw) 325 | if err != nil { 326 | return err 327 | } 328 | trec.Set("prep", offset, bufw.bufferedLen(), bufw.checksum()) 329 | err = bufw.flush() 330 | if err != nil { 331 | return err 332 | } 333 | } 334 | 335 | // cvt. 336 | if f.cvt != nil { 337 | offset = startOffset + bufw.flushedLen 338 | err = f.writeCvt(bufw) 339 | if err != nil { 340 | return err 341 | } 342 | trec.Set("cvt", offset, bufw.bufferedLen(), bufw.checksum()) 343 | err = bufw.flush() 344 | if err != nil { 345 | return err 346 | } 347 | } 348 | 349 | // fpgm. 350 | if f.fpgm != nil { 351 | offset = startOffset + bufw.flushedLen 352 | err = f.writeFpgm(bufw) 353 | if err != nil { 354 | return err 355 | } 356 | trec.Set("fpgm", offset, bufw.bufferedLen(), bufw.checksum()) 357 | err = bufw.flush() 358 | if err != nil { 359 | return err 360 | } 361 | } 362 | 363 | // name. 364 | if f.name != nil { 365 | offset = startOffset + bufw.flushedLen 366 | err = f.writeNameTable(bufw) 367 | if err != nil { 368 | return err 369 | } 370 | trec.Set("name", offset, bufw.bufferedLen(), bufw.checksum()) 371 | err = bufw.flush() 372 | if err != nil { 373 | return err 374 | } 375 | } 376 | 377 | // os2. 378 | if f.os2 != nil { 379 | offset = startOffset + bufw.flushedLen 380 | err = f.writeOS2(bufw) 381 | if err != nil { 382 | return err 383 | } 384 | trec.Set("OS/2", offset, bufw.bufferedLen(), bufw.checksum()) 385 | err = bufw.flush() 386 | if err != nil { 387 | return err 388 | } 389 | } 390 | 391 | // post 392 | if f.post != nil { 393 | offset = startOffset + bufw.flushedLen 394 | err = f.writePost(bufw) 395 | if err != nil { 396 | return err 397 | } 398 | trec.Set("post", offset, bufw.bufferedLen(), bufw.checksum()) 399 | err = bufw.flush() 400 | if err != nil { 401 | return err 402 | } 403 | } 404 | 405 | // cmap 406 | if f.cmap != nil { 407 | offset = startOffset + bufw.flushedLen 408 | err = f.writeCmap(bufw) 409 | if err != nil { 410 | return err 411 | } 412 | trec.Set("cmap", offset, bufw.bufferedLen(), bufw.checksum()) 413 | err = bufw.flush() 414 | if err != nil { 415 | return err 416 | } 417 | } 418 | } 419 | logrus.Trace("Write 3") 420 | 421 | // Write the offset and table records to another mock buffer. 422 | var bufh bytes.Buffer 423 | { 424 | bufw := newByteWriter(&bufh) 425 | // Create a mock font for writing without modifying the original entries of `f`. 426 | mockf := &font{ 427 | ot: otTable, 428 | trec: trec, 429 | } 430 | 431 | err := mockf.writeOffsetTable(bufw) 432 | if err != nil { 433 | return err 434 | } 435 | 436 | err = mockf.writeTableRecords(bufw) 437 | if err != nil { 438 | return err 439 | } 440 | err = bufw.flush() 441 | if err != nil { 442 | return err 443 | } 444 | } 445 | 446 | // Write everything to bufh. 447 | _, err := buf.WriteTo(&bufh) 448 | if err != nil { 449 | return err 450 | } 451 | 452 | // Calculate total checksum for the entire font. 453 | checksummer := byteWriter{ 454 | buffer: bufh, 455 | } 456 | fontChecksum := checksummer.checksum() 457 | checksumAdjustment := 0xB1B0AFBA - fontChecksum 458 | 459 | // Set the checksumAdjustment of the head table. 460 | data := bufh.Bytes() 461 | hoff := startOffset 462 | binary.BigEndian.PutUint32(data[hoff+8:hoff+12], checksumAdjustment) 463 | 464 | buffer := bytes.NewBuffer(data) 465 | _, err = io.Copy(&w.buffer, buffer) 466 | return err 467 | } 468 | 469 | // TableInfo provides readable information regarding a table. 470 | func (f *font) TableInfo(table string) string { 471 | var b bytes.Buffer 472 | 473 | switch table { 474 | case "trec": 475 | if f.trec == nil { 476 | b.WriteString(fmt.Sprintf("trec: missing\n")) 477 | break 478 | } 479 | b.WriteString(fmt.Sprintf("trec: present with %d table records\n", len(f.trec.list))) 480 | for _, tr := range f.trec.list { 481 | if tr.length > 1024*1024 { 482 | b.WriteString(fmt.Sprintf("%s: %.2f MB\n", tr.tableTag.String(), float64(tr.length)/1024/1024)) 483 | } else if tr.length > 1024 { 484 | b.WriteString(fmt.Sprintf("%s: %.2f kB\n", tr.tableTag.String(), float64(tr.length)/1024)) 485 | } else { 486 | b.WriteString(fmt.Sprintf("%s: %d B\n", tr.tableTag.String(), tr.length)) 487 | } 488 | } 489 | b.WriteString("--\n") 490 | case "head": 491 | if f.head == nil { 492 | b.WriteString("head: missing\n") 493 | break 494 | } 495 | b.WriteString(fmt.Sprintf("head table: %#v\n", f.head)) 496 | case "os2": 497 | if f.os2 == nil { 498 | b.WriteString("os2: missing\n") 499 | break 500 | } 501 | b.WriteString(fmt.Sprintf("os/2 table: %#v\n", f.os2)) 502 | case "hhea": 503 | if f.hhea == nil { 504 | b.WriteString("hhea: missing\n") 505 | break 506 | } 507 | b.WriteString(fmt.Sprintf("hhea table: numHMetrics: %d\n", f.hhea.numberOfHMetrics)) 508 | case "hmtx": 509 | if f.hmtx == nil { 510 | b.WriteString("hmtx: missing\n") 511 | break 512 | } 513 | b.WriteString(fmt.Sprintf("hmtx: hmetrics: %d, leftSideBearings: %d\n", 514 | len(f.hmtx.hMetrics), len(f.hmtx.leftSideBearings))) 515 | case "cmap": 516 | if f.cmap == nil { 517 | b.WriteString("cmap: missing\n") 518 | break 519 | } 520 | b.WriteString(fmt.Sprintf("cmap version: %d\n", 521 | f.cmap.version)) 522 | b.WriteString(fmt.Sprintf("cmap: encoding records: %d subtables: %d\n", 523 | len(f.cmap.encodingRecords), len(f.cmap.subtables))) 524 | b.WriteString(fmt.Sprintf("cmap: subtables: %+v\n", f.cmap.subtableKeys)) 525 | for _, k := range f.cmap.subtableKeys { 526 | subt := f.cmap.subtables[k] 527 | b.WriteString(fmt.Sprintf("cmap subtable: %s: runes: %d\n", k, len(subt.runes))) 528 | for i := range subt.charcodes { 529 | b.WriteString(fmt.Sprintf("\t%d - Charcode %d (0x%X) - rune % X\n", i, subt.charcodes[i], subt.charcodes[i], subt.runes[i])) 530 | } 531 | } 532 | case "loca": 533 | if f.loca == nil { 534 | b.WriteString("loca: missing\n") 535 | break 536 | } 537 | b.WriteString(fmt.Sprintf("Loca table\n")) 538 | b.WriteString(fmt.Sprintf("- Short offsets: %d\n", len(f.loca.offsetsShort))) 539 | b.WriteString(fmt.Sprintf("- Long offsets: %d\n", len(f.loca.offsetsLong))) 540 | case "name": 541 | if f.name == nil { 542 | b.WriteString("name: missing\n") 543 | break 544 | } 545 | b.WriteString(fmt.Sprintf("name table\n")) 546 | b.WriteString(fmt.Sprintf("%#v\n", f.name)) 547 | case "glyf": 548 | if f.glyf == nil { 549 | b.WriteString("glyf: missing\n") 550 | break 551 | } 552 | rawTotal := 0.0 553 | for _, desc := range f.glyf.descs { 554 | rawTotal += float64(len(desc.raw)) 555 | } 556 | b.WriteString(fmt.Sprintf("glyf table present: %d descriptions (%.2f kB)\n", len(f.glyf.descs), rawTotal/1024)) 557 | case "post": 558 | if f.post == nil { 559 | b.WriteString("post: missing\n") 560 | break 561 | } 562 | b.WriteString(fmt.Sprintf("post table present: %d numGlyphs\n", f.post.numGlyphs)) 563 | b.WriteString(fmt.Sprintf("- post glyphNameIndex: %d\n", len(f.post.glyphNameIndex))) 564 | b.WriteString(fmt.Sprintf("- post glyphNames: %d\n", len(f.post.glyphNames))) 565 | for i, gn := range f.post.glyphNames { 566 | if i > 10 { 567 | break 568 | } 569 | b.WriteString(fmt.Sprintf("- post: %d: %s\n", i+1, gn)) 570 | } 571 | b.WriteString(fmt.Sprintf("%#v\n", f.post)) 572 | default: 573 | b.WriteString(fmt.Sprintf("%s: unsupported table for info\n", table)) 574 | } 575 | 576 | return b.String() 577 | } 578 | 579 | // String outputs some readable information about the font (table record stats). 580 | func (f *font) String() string { 581 | return f.TableInfo("trec") 582 | } 583 | -------------------------------------------------------------------------------- /font_test.go: -------------------------------------------------------------------------------- 1 | package unitype 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestReadWrite(t *testing.T) { 11 | testcases := []struct { 12 | fontPath string 13 | }{ 14 | { 15 | "./testdata/FreeSans.ttf", 16 | }, 17 | /* 18 | { 19 | "./testdata/wts11.ttf", 20 | }, 21 | { 22 | "./testdata/roboto/Roboto-BoldItalic.ttf", 23 | }, 24 | */ 25 | } 26 | 27 | for _, tcase := range testcases { 28 | t.Logf("%s", tcase.fontPath) 29 | fnt, err := ParseFile(tcase.fontPath) 30 | require.NoError(t, err) 31 | 32 | logrus.Debug("Write") 33 | outPath := "/tmp/1.ttf" 34 | 35 | t.Logf("WriteFile -> %s", outPath) 36 | err = fnt.WriteFile(outPath) 37 | require.NoError(t, err) 38 | 39 | t.Logf("Validating %s...", outPath) 40 | err = ValidateFile(outPath) 41 | require.NoError(t, err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/unidoc/unitype 2 | 3 | require ( 4 | github.com/sirupsen/logrus v1.9.3 5 | github.com/stretchr/testify v1.10.0 6 | golang.org/x/text v0.21.0 7 | ) 8 | 9 | require ( 10 | github.com/davecgh/go-spew v1.1.1 // indirect 11 | github.com/kr/pretty v0.1.0 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | golang.org/x/sys v0.29.0 // indirect 14 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 15 | gopkg.in/yaml.v3 v3.0.1 // indirect 16 | ) 17 | 18 | go 1.18 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 5 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 6 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 7 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 12 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 15 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 16 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 17 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 18 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 19 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 20 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 21 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 24 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 25 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 27 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 28 | -------------------------------------------------------------------------------- /internal/strutils/encoding.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | // Package strutils provides convenient functions for string processing in unidoc internally. 7 | package strutils 8 | 9 | import ( 10 | "bytes" 11 | "unicode/utf16" 12 | 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | var pdfdocEncodingRuneMap map[rune]byte 17 | 18 | func init() { 19 | pdfdocEncodingRuneMap = map[rune]byte{} 20 | for b, r := range pdfDocEncoding { 21 | pdfdocEncodingRuneMap[r] = b 22 | } 23 | } 24 | 25 | // UTF16ToRunes decodes the UTF-16BE encoded byte slice `b` to unicode runes. 26 | func UTF16ToRunes(b []byte) []rune { 27 | if len(b) == 1 { 28 | return []rune{rune(b[0])} 29 | } 30 | if len(b)%2 != 0 { 31 | b = append(b, 0) 32 | logrus.Debug("ERROR: UTF16ToRunes. Padding with zeros.") 33 | } 34 | n := len(b) >> 1 35 | chars := make([]uint16, n) 36 | for i := 0; i < n; i++ { 37 | chars[i] = uint16(b[i<<1])<<8 + uint16(b[i<<1+1]) 38 | } 39 | runes := utf16.Decode(chars) 40 | return runes 41 | } 42 | 43 | // UTF16ToString decodes the UTF-16BE encoded byte slice `b` to a unicode go string. 44 | func UTF16ToString(b []byte) string { 45 | return string(UTF16ToRunes(b)) 46 | } 47 | 48 | // StringToUTF16 encoded `s` to UTF16 and returns a string containing UTF16 runes. 49 | func StringToUTF16(s string) string { 50 | encoded := utf16.Encode([]rune(s)) 51 | 52 | var buf bytes.Buffer 53 | for _, code := range encoded { 54 | buf.WriteByte(byte((code >> 8) & 0xff)) 55 | buf.WriteByte(byte(code & 0xff)) 56 | } 57 | 58 | return buf.String() 59 | } 60 | 61 | // PDFDocEncodingToRunes decodes PDFDocEncoded byte slice `b` to unicode runes. 62 | func PDFDocEncodingToRunes(b []byte) []rune { 63 | var runes []rune 64 | for _, bval := range b { 65 | rune, has := pdfDocEncoding[bval] 66 | if !has { 67 | logrus.Debugf("Error: PDFDocEncoding input mapping error %d - skipping", bval) 68 | continue 69 | } 70 | 71 | runes = append(runes, rune) 72 | } 73 | 74 | return runes 75 | } 76 | 77 | // PDFDocEncodingToString decodes PDFDocEncoded byte slice `b` to unicode go string. 78 | func PDFDocEncodingToString(b []byte) string { 79 | return string(PDFDocEncodingToRunes(b)) 80 | } 81 | 82 | // StringToPDFDocEncoding encoded go string `s` to PdfDocEncoding. 83 | func StringToPDFDocEncoding(s string) []byte { 84 | var buf bytes.Buffer 85 | for _, r := range s { 86 | b, has := pdfdocEncodingRuneMap[r] 87 | if !has { 88 | logrus.Debugf("ERROR: PDFDocEncoding rune mapping missing %c/%X - skipping", r, r) 89 | continue 90 | } 91 | buf.WriteByte(b) 92 | } 93 | 94 | return buf.Bytes() 95 | } 96 | -------------------------------------------------------------------------------- /internal/strutils/encoding_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package strutils 7 | 8 | import ( 9 | "testing" 10 | ) 11 | 12 | var utf16enc = []byte{ 13 | /* 0xfe, 0xff, */ 0x00, 0x62, 0x00, 0x75, 0x00, 0x74, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x41, 14 | 0x00, 0x72, 0x00, 0x65, 0x00, 0x61, 0x00, 0x53, 0x00, 0x75, 0x00, 0x62, 0x00, 0x66, 0x00, 0x6f, 15 | 0x00, 0x72, 0x00, 0x6d, 0x00, 0x5b, 0x00, 0x30, 0x00, 0x5d, 16 | } 17 | 18 | func TestUTF16Encoding(t *testing.T) { 19 | b := utf16enc 20 | exp := "buttonAreaSubform[0]" 21 | v := UTF16ToString(b) 22 | 23 | if v != exp { 24 | t.Errorf("'%s' != '%s'\n", v, exp) 25 | } 26 | } 27 | 28 | func TestUTF16EncodeDecode(t *testing.T) { 29 | testcases := []string{"þráður321", "áþðurfyrr \n", "⌘⌘⌘⺃⺓$", "€€$£"} 30 | 31 | for _, tcase := range testcases { 32 | encoded := StringToUTF16(tcase) 33 | decoded := UTF16ToString([]byte(encoded)) 34 | if decoded != tcase { 35 | t.Fatalf("'% X' != '% X'\n", decoded, tcase) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/strutils/pdfdocenc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package strutils 7 | 8 | // pdfDocEncoding defines the simple PdfDocEncoding. 9 | // Note: Is a copy from internal/textencoding/simple.go, needed to avoid import cycles. 10 | // TODO: In the future may be better to define an import-free package containing all encodingdata. 11 | var pdfDocEncoding = map[byte]rune{ 12 | 0x01: '\u0001', // "controlSTX" 13 | 0x02: '\u0002', // "controlSOT" 14 | 0x03: '\u0003', // "controlETX" 15 | 0x04: '\u0004', // "controlEOT" 16 | 0x05: '\u0005', // "controlENQ" 17 | 0x06: '\u0006', // "controlACK" 18 | 0x07: '\u0007', // "controlBEL" 19 | 0x08: '\u0008', // "controlBS" 20 | 0x09: '\u0009', // "controlHT" 21 | 0x0a: '\u000a', // "controlLF" 22 | 0x0b: '\u000b', // "controlVT" 23 | 0x0c: '\u000c', // "controlFF" 24 | 0x0d: '\u000d', // "controlCR" 25 | 0x0e: '\u000e', // "controlSO" 26 | 0x0f: '\u000f', // "controlSI" 27 | 0x10: '\u0010', // "controlDLE" 28 | 0x11: '\u0011', // "controlDC1" 29 | 0x12: '\u0012', // "controlDC2" 30 | 0x13: '\u0013', // "controlDC3" 31 | 0x14: '\u0014', // "controlDC4" 32 | 0x15: '\u0015', // "controlNAK" 33 | 0x16: '\u0017', // "controlETB" 34 | 0x17: '\u0017', // "controlETB" 35 | 0x18: '\u02d8', // ˘ "breve" 36 | 0x19: '\u02c7', // ˇ "caron" 37 | 0x1a: '\u02c6', // ˆ "circumflex" 38 | 0x1b: '\u02d9', // ˙ "dotaccent" 39 | 0x1c: '\u02dd', // ˝ "hungarumlaut" 40 | 0x1d: '\u02db', // ˛ "ogonek" 41 | 0x1e: '\u02da', // ˚ "ring" 42 | 0x1f: '\u02dc', // ˜ "ilde" 43 | 0x20: '\u0020', // "space" 44 | 0x21: '\u0021', // ! "exclam" 45 | 0x22: '\u0022', // " "quotedbl" 46 | 0x23: '\u0023', // # "numbersign" 47 | 0x24: '\u0024', // $ "dollar" 48 | 0x25: '\u0025', // % "percent" 49 | 0x26: '\u0026', // & "ampersand" 50 | 0x27: '\u0027', // ' "quotesingle" 51 | 0x28: '\u0028', // ( "parenleft" 52 | 0x29: '\u0029', // ) "parenright" 53 | 0x2a: '\u002a', // * "asterisk" 54 | 0x2b: '\u002b', // + "plus" 55 | 0x2c: '\u002c', // , "comma" 56 | 0x2d: '\u002d', // - "hyphen" 57 | 0x2e: '\u002e', // . "period" 58 | 0x2f: '\u002f', // / "slash" 59 | 0x30: '\u0030', // 0 "zero" 60 | 0x31: '\u0031', // 1 "one" 61 | 0x32: '\u0032', // 2 "two" 62 | 0x33: '\u0033', // 3 "three" 63 | 0x34: '\u0034', // 4 "four" 64 | 0x35: '\u0035', // 5 "five" 65 | 0x36: '\u0036', // 6 "six" 66 | 0x37: '\u0037', // 7 "seven" 67 | 0x38: '\u0038', // 8 "eight" 68 | 0x39: '\u0039', // 9 "nine" 69 | 0x3a: '\u003a', // : "colon" 70 | 0x3b: '\u003b', // ; "semicolon" 71 | 0x3c: '\u003c', // < "less" 72 | 0x3d: '\u003d', // = "equal" 73 | 0x3e: '\u003e', // > "greater" 74 | 0x3f: '\u003f', // ? "question" 75 | 0x40: '\u0040', // @ "at" 76 | 0x41: '\u0041', // A "A" 77 | 0x42: '\u0042', // B "B" 78 | 0x43: '\u0043', // C "C" 79 | 0x44: '\u0044', // D "D" 80 | 0x45: '\u0045', // E "E" 81 | 0x46: '\u0046', // F "F" 82 | 0x47: '\u0047', // G "G" 83 | 0x48: '\u0048', // H "H" 84 | 0x49: '\u0049', // I "I" 85 | 0x4a: '\u004a', // J "J" 86 | 0x4b: '\u004b', // K "K" 87 | 0x4c: '\u004c', // L "L" 88 | 0x4d: '\u004d', // M "M" 89 | 0x4e: '\u004e', // N "N" 90 | 0x4f: '\u004f', // O "O" 91 | 0x50: '\u0050', // P "P" 92 | 0x51: '\u0051', // Q "Q" 93 | 0x52: '\u0052', // R "R" 94 | 0x53: '\u0053', // S "S" 95 | 0x54: '\u0054', // T "T" 96 | 0x55: '\u0055', // U "U" 97 | 0x56: '\u0056', // V "V" 98 | 0x57: '\u0057', // W "W" 99 | 0x58: '\u0058', // X "X" 100 | 0x59: '\u0059', // Y "Y" 101 | 0x5a: '\u005a', // Z "Z" 102 | 0x5b: '\u005b', // [ "bracketleft" 103 | 0x5c: '\u005c', // \ "backslash" 104 | 0x5d: '\u005d', // ] "bracketright" 105 | 0x5e: '\u005e', // ^ "asciicircum" 106 | 0x5f: '\u005f', // _ "underscore" 107 | 0x60: '\u0060', // ` "grave" 108 | 0x61: '\u0061', // a "a" 109 | 0x62: '\u0062', // b "b" 110 | 0x63: '\u0063', // c "c" 111 | 0x64: '\u0064', // d "d" 112 | 0x65: '\u0065', // e "e" 113 | 0x66: '\u0066', // f "f" 114 | 0x67: '\u0067', // g "g" 115 | 0x68: '\u0068', // h "h" 116 | 0x69: '\u0069', // i "i" 117 | 0x6a: '\u006a', // j "j" 118 | 0x6b: '\u006b', // k "k" 119 | 0x6c: '\u006c', // l "l" 120 | 0x6d: '\u006d', // m "m" 121 | 0x6e: '\u006e', // n "n" 122 | 0x6f: '\u006f', // o "o" 123 | 0x70: '\u0070', // p "p" 124 | 0x71: '\u0071', // q "q" 125 | 0x72: '\u0072', // r "r" 126 | 0x73: '\u0073', // s "s" 127 | 0x74: '\u0074', // t "t" 128 | 0x75: '\u0075', // u "u" 129 | 0x76: '\u0076', // v "v" 130 | 0x77: '\u0077', // w "w" 131 | 0x78: '\u0078', // x "x" 132 | 0x79: '\u0079', // y "y" 133 | 0x7a: '\u007a', // z "z" 134 | 0x7b: '\u007b', // { "braceleft" 135 | 0x7c: '\u007c', // | "bar" 136 | 0x7d: '\u007d', // } "braceright" 137 | 0x7e: '\u007e', // ~ "asciitilde" 138 | 0x80: '\u2022', // • "bullet" 139 | 0x81: '\u2020', // † "dagger" 140 | 0x82: '\u2021', // ‡ "daggerdbl" 141 | 0x83: '\u2026', // … "ellipsis" 142 | 0x84: '\u2014', // — "emdash" 143 | 0x85: '\u2013', // – "endash" 144 | 0x86: '\u0192', // ƒ "florin" 145 | 0x87: '\u2044', // ⁄ "fraction" 146 | 0x88: '\u2039', // ‹ "guilsinglleft" 147 | 0x89: '\u203a', // › "guilsinglright" 148 | 0x8a: '\u2212', // − "minus" 149 | 0x8b: '\u2030', // ‰ "perthousand" 150 | 0x8c: '\u201e', // „ "quotedblbase" 151 | 0x8d: '\u201c', // “ "quotedblleft" 152 | 0x8e: '\u201d', // ” "quotedblright" 153 | 0x8f: '\u2018', // ‘ "quoteleft" 154 | 0x90: '\u2019', // ’ "quoteright" 155 | 0x91: '\u201a', // ‚ "quotesinglbase" 156 | 0x92: '\u2122', // ™ "trademark" 157 | 0x93: '\ufb01', // fi "fi" 158 | 0x94: '\ufb02', // fl "fl" 159 | 0x95: '\u0141', // Ł "Lslash" 160 | 0x96: '\u0152', // Œ "OE" 161 | 0x97: '\u0160', // Š "Scaron" 162 | 0x98: '\u0178', // Ÿ "Ydieresis" 163 | 0x99: '\u017d', // Ž "Zcaron" 164 | 0x9a: '\u0131', // ı "dotlessi" 165 | 0x9b: '\u0142', // ł "lslash" 166 | 0x9c: '\u0153', // œ "oe" 167 | 0x9d: '\u0161', // š "scaron" 168 | 0x9e: '\u017e', // ž "zcaron" 169 | 0xa0: '\u20ac', // € "Euro" 170 | 0xa1: '\u00a1', // ¡ "exclamdown" 171 | 0xa2: '\u00a2', // ¢ "cent" 172 | 0xa3: '\u00a3', // £ "sterling" 173 | 0xa4: '\u00a4', // ¤ "currency" 174 | 0xa5: '\u00a5', // ¥ "yen" 175 | 0xa6: '\u00a6', // ¦ "brokenbar" 176 | 0xa7: '\u00a7', // § "section" 177 | 0xa8: '\u00a8', // ¨ "dieresis" 178 | 0xa9: '\u00a9', // © "copyright" 179 | 0xaa: '\u00aa', // ª "ordfeminine" 180 | 0xab: '\u00ab', // « "guillemotleft" 181 | 0xac: '\u00ac', // ¬ "logicalnot" 182 | 0xae: '\u00ae', // ® "registered" 183 | 0xaf: '\u00af', // ¯ "macron" 184 | 0xb0: '\u00b0', // ° "degree" 185 | 0xb1: '\u00b1', // ± "plusminus" 186 | 0xb2: '\u00b2', // ² "twosuperior" 187 | 0xb3: '\u00b3', // ³ "threesuperior" 188 | 0xb4: '\u00b4', // ´ "acute" 189 | 0xb5: '\u00b5', // µ "mu" 190 | 0xb6: '\u00b6', // ¶ "paragraph" 191 | 0xb7: '\u00b7', // · "middot" 192 | 0xb8: '\u00b8', // ¸ "cedilla" 193 | 0xb9: '\u00b9', // ¹ "onesuperior" 194 | 0xba: '\u00ba', // º "ordmasculine" 195 | 0xbb: '\u00bb', // » "guillemotright" 196 | 0xbc: '\u00bc', // ¼ "onequarter" 197 | 0xbd: '\u00bd', // ½ "onehalf" 198 | 0xbe: '\u00be', // ¾ "threequarters" 199 | 0xbf: '\u00bf', // ¿ "questiondown" 200 | 0xc0: '\u00c0', // À "Agrave" 201 | 0xc1: '\u00c1', // Á "Aacute" 202 | 0xc2: '\u00c2', //  "Acircumflex" 203 | 0xc3: '\u00c3', // à "Atilde" 204 | 0xc4: '\u00c4', // Ä "Adieresis" 205 | 0xc5: '\u00c5', // Å "Aring" 206 | 0xc6: '\u00c6', // Æ "AE" 207 | 0xc7: '\u00c7', // Ç "Ccedilla" 208 | 0xc8: '\u00c8', // È "Egrave" 209 | 0xc9: '\u00c9', // É "Eacute" 210 | 0xca: '\u00ca', // Ê "Ecircumflex" 211 | 0xcb: '\u00cb', // Ë "Edieresis" 212 | 0xcc: '\u00cc', // Ì "Igrave" 213 | 0xcd: '\u00cd', // Í "Iacute" 214 | 0xce: '\u00ce', // Î "Icircumflex" 215 | 0xcf: '\u00cf', // Ï "Idieresis" 216 | 0xd0: '\u00d0', // Ð "Eth" 217 | 0xd1: '\u00d1', // Ñ "Ntilde" 218 | 0xd2: '\u00d2', // Ò "Ograve" 219 | 0xd3: '\u00d3', // Ó "Oacute" 220 | 0xd4: '\u00d4', // Ô "Ocircumflex" 221 | 0xd5: '\u00d5', // Õ "Otilde" 222 | 0xd6: '\u00d6', // Ö "Odieresis" 223 | 0xd7: '\u00d7', // × "multiply" 224 | 0xd8: '\u00d8', // Ø "Oslash" 225 | 0xd9: '\u00d9', // Ù "Ugrave" 226 | 0xda: '\u00da', // Ú "Uacute" 227 | 0xdb: '\u00db', // Û "Ucircumflex" 228 | 0xdc: '\u00dc', // Ü "Udieresis" 229 | 0xdd: '\u00dd', // Ý "Yacute" 230 | 0xde: '\u00de', // Þ "Thorn" 231 | 0xdf: '\u00df', // ß "germandbls" 232 | 0xe0: '\u00e0', // à "agrave" 233 | 0xe1: '\u00e1', // á "aacute" 234 | 0xe2: '\u00e2', // â "acircumflex" 235 | 0xe3: '\u00e3', // ã "atilde" 236 | 0xe4: '\u00e4', // ä "adieresis" 237 | 0xe5: '\u00e5', // å "aring" 238 | 0xe6: '\u00e6', // æ "ae" 239 | 0xe7: '\u00e7', // ç "ccedilla" 240 | 0xe8: '\u00e8', // è "egrave" 241 | 0xe9: '\u00e9', // é "eacute" 242 | 0xea: '\u00ea', // ê "ecircumflex" 243 | 0xeb: '\u00eb', // ë "edieresis" 244 | 0xec: '\u00ec', // ì "igrave" 245 | 0xed: '\u00ed', // í "iacute" 246 | 0xee: '\u00ee', // î "icircumflex" 247 | 0xef: '\u00ef', // ï "idieresis" 248 | 0xf0: '\u00f0', // ð "eth" 249 | 0xf1: '\u00f1', // ñ "ntilde" 250 | 0xf2: '\u00f2', // ò "ograve" 251 | 0xf3: '\u00f3', // ó "oacute" 252 | 0xf4: '\u00f4', // ô "ocircumflex" 253 | 0xf5: '\u00f5', // õ "otilde" 254 | 0xf6: '\u00f6', // ö "odieresis" 255 | 0xf7: '\u00f7', // ÷ "divide" 256 | 0xf8: '\u00f8', // ø "oslash" 257 | 0xf9: '\u00f9', // ù "ugrave" 258 | 0xfa: '\u00fa', // ú "uacute" 259 | 0xfb: '\u00fb', // û "ucircumflex" 260 | 0xfc: '\u00fc', // ü "udieresis" 261 | 0xfd: '\u00fd', // ý "yacute" 262 | 0xfe: '\u00fe', // þ "thorn" 263 | 0xff: '\u00ff', // ÿ "ydieresis" 264 | } 265 | -------------------------------------------------------------------------------- /internal/strutils/pdfdocenc_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package strutils 7 | 8 | import ( 9 | "bytes" 10 | "testing" 11 | ) 12 | 13 | func TestPDFDocEncodingDecode(t *testing.T) { 14 | testcases := []struct { 15 | Encoded []byte 16 | Expected string 17 | }{ 18 | {[]byte{0x47, 0x65, 0x72, 0xfe, 0x72, 0xfa, 0xf0, 0x75, 0x72}, "Gerþrúður"}, 19 | {[]byte("Ger\xfer\xfa\xf0ur"), "Gerþrúður"}, 20 | } 21 | 22 | for _, testcase := range testcases { 23 | str := PDFDocEncodingToString(testcase.Encoded) 24 | if str != testcase.Expected { 25 | t.Fatalf("Mismatch %s != %s", str, testcase.Expected) 26 | } 27 | 28 | enc := StringToPDFDocEncoding(str) 29 | if !bytes.Equal(enc, testcase.Encoded) { 30 | t.Fatalf("Encode mismatch %s (%X) != %s (%X)", enc, enc, testcase.Encoded, testcase.Encoded) 31 | } 32 | } 33 | 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /io_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "encoding/binary" 10 | "io" 11 | "os" 12 | "testing" 13 | ) 14 | 15 | /* 16 | Comparing use of binary.Read vs binary.BigEndian.... direct use. 17 | Benchmark results indicate that the performance in all cases is pretty comparable, 18 | within 5% difference. Thus choosing the simplest option (first one). 19 | 20 | BenchmarkBinaryRead-8 3000 509553 ns/op 21 | --- BENCH: BenchmarkBinaryRead-8 22 | io_test.go:26: Result: 1633005 (N: 1) 23 | io_test.go:26: Result: 163300500 (N: 100) 24 | io_test.go:26: Result: 4899015000 (N: 3000) 25 | BenchmarkBinaryRead2-8 3000 492651 ns/op 26 | --- BENCH: BenchmarkBinaryRead2-8 27 | io_test.go:49: Result: 1633005 (N: 1) 28 | io_test.go:49: Result: 163300500 (N: 100) 29 | io_test.go:49: Result: 4899015000 (N: 3000) 30 | BenchmarkBinaryRead3-8 3000 508578 ns/op 31 | --- BENCH: BenchmarkBinaryRead3-8 32 | io_test.go:76: Result: 1633005 (N: 1) 33 | io_test.go:76: Result: 163300500 (N: 100) 34 | io_test.go:76: Result: 4899015000 (N: 3000) 35 | PASS 36 | ok github.com/unidoc/unitype 10.357s 37 | */ 38 | 39 | func BenchmarkBinaryRead(b *testing.B) { 40 | f, err := os.Open("./testdata/FreeSans.ttf") 41 | if err != nil { 42 | b.Fatalf("Error: %v", err) 43 | } 44 | defer f.Close() 45 | 46 | var sum int64 47 | for i := 0; i < b.N; i++ { 48 | f.Seek(0, io.SeekStart) 49 | for j := 0; j < 100; j++ { 50 | var val offset16 51 | binary.Read(f, binary.BigEndian, &val) 52 | sum += int64(val) 53 | } 54 | } 55 | b.Logf("Result: %d (N: %d)", sum, b.N) 56 | } 57 | 58 | func BenchmarkBinaryRead2(b *testing.B) { 59 | f, err := os.Open("./testdata/FreeSans.ttf") 60 | if err != nil { 61 | b.Fatalf("Error: %v", err) 62 | } 63 | defer f.Close() 64 | 65 | var sum int64 66 | for i := 0; i < b.N; i++ { 67 | f.Seek(0, io.SeekStart) 68 | for j := 0; j < 100; j++ { 69 | data := make([]byte, 2) 70 | _, err = io.ReadFull(f, data) 71 | if err != nil { 72 | b.Fatalf("Error: %v", err) 73 | } 74 | val := offset16(binary.BigEndian.Uint16(data)) 75 | sum += int64(val) 76 | } 77 | } 78 | b.Logf("Result: %d (N: %d)", sum, b.N) 79 | } 80 | 81 | func BenchmarkBinaryRead3(b *testing.B) { 82 | f, err := os.Open("./testdata/FreeSans.ttf") 83 | if err != nil { 84 | b.Fatalf("Error: %v", err) 85 | } 86 | defer f.Close() 87 | 88 | readOffset16 := func(r io.Reader) (offset16, error) { 89 | var val offset16 90 | err := binary.Read(f, binary.BigEndian, &val) 91 | return val, err 92 | } 93 | 94 | var sum int64 95 | for i := 0; i < b.N; i++ { 96 | f.Seek(0, io.SeekStart) 97 | for j := 0; j < 100; j++ { 98 | val, err := readOffset16(f) 99 | if err != nil { 100 | b.Fatalf("Error: %v", err) 101 | } 102 | sum += int64(val) 103 | } 104 | } 105 | b.Logf("Result: %d (N: %d)", sum, b.N) 106 | } 107 | -------------------------------------------------------------------------------- /table_cmap_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "bytes" 10 | "os" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestCmapTableReadWrite(t *testing.T) { 18 | type expectedCmap struct { 19 | format int 20 | platformID int 21 | encodingID int 22 | numRuneEntries int 23 | numMapEntries int // number of entries in the map 24 | checks map[rune]GlyphIndex // a few spot checks. 25 | } 26 | testcases := []struct { 27 | fontPath string 28 | expectedCmaps map[string]expectedCmap 29 | }{ 30 | { 31 | "./testdata/FreeSans.ttf", 32 | map[string]expectedCmap{ 33 | "4,0,3": { 34 | 4, 35 | 0, 36 | 3, 37 | 3726, 38 | 10378 - 7574 + 1, 39 | map[rune]GlyphIndex{ 40 | 'a': 70, 41 | ' ': 5, 42 | '!': 6, 43 | '@': 37, 44 | 'Æ': 138, 45 | 'π': 711, 46 | rune(0x1FFE): 2193, 47 | }, 48 | }, 49 | "6,1,0": { 50 | 6, 51 | 1, 52 | 0, 53 | 256, 54 | 10636 - 10381 + 1, 55 | map[rune]GlyphIndex{ 56 | 'a': 70, 57 | ' ': 5, 58 | '!': 6, 59 | '@': 37, 60 | 'Æ': 138, 61 | 'π': 711, 62 | }, 63 | }, 64 | "4,3,1": { 65 | 4, 66 | 3, 67 | 1, 68 | 3726, 69 | 13443 - 10639 + 1, 70 | map[rune]GlyphIndex{ 71 | 'a': 70, 72 | ' ': 5, 73 | '!': 6, 74 | '@': 37, 75 | 'Æ': 138, 76 | 'π': 711, 77 | rune(0x1FFE): 2193, 78 | }, 79 | }, 80 | }, 81 | }, 82 | { 83 | "./testdata/wts11.ttf", 84 | map[string]expectedCmap{ 85 | "4,0,3": { 86 | 4, 87 | 0, 88 | 3, 89 | 14148, 90 | 42387 - 28418 + 1, 91 | map[rune]GlyphIndex{}, 92 | }, 93 | "0,1,0": { 94 | 0, 95 | 1, 96 | 0, 97 | 256, 98 | 105, //42645 - 42390 + 1, not counting notdefs 99 | map[rune]GlyphIndex{}, 100 | }, 101 | "4,3,1": { 102 | 4, 103 | 3, 104 | 1, 105 | 14148, 106 | 56617 - 42648 + 1, 107 | map[rune]GlyphIndex{}, 108 | }, 109 | }, 110 | }, 111 | { 112 | "./testdata/roboto/Roboto-BoldItalic.ttf", 113 | map[string]expectedCmap{ 114 | "4,0,3": { 115 | 4, 116 | 0, 117 | 3, 118 | 1294, 119 | 896, 120 | map[rune]GlyphIndex{}, 121 | }, 122 | "4,3,1": { 123 | 4, 124 | 3, 125 | 1, 126 | 1294, 127 | 896, 128 | map[rune]GlyphIndex{}, 129 | }, 130 | "12,3,10": { 131 | 12, 132 | 3, 133 | 10, 134 | 1294, 135 | 896, 136 | map[rune]GlyphIndex{}, 137 | }, 138 | }, 139 | }, 140 | } 141 | 142 | for _, tcase := range testcases { 143 | t.Run(tcase.fontPath, func(t *testing.T) { 144 | t.Logf("%s", tcase.fontPath) 145 | f, err := os.Open(tcase.fontPath) 146 | assert.Equal(t, nil, err) 147 | defer f.Close() 148 | 149 | br := newByteReader(f) 150 | fnt, err := parseFont(br) 151 | assert.Equal(t, nil, err) 152 | require.NoError(t, err) 153 | 154 | require.NotNil(t, fnt) 155 | require.NotNil(t, fnt.cmap) 156 | require.NotNil(t, fnt.cmap.subtables) 157 | 158 | require.Equal(t, len(tcase.expectedCmaps), len(fnt.cmap.subtables)) 159 | 160 | for _, key := range fnt.cmap.subtableKeys { 161 | subtable := fnt.cmap.subtables[key] 162 | t.Logf("subtable %d %d/%d '%s'", subtable.format, subtable.platformID, subtable.encodingID, key) 163 | exp := tcase.expectedCmaps[key] 164 | require.Equal(t, exp.format, subtable.format) 165 | require.Equal(t, exp.platformID, subtable.platformID) 166 | require.Equal(t, exp.encodingID, subtable.encodingID) 167 | require.Equal(t, exp.numRuneEntries, len(subtable.runes)) 168 | require.Equal(t, exp.numMapEntries, len(subtable.cmap)) 169 | 170 | t.Logf("- cmap len: %d", len(subtable.cmap)) 171 | // spot checks. 172 | for r, gid := range exp.checks { 173 | t.Logf("%c 0x%X", r, r) 174 | require.Equal(t, gid, subtable.cmap[r]) 175 | } 176 | } 177 | 178 | // Write, read back and repeat checks. 179 | var buf bytes.Buffer 180 | bw := newByteWriter(&buf) 181 | err = fnt.write(bw) 182 | require.NoError(t, err) 183 | err = bw.flush() 184 | require.NoError(t, err) 185 | br = newByteReader(bytes.NewReader(buf.Bytes())) 186 | fnt, err = parseFont(br) 187 | assert.Equal(t, nil, err) 188 | require.NoError(t, err) 189 | require.NotNil(t, fnt) 190 | require.NotNil(t, fnt.cmap) 191 | require.NotNil(t, fnt.cmap.subtables) 192 | require.Equal(t, len(tcase.expectedCmaps), len(fnt.cmap.subtables)) 193 | for _, key := range fnt.cmap.subtableKeys { 194 | subtable := fnt.cmap.subtables[key] 195 | t.Logf("2 subtable %d %d/%d '%s'", subtable.format, subtable.platformID, subtable.encodingID, key) 196 | exp := tcase.expectedCmaps[key] 197 | require.Equal(t, exp.format, subtable.format) 198 | require.Equal(t, exp.platformID, subtable.platformID) 199 | require.Equal(t, exp.encodingID, subtable.encodingID) 200 | require.Equal(t, exp.numRuneEntries, len(subtable.runes)) 201 | require.Equal(t, exp.numMapEntries, len(subtable.cmap)) 202 | 203 | t.Logf("2 - cmap len: %d", len(subtable.cmap)) 204 | 205 | // spot checks. 206 | for r, gid := range exp.checks { 207 | t.Logf("%c 0x%X", r, r) 208 | require.Equal(t, gid, subtable.cmap[r]) 209 | } 210 | } 211 | }) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /table_cvt.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // cvtTable represents the Control Value Table (cvt). 13 | // This table contains a list of values that can be referenced by instructions. 14 | // TODO: For subsetting/optimization it would be good to know what glyphs need each value, so non-used values can be removed. 15 | // Probably part of optimization in the locations where these values are referenced. 16 | type cvtTable struct { 17 | controlValues []int16 //fword 18 | } 19 | 20 | func (f *font) parseCvt(r *byteReader) (*cvtTable, error) { 21 | tr, has, err := f.seekToTable(r, "cvt") 22 | if err != nil { 23 | return nil, err 24 | } 25 | if !has || tr == nil { 26 | logrus.Debug("cvt table absent") 27 | return nil, nil 28 | } 29 | 30 | t := &cvtTable{} 31 | numVals := int(tr.length / 2) 32 | err = r.readSlice(&t.controlValues, numVals) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return t, nil 37 | } 38 | 39 | func (f *font) writeCvt(w *byteWriter) error { 40 | if f.cvt == nil { 41 | return nil 42 | } 43 | 44 | return w.writeSlice(f.cvt.controlValues) 45 | } 46 | -------------------------------------------------------------------------------- /table_fpgm.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // fpgmTable represents font program instructions and is needed by fonts that are instructed. 13 | type fpgmTable struct { 14 | instructions []uint8 15 | } 16 | 17 | func (f *font) parseFpgm(r *byteReader) (*fpgmTable, error) { 18 | tr, has, err := f.seekToTable(r, "fpgm") 19 | if err != nil { 20 | return nil, err 21 | } 22 | if !has || tr == nil { 23 | logrus.Debug("fpgm table absent") 24 | return nil, nil 25 | } 26 | 27 | t := &fpgmTable{} 28 | numVals := int(tr.length) 29 | err = r.readSlice(&t.instructions, numVals) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return t, nil 34 | } 35 | 36 | func (f *font) writeFpgm(w *byteWriter) error { 37 | if f.fpgm == nil { 38 | return nil 39 | } 40 | 41 | return w.writeSlice(f.fpgm.instructions) 42 | } 43 | -------------------------------------------------------------------------------- /table_glyf_test.go: -------------------------------------------------------------------------------- 1 | package unitype 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | // Try reading in a glyf table and write back out, read again and verify. 15 | func TestGlyfReadWrite(t *testing.T) { 16 | testcases := []struct { 17 | fontPath string 18 | }{ 19 | { 20 | "./testdata/FreeSans.ttf", 21 | }, 22 | /* 23 | { 24 | "../../creator/testdata/wts11.ttf", 25 | 14148, 26 | }, 27 | { 28 | "../../creator/testdata/roboto/Roboto-BoldItalic.ttf", 29 | 1294, 30 | }, 31 | */ 32 | } 33 | 34 | for _, tcase := range testcases { 35 | t.Run(tcase.fontPath, func(t *testing.T) { 36 | t.Logf("%s", tcase.fontPath) 37 | f, err := os.Open(tcase.fontPath) 38 | assert.Equal(t, nil, err) 39 | defer f.Close() 40 | 41 | br := newByteReader(f) 42 | fnt, err := parseFont(br) 43 | assert.Equal(t, nil, err) 44 | require.NoError(t, err) 45 | 46 | require.NotNil(t, fnt) 47 | require.NotNil(t, fnt.glyf) 48 | 49 | // Read the glyf table from the font. 50 | tr := fnt.trec.trMap["glyf"] 51 | f.Seek(int64(tr.offset), io.SeekStart) 52 | b := make([]byte, tr.length) 53 | _, err = io.ReadFull(f, b) 54 | require.NoError(t, err) 55 | 56 | // Write the glyf table out to glyfBuf. 57 | var glyfBuf bytes.Buffer 58 | glyfw := newByteWriter(&glyfBuf) 59 | err = fnt.writeGlyf(glyfw) 60 | require.NoError(t, err) 61 | err = glyfw.flush() 62 | require.NoError(t, err) 63 | 64 | fmt.Printf("Read (%d):\n", len(b)) 65 | fmt.Printf("Write (%d):\n", len(glyfBuf.Bytes())) 66 | require.Equal(t, b, glyfBuf.Bytes()) 67 | 68 | // Hack to set position right for this buffer. 69 | tr.offset = 0 70 | fnt.trec.trMap["glyf"] = tr 71 | 72 | // Try to parse? 73 | glyfr := newByteReader(bytes.NewReader(glyfBuf.Bytes())) 74 | 75 | glyft, err := fnt.parseGlyf(glyfr) 76 | require.NoError(t, err) 77 | require.Equal(t, fnt.glyf, glyft) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /table_head.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "errors" 10 | 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // Font header. 15 | // https://docs.microsoft.com/en-us/typography/opentype/spec/head 16 | type headTable struct { 17 | majorVersion uint16 // 00 01 18 | minorVersion uint16 // 00 00 19 | fontRevision fixed // 00 01 CA 3D 20 | checksumAdjustment uint32 // 00 00 00 00 21 | magicNumber uint32 // 5F 0F 3C F5 22 | flags uint16 23 | unitsPerEm uint16 24 | created longdatetime 25 | modified longdatetime 26 | xMin int16 27 | yMin int16 28 | xMax int16 29 | yMax int16 30 | macStyle uint16 31 | lowestRecPPEM uint16 32 | fontDirectionHint int16 33 | indexToLocFormat int16 34 | glyphDataFormat int16 35 | } 36 | 37 | // parse the font's *head* table from `r` in the context of `f`. 38 | // TODO(gunnsth): Read the table as bytes first and then process? Probably easier in terms of checksumming etc. 39 | func (f *font) parseHead(r *byteReader) (*headTable, error) { 40 | _, has, err := f.seekToTable(r, "head") 41 | if err != nil { 42 | return nil, err 43 | } 44 | if !has { 45 | // Does not have head. 46 | return nil, nil 47 | } 48 | 49 | t := &headTable{} 50 | err = r.read(&t.majorVersion, &t.minorVersion, &t.fontRevision) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | err = r.read(&t.checksumAdjustment, &t.magicNumber) 56 | if err != nil { 57 | return nil, err 58 | } 59 | if t.magicNumber != 0x5F0F3CF5 { 60 | logrus.Debugf("Error: got magic number 0x%X", t.magicNumber) 61 | return nil, errors.New("magic number mismatch") 62 | } 63 | 64 | err = r.read(&t.flags, &t.unitsPerEm, &t.created, &t.modified) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | err = r.read(&t.xMin, &t.yMin, &t.xMax, &t.yMax) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return t, r.read(&t.macStyle, &t.lowestRecPPEM, &t.fontDirectionHint, &t.indexToLocFormat, &t.glyphDataFormat) 75 | } 76 | 77 | func (f *font) writeHead(w *byteWriter) error { 78 | if f.head == nil { 79 | return errRequiredField 80 | } 81 | t := f.head 82 | err := w.write(t.majorVersion, t.minorVersion, t.fontRevision, t.checksumAdjustment, t.magicNumber) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | err = w.write(t.flags, t.unitsPerEm, t.created, t.modified, t.xMin, t.yMin, t.xMax, t.yMax) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return w.write(t.macStyle, t.lowestRecPPEM, t.fontDirectionHint, t.indexToLocFormat, t.glyphDataFormat) 93 | } 94 | -------------------------------------------------------------------------------- /table_hhea.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // hheaTable represents the horizontal header table (hhea). 13 | // This table contains information for horizontal layout. 14 | // https://docs.microsoft.com/en-us/typography/opentype/spec/hhea 15 | type hheaTable struct { 16 | majorVersion uint16 17 | minorVersion uint16 18 | ascender fword 19 | descender fword 20 | lineGap fword 21 | advanceWidthMax ufword 22 | minLeftSideBearing fword 23 | minRightSideBearing fword 24 | xMaxExtent fword 25 | caretSlopeRise int16 26 | caretSlopeRun int16 27 | caretOffset int16 28 | metricDataFormat int16 29 | numberOfHMetrics uint16 // Number of hMetric entries in 'hmtx' table. 30 | } 31 | 32 | func (f *font) parseHhea(r *byteReader) (*hheaTable, error) { 33 | _, has, err := f.seekToTable(r, "hhea") 34 | if err != nil { 35 | return nil, err 36 | } 37 | if !has { 38 | logrus.Debug("hhea table absent") 39 | return nil, nil 40 | } 41 | 42 | t := &hheaTable{} 43 | err = r.read(&t.majorVersion, &t.minorVersion) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | err = r.read(&t.ascender, &t.descender, &t.lineGap) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | err = r.read(&t.advanceWidthMax, &t.minLeftSideBearing, &t.minRightSideBearing, &t.xMaxExtent) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | err = r.read(&t.caretSlopeRise, &t.caretSlopeRun, &t.caretOffset) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | // Skip over reserved bytes. 64 | err = r.Skip(4 * 2) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return t, r.read(&t.metricDataFormat, &t.numberOfHMetrics) 70 | } 71 | 72 | func (f *font) writeHhea(w *byteWriter) error { 73 | if f.hhea == nil { 74 | logrus.Debug("hhea is nil - nothing to write") 75 | return nil 76 | } 77 | 78 | t := f.hhea 79 | err := w.write(t.majorVersion, t.minorVersion) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | err = w.write(t.ascender, t.descender, t.lineGap) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | err = w.write(t.advanceWidthMax, t.minLeftSideBearing, t.minRightSideBearing, t.xMaxExtent) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | err = w.write(t.caretSlopeRise, t.caretSlopeRun, t.caretOffset) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | reserved := int16(0) 100 | err = w.write(reserved, reserved, reserved, reserved) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | return w.write(t.metricDataFormat, t.numberOfHMetrics) 106 | } 107 | -------------------------------------------------------------------------------- /table_hmtx.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type hmtxTable struct { 13 | hMetrics []longHorMetric // length is numberOfHMetrics from hhea table. 14 | leftSideBearings []int16 // length is (numGlyphs - numberOfHmetrics) from maxp and hhea tables. 15 | } 16 | 17 | type longHorMetric struct { 18 | advanceWidth uint16 19 | lsb int16 20 | } 21 | 22 | func (f *font) parseHmtx(r *byteReader) (*hmtxTable, error) { 23 | if f.maxp == nil || f.hhea == nil { 24 | logrus.Debug("maxp or hhea table missing") 25 | return nil, errRequiredField 26 | } 27 | 28 | _, has, err := f.seekToTable(r, "hmtx") 29 | if err != nil { 30 | return nil, err 31 | } 32 | if !has { 33 | logrus.Debug("hmtx table absent") 34 | return nil, nil 35 | } 36 | 37 | t := &hmtxTable{} 38 | 39 | numberOfHMetrics := int(f.hhea.numberOfHMetrics) 40 | for i := 0; i < numberOfHMetrics; i++ { 41 | var lhm longHorMetric 42 | err := r.read(&lhm.advanceWidth, &lhm.lsb) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | t.hMetrics = append(t.hMetrics, lhm) 48 | } 49 | 50 | lsbLen := int(f.maxp.numGlyphs) - numberOfHMetrics 51 | if lsbLen > 0 { 52 | err = r.readSlice(&t.leftSideBearings, lsbLen) 53 | if err != nil { 54 | return nil, err 55 | } 56 | } 57 | 58 | return t, nil 59 | } 60 | 61 | // optimizeHmtx optimizes the htmx table. 62 | func (f *font) optimizeHmtx() { 63 | i := len(f.hmtx.hMetrics) - 1 64 | if i <= 0 { 65 | return 66 | } 67 | lastWidth := f.hmtx.hMetrics[i].advanceWidth 68 | j := i - 1 69 | for j >= 0 && f.hmtx.hMetrics[j].advanceWidth == lastWidth { 70 | j-- 71 | } 72 | numStrip := i - j - 1 73 | if numStrip == 0 { 74 | return 75 | } 76 | 77 | f.hhea.numberOfHMetrics = uint16(j + 2) 78 | var lsbPrepend []int16 79 | for k := j + 2; k <= i; k++ { 80 | lsbPrepend = append(lsbPrepend, f.hmtx.hMetrics[k].lsb) 81 | } 82 | f.hmtx.leftSideBearings = append(lsbPrepend, f.hmtx.leftSideBearings...) 83 | f.hmtx.hMetrics = f.hmtx.hMetrics[0 : j+2] 84 | } 85 | 86 | // writeHmtx writes the font's hmtx table to `w`. 87 | func (f *font) writeHmtx(w *byteWriter) error { 88 | if f.hmtx == nil || f.hhea == nil { 89 | return nil 90 | } 91 | 92 | for _, lhm := range f.hmtx.hMetrics { 93 | err := w.write(lhm.advanceWidth, lhm.lsb) 94 | if err != nil { 95 | return err 96 | } 97 | } 98 | 99 | return w.writeSlice(f.hmtx.leftSideBearings) 100 | } 101 | -------------------------------------------------------------------------------- /table_hmtx_test.go: -------------------------------------------------------------------------------- 1 | package unitype 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestOptimizeHmtxTable(t *testing.T) { 10 | testcases := []struct { 11 | fnt *font 12 | expNumGlyphs int 13 | expNumHMetrics int 14 | expLSB []int16 15 | exphMetrics []longHorMetric 16 | }{ 17 | { 18 | fnt: &font{ 19 | maxp: &maxpTable{ 20 | numGlyphs: 5, 21 | }, 22 | hhea: &hheaTable{ 23 | numberOfHMetrics: 3, 24 | }, 25 | hmtx: &hmtxTable{ 26 | hMetrics: []longHorMetric{ 27 | {advanceWidth: 10, lsb: 1}, 28 | {advanceWidth: 20, lsb: 2}, 29 | {advanceWidth: 30, lsb: 3}, 30 | }, 31 | leftSideBearings: []int16{ 32 | 4, 33 | 5, 34 | }, 35 | }, 36 | }, 37 | expNumGlyphs: 5, 38 | expNumHMetrics: 3, 39 | exphMetrics: []longHorMetric{ 40 | {advanceWidth: 10, lsb: 1}, 41 | {advanceWidth: 20, lsb: 2}, 42 | {advanceWidth: 30, lsb: 3}, 43 | }, 44 | expLSB: []int16{4, 5}, 45 | }, 46 | { 47 | fnt: &font{ 48 | maxp: &maxpTable{ 49 | numGlyphs: 7, 50 | }, 51 | hhea: &hheaTable{ 52 | numberOfHMetrics: 7, 53 | }, 54 | hmtx: &hmtxTable{ 55 | hMetrics: []longHorMetric{ 56 | {advanceWidth: 10, lsb: 1}, 57 | {advanceWidth: 20, lsb: 2}, 58 | {advanceWidth: 30, lsb: 3}, 59 | {advanceWidth: 40, lsb: 4}, 60 | {advanceWidth: 50, lsb: 5}, 61 | {advanceWidth: 60, lsb: 6}, // should include this once optimized. 62 | {advanceWidth: 60, lsb: 7}, 63 | }, 64 | leftSideBearings: []int16{}, 65 | }, 66 | }, 67 | expNumGlyphs: 7, 68 | expNumHMetrics: 6, 69 | exphMetrics: []longHorMetric{ 70 | {advanceWidth: 10, lsb: 1}, 71 | {advanceWidth: 20, lsb: 2}, 72 | {advanceWidth: 30, lsb: 3}, 73 | {advanceWidth: 40, lsb: 4}, 74 | {advanceWidth: 50, lsb: 5}, 75 | {advanceWidth: 60, lsb: 6}, 76 | }, 77 | expLSB: []int16{7}, 78 | }, 79 | { 80 | fnt: &font{ 81 | maxp: &maxpTable{ 82 | numGlyphs: 13, 83 | }, 84 | hhea: &hheaTable{ 85 | numberOfHMetrics: 10, 86 | }, 87 | hmtx: &hmtxTable{ 88 | hMetrics: []longHorMetric{ 89 | {advanceWidth: 10, lsb: 1}, 90 | {advanceWidth: 20, lsb: 2}, 91 | {advanceWidth: 30, lsb: 3}, 92 | {advanceWidth: 40, lsb: 4}, 93 | {advanceWidth: 50, lsb: 5}, 94 | {advanceWidth: 60, lsb: 6}, // should include this once optimized. 95 | {advanceWidth: 60, lsb: 7}, 96 | {advanceWidth: 60, lsb: 8}, 97 | {advanceWidth: 60, lsb: 9}, 98 | {advanceWidth: 60, lsb: 10}, 99 | }, 100 | leftSideBearings: []int16{ 101 | 11, 102 | 12, 103 | 13, 104 | }, 105 | }, 106 | }, 107 | expNumGlyphs: 13, 108 | expNumHMetrics: 6, 109 | exphMetrics: []longHorMetric{ 110 | {advanceWidth: 10, lsb: 1}, 111 | {advanceWidth: 20, lsb: 2}, 112 | {advanceWidth: 30, lsb: 3}, 113 | {advanceWidth: 40, lsb: 4}, 114 | {advanceWidth: 50, lsb: 5}, 115 | {advanceWidth: 60, lsb: 6}, 116 | }, 117 | expLSB: []int16{7, 8, 9, 10, 11, 12, 13}, 118 | }, 119 | } 120 | 121 | for _, tcase := range testcases { 122 | tcase.fnt.optimizeHmtx() 123 | assert.EqualValues(t, tcase.expNumGlyphs, tcase.fnt.maxp.numGlyphs) 124 | assert.EqualValues(t, tcase.expNumHMetrics, tcase.fnt.hhea.numberOfHMetrics) 125 | assert.Len(t, tcase.fnt.hmtx.hMetrics, tcase.expNumHMetrics) 126 | assert.Len(t, tcase.fnt.hmtx.leftSideBearings, tcase.expNumGlyphs-tcase.expNumHMetrics) 127 | assert.Equal(t, tcase.expLSB, tcase.fnt.hmtx.leftSideBearings) 128 | assert.Equal(t, tcase.exphMetrics, tcase.fnt.hmtx.hMetrics) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /table_loca.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "errors" 10 | 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // locaTable represents the Index to Location (loca) table. 15 | // https://docs.microsoft.com/en-us/typography/opentype/spec/loca 16 | type locaTable struct { 17 | // The extra entry at the end helps calculating the length of the last glyph data element. 18 | offsetsShort []offset16 // short format. (numGlyphs+1 entries). 19 | offsetsLong []offset32 // long format. (numGlyphs+1 entries). 20 | } 21 | 22 | // GetGlyphDataOffset returns offset for glyph index `gid`. The offset is relative to 23 | // the beginning of the glyf table. 24 | func (f *font) GetGlyphDataOffset(gid GlyphIndex) (offset int64, len int64, err error) { 25 | if f.loca == nil || f.head == nil { 26 | logrus.Debug("loca or head missing") 27 | return 0, 0, errRequiredField 28 | } 29 | if gid < 0 || int(gid) >= int(f.maxp.numGlyphs) { 30 | logrus.Debug("invalid range") 31 | return 0, 0, errRangeCheck 32 | } 33 | 34 | short := f.head.indexToLocFormat == 0 35 | if short { 36 | offset1 := 2 * int64(f.loca.offsetsShort[gid]) 37 | offset2 := 2 * int64(f.loca.offsetsShort[gid+1]) 38 | return offset1, offset2 - offset1, nil 39 | } 40 | 41 | offset1 := int64(f.loca.offsetsLong[gid]) 42 | offset2 := int64(f.loca.offsetsLong[gid+1]) 43 | return offset1, offset2 - offset1, nil 44 | } 45 | 46 | func (f *font) parseLoca(r *byteReader) (*locaTable, error) { 47 | if f.head == nil || f.maxp == nil { 48 | logrus.Debug("head or maxp not set - required missing") 49 | return nil, errRequiredField 50 | } 51 | 52 | _, has, err := f.seekToTable(r, "loca") 53 | if err != nil { 54 | return nil, err 55 | } 56 | if !has { 57 | logrus.Debug("loca table not present") 58 | return nil, nil 59 | } 60 | 61 | if f.head.indexToLocFormat < 0 || f.head.indexToLocFormat > 1 { 62 | logrus.Debug("Invalid index to loca value") 63 | return nil, errRangeCheck 64 | } 65 | 66 | loca := &locaTable{} 67 | 68 | numGlyphs := int(f.maxp.numGlyphs) 69 | isShort := f.head.indexToLocFormat == 0 70 | 71 | if isShort { 72 | err := r.readSlice(&loca.offsetsShort, numGlyphs+1) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return loca, nil 77 | } 78 | 79 | err = r.readSlice(&loca.offsetsLong, numGlyphs+1) 80 | if err != nil { 81 | return nil, err 82 | } 83 | for i := 0; i < numGlyphs; i++ { 84 | offset := loca.offsetsLong[i] 85 | len := loca.offsetsLong[i+1] - loca.offsetsLong[i] 86 | if offset < 0 { 87 | logrus.Debug("Invalid offset") 88 | return nil, errors.New("invalid indexToLoca offset") 89 | } 90 | if len < 0 { 91 | logrus.Debug("Invalid length") 92 | return nil, errors.New("invalid indexToLoca len") 93 | } 94 | 95 | } 96 | 97 | return loca, nil 98 | } 99 | 100 | func (f *font) writeLoca(w *byteWriter) error { 101 | if f.loca == nil || f.head == nil || f.maxp == nil { 102 | return errRequiredField 103 | } 104 | numGlyphs := int(f.maxp.numGlyphs) 105 | isShort := f.head.indexToLocFormat == 0 106 | 107 | t := f.loca 108 | if isShort { 109 | if numGlyphs+1 != len(t.offsetsShort) { 110 | logrus.Debug("Unexpected length") 111 | } 112 | return w.writeSlice(t.offsetsShort) 113 | } 114 | return w.writeSlice(t.offsetsLong) 115 | } 116 | -------------------------------------------------------------------------------- /table_maxp.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // maxpTable represents the Maximum Profile (maxp) table. 13 | // This table establishes the memory requirements for the font. 14 | type maxpTable struct { 15 | // Version 0.5 and above: 16 | version fixed 17 | numGlyphs uint16 18 | 19 | // Version 1.0 and above: 20 | maxPoints uint16 21 | maxContours uint16 22 | maxCompositePoints uint16 23 | maxCompositeContours uint16 24 | maxZones uint16 25 | maxTwilightPoints uint16 26 | maxStorage uint16 27 | maxFunctionDefs uint16 28 | maxInstructionDefs uint16 29 | maxStackElements uint16 30 | maxSizeOfInstructions uint16 31 | maxComponentElements uint16 32 | maxComponentDepth uint16 33 | } 34 | 35 | func (f *font) parseMaxp(r *byteReader) (*maxpTable, error) { 36 | _, has, err := f.seekToTable(r, "maxp") 37 | if err != nil { 38 | return nil, err 39 | } 40 | if !has { 41 | logrus.Debug("maxp table not present") 42 | return nil, nil 43 | } 44 | 45 | t := &maxpTable{} 46 | 47 | err = r.read(&t.version, &t.numGlyphs) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | if t.version < 0x00010000 { 53 | logrus.Debug("Range check error") 54 | return nil, errRangeCheck 55 | } 56 | 57 | err = r.read(&t.maxPoints, &t.maxContours, &t.maxCompositePoints, &t.maxCompositeContours) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | err = r.read(&t.maxZones, &t.maxTwilightPoints, &t.maxStorage, &t.maxFunctionDefs, &t.maxInstructionDefs) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return t, r.read(&t.maxStackElements, &t.maxSizeOfInstructions, &t.maxComponentElements, &t.maxComponentDepth) 68 | } 69 | 70 | func (f *font) writeMaxp(w *byteWriter) error { 71 | if f.maxp == nil { 72 | return errRequiredField 73 | } 74 | t := f.maxp 75 | err := w.write(t.version, t.numGlyphs) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | if t.version < 0x00010000 { 81 | logrus.Debug("Range check error") 82 | return errRangeCheck 83 | } 84 | 85 | err = w.write(t.maxPoints, t.maxContours, t.maxCompositePoints, t.maxCompositeContours) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | err = w.write(t.maxZones, t.maxTwilightPoints, t.maxStorage, t.maxFunctionDefs, t.maxInstructionDefs) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | return w.write(t.maxStackElements, t.maxSizeOfInstructions, t.maxComponentElements, t.maxComponentDepth) 96 | } 97 | -------------------------------------------------------------------------------- /table_maxp_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "os" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestMaxpTable(t *testing.T) { 17 | // Run only this function in debugmode. 18 | /* 19 | common.SetLogger(common.NewConsoleLogger(common.LogLevelDebug)) 20 | defer func() { 21 | common.SetLogger(common.NewConsoleLogger(common.LogLevelInfo)) 22 | }() 23 | */ 24 | 25 | testcases := []struct { 26 | fontPath string 27 | numGlyphs int 28 | }{ 29 | { 30 | "./testdata/FreeSans.ttf", 31 | 3726, 32 | }, 33 | { 34 | "./testdata/wts11.ttf", 35 | 14148, 36 | }, 37 | { 38 | "./testdata/roboto/Roboto-BoldItalic.ttf", 39 | 1294, 40 | }, 41 | } 42 | 43 | for _, tcase := range testcases { 44 | t.Run(tcase.fontPath, func(t *testing.T) { 45 | t.Logf("%s", tcase.fontPath) 46 | f, err := os.Open(tcase.fontPath) 47 | assert.Equal(t, nil, err) 48 | defer f.Close() 49 | 50 | br := newByteReader(f) 51 | fnt, err := parseFont(br) 52 | assert.Equal(t, nil, err) 53 | require.NoError(t, err) 54 | 55 | require.NotNil(t, fnt) 56 | require.NotNil(t, fnt.maxp) 57 | require.Equal(t, int(tcase.numGlyphs), int(fnt.maxp.numGlyphs)) 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /table_name.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "bytes" 10 | "strconv" 11 | "unicode" 12 | "unicode/utf8" 13 | 14 | "github.com/sirupsen/logrus" 15 | 16 | "golang.org/x/text/encoding/charmap" 17 | 18 | "github.com/unidoc/unitype/internal/strutils" 19 | ) 20 | 21 | // nameTable represents the Naming table (name). 22 | // The naming table allows multilingual strings to be associated with the font. 23 | // These strings can represent copyright notices, font names, family names, style names, and so on. 24 | type nameTable struct { 25 | // format >= 0 26 | format uint16 27 | count uint16 28 | stringOffset offset16 29 | nameRecords []*nameRecord // len = count. 30 | 31 | // format = 1 adds 32 | langTagCount uint16 33 | langTagRecords []*langTagRecord // len = langTagCount 34 | } 35 | 36 | type langTagRecord struct { 37 | length uint16 38 | offset offset16 39 | data []byte // actual string data (UTF-16BE format). 40 | } 41 | 42 | // Each string in the string storage is referenced by a name record. 43 | type nameRecord struct { 44 | platformID uint16 45 | encodingID uint16 46 | languageID uint16 47 | nameID uint16 48 | length uint16 49 | offset offset16 50 | data []byte // actual string data. 51 | } 52 | 53 | // GetNameByID returns the first entry according to the name table with `nameID`. 54 | // An empty string is returned otherwise (nothing found). 55 | func (f *font) GetNameByID(nameID int) string { 56 | if f == nil || f.name == nil { 57 | logrus.Debug("ERROR: Font or name not set") 58 | return "" 59 | } 60 | for _, nr := range f.name.nameRecords { 61 | if int(nr.nameID) == nameID { 62 | return nr.Decoded() 63 | } 64 | } 65 | return "" 66 | } 67 | 68 | // GetNameRecords returns name records as map of language ID 69 | // that contais name ID and it's value. 70 | func (f *font) GetNameRecords() map[uint16]map[uint16]string { 71 | var nameRecords = make(map[uint16]map[uint16]string, 0) 72 | for _, nr := range f.name.nameRecords { 73 | nameRec, ok := nameRecords[nr.languageID] 74 | if ok { 75 | nameRec[nr.nameID] = nr.Decoded() 76 | } else { 77 | nameRec = map[uint16]string{nr.nameID: nr.Decoded()} 78 | } 79 | nameRecords[nr.languageID] = nameRec 80 | } 81 | return nameRecords 82 | } 83 | 84 | // numPrintables returns the number of printable runes in `str` 85 | func numPrintables(str string) int { 86 | printables := 0 87 | for _, r := range str { 88 | if unicode.IsPrint(r) || r == '\n' { 89 | printables++ 90 | } 91 | } 92 | return printables 93 | } 94 | 95 | // makePrintable replaces unprintable runes with quotes runes, returning printable string. 96 | func makePrintable(str string) string { 97 | var buf bytes.Buffer 98 | for _, r := range str { 99 | if unicode.IsPrint(r) || r == '\n' { 100 | buf.WriteRune(r) 101 | } else { 102 | buf.WriteString(strconv.QuoteRune(r)) 103 | } 104 | } 105 | return buf.String() 106 | } 107 | 108 | // Decoded attempts to decode the underlying data and convert to a string. 109 | // NOTE: Works in many cases but often has some -garbage- around texts. 110 | func (nr nameRecord) Decoded() string { 111 | switch nr.platformID { 112 | case 0: // unicode 113 | // TODO(gunnsth): Untested as have not encountered this yet. 114 | dup := make([]byte, len(nr.data)) 115 | copy(dup, nr.data) 116 | var decoded bytes.Buffer 117 | 118 | for len(dup) > 0 { 119 | r, size := utf8.DecodeRune(dup) 120 | dup = dup[size:] 121 | decoded.WriteRune(r) 122 | } 123 | 124 | return makePrintable(decoded.String()) 125 | case 1: // macintosh 126 | var decoded bytes.Buffer 127 | for _, val := range nr.data { 128 | decoded.WriteRune(charmap.Macintosh.DecodeByte(val)) 129 | } 130 | macs := decoded.String() 131 | 132 | // Following may be needed in rare cases: 133 | /* 134 | utf16s := strutils.UTF16ToString([]byte(macs)) 135 | if numPrintables(utf16s) > numPrintables(macs) { 136 | return makePrintable(utf16s) 137 | } 138 | */ 139 | return makePrintable(macs) 140 | 141 | case 3: // windows 142 | // When building a Unicode font for Windows, the platform ID should be 3 and the encoding ID should be 1, 143 | // and the referenced string data must be encoded in UTF-16BE. When building a symbol font for Windows, 144 | // the platform ID should be 3 and the encoding ID should be 0, and the referenced string data must be 145 | // encoded in UTF-16BE. (https://docs.microsoft.com/en-us/typography/opentype/spec/name). 146 | if nr.encodingID == 0 || nr.encodingID == 1 { 147 | if len(nr.data) > 0 { 148 | decoded := strutils.UTF16ToString(nr.data) 149 | return makePrintable(decoded) 150 | } 151 | } 152 | } 153 | 154 | return makePrintable(string(nr.data)) 155 | } 156 | 157 | func (f *font) parseNameTable(r *byteReader) (*nameTable, error) { 158 | tr, has, err := f.seekToTable(r, "name") 159 | if err != nil { 160 | return nil, err 161 | } 162 | if !has { 163 | return nil, nil 164 | } 165 | logrus.Debugf("TR: %+v", tr) 166 | 167 | t := &nameTable{} 168 | err = r.read(&t.format, &t.count, &t.stringOffset) 169 | if err != nil { 170 | return nil, err 171 | } 172 | logrus.Debugf("format/count/stringOffset: %v/%v/%v", t.format, t.count, t.stringOffset) 173 | logrus.Debugf("-- name string offset: %d", t.stringOffset) 174 | 175 | if t.format > 1 { 176 | logrus.Debugf("ERROR: format > 1 (%d)", t.format) 177 | return nil, errRangeCheck 178 | } 179 | 180 | for i := 0; i < int(t.count); i++ { 181 | var nr nameRecord 182 | err = r.read(&nr.platformID, &nr.encodingID, &nr.languageID, &nr.nameID, &nr.length, &nr.offset) 183 | if err != nil { 184 | return nil, err 185 | } 186 | logrus.Debugf("name record %d: %v/%v/%v/%v/%v/%v", i, nr.platformID, nr.encodingID, nr.languageID, nr.nameID, 187 | nr.length, nr.offset) 188 | t.nameRecords = append(t.nameRecords, &nr) 189 | } 190 | 191 | if t.format == 1 { 192 | err = r.read(&t.langTagCount) 193 | if err != nil { 194 | return nil, err 195 | } 196 | for i := 0; i < int(t.langTagCount); i++ { 197 | var ltr langTagRecord 198 | err = r.read(<r.length, <r.offset) 199 | if err != nil { 200 | return nil, err 201 | } 202 | logrus.Debugf("ltr name record %d: %v/%v", i, ltr.offset, ltr.length) 203 | t.langTagRecords = append(t.langTagRecords, <r) 204 | } 205 | } 206 | 207 | // Get the actual string data. 208 | for _, nr := range t.nameRecords { 209 | if int(t.stringOffset)+int(nr.offset)+int(nr.length) > int(tr.length) { 210 | logrus.Debugf("%v> %v", int(t.stringOffset)+int(nr.offset)+int(nr.length), int(tr.length)) 211 | logrus.Debug("name string offset outside table") 212 | return nil, errRangeCheck 213 | } 214 | 215 | err = r.SeekTo(int64(t.stringOffset) + int64(tr.offset) + int64(nr.offset)) 216 | if err != nil { 217 | logrus.Debugf("Error: %v", err) 218 | return nil, err 219 | } 220 | 221 | err = r.readBytes(&nr.data, int(nr.length)) 222 | if err != nil { 223 | logrus.Debugf("Error: %v", err) 224 | return nil, err 225 | } 226 | } 227 | 228 | for _, ltr := range t.langTagRecords { 229 | if int(t.stringOffset)+int(ltr.offset)+int(ltr.length) > int(tr.length) { 230 | logrus.Debug("lang tag string offset outside table") 231 | return nil, errRangeCheck 232 | } 233 | 234 | err = r.SeekTo(int64(t.stringOffset) + int64(tr.offset) + int64(ltr.offset)) 235 | if err != nil { 236 | logrus.Debugf("Error: %v", err) 237 | return nil, err 238 | } 239 | err = r.readBytes(<r.data, int(ltr.length)) 240 | if err != nil { 241 | logrus.Debugf("Error: %v", err) 242 | return nil, err 243 | } 244 | } 245 | 246 | logrus.Debugf("Name records: %d", len(t.nameRecords)) 247 | for _, nr := range t.nameRecords { 248 | logrus.Debugf("%d %d %d - '%s' (%d)", nr.platformID, nr.encodingID, nr.nameID, nr.Decoded(), len(nr.data)) 249 | } 250 | 251 | return t, nil 252 | } 253 | 254 | func (f *font) writeNameTable(w *byteWriter) error { 255 | if f.name == nil { 256 | logrus.Debug("name is nil") 257 | return nil 258 | } 259 | t := f.name 260 | 261 | // Preprocess: Write to buffer and update offsets. 262 | var buf bytes.Buffer 263 | { 264 | bufw := newByteWriter(&buf) 265 | for _, nr := range t.nameRecords { 266 | nr.offset = offset16(bufw.bufferedLen()) 267 | nr.length = uint16(len(nr.data)) 268 | err := bufw.writeSlice(nr.data) 269 | if err != nil { 270 | return err 271 | } 272 | } 273 | for _, ltr := range t.langTagRecords { 274 | ltr.offset = offset16(bufw.bufferedLen()) 275 | ltr.length = uint16(len(ltr.data)) 276 | err := bufw.writeSlice(ltr.data) 277 | if err != nil { 278 | return err 279 | } 280 | } 281 | err := bufw.flush() 282 | if err != nil { 283 | return err 284 | } 285 | } 286 | logrus.Debugf("Buffer length: %d", buf.Len()) 287 | 288 | // Update count and stringOffsets (calculated). 289 | t.count = uint16(len(t.nameRecords)) 290 | t.langTagCount = uint16(len(t.langTagRecords)) 291 | 292 | // 2+2+2+count*(6*2) + (format=1) 2+langTagCount*2 293 | t.stringOffset = 6 + offset16(t.count)*12 294 | if t.format == 1 { 295 | t.stringOffset += 2 + offset16(t.langTagCount)*4 296 | } 297 | 298 | logrus.Debugf("w @ %d", w.bufferedLen()) 299 | err := w.write(t.format, t.count, t.stringOffset) 300 | if err != nil { 301 | return err 302 | } 303 | 304 | for _, nr := range t.nameRecords { 305 | err = w.write(nr.platformID, nr.encodingID, nr.languageID, nr.nameID, nr.length, nr.offset) 306 | if err != nil { 307 | return err 308 | } 309 | } 310 | logrus.Debugf("w @ %d", w.bufferedLen()) 311 | 312 | if t.format == 1 { 313 | err = w.write(t.langTagCount) 314 | if err != nil { 315 | return err 316 | } 317 | for _, ltr := range t.langTagRecords { 318 | err = w.write(ltr.length, ltr.offset) 319 | if err != nil { 320 | return err 321 | } 322 | } 323 | } 324 | 325 | logrus.Debugf("w @ %d", w.bufferedLen()) 326 | // Write the buffered data. 327 | err = w.writeBytes(buf.Bytes()) 328 | if err != nil { 329 | return err 330 | } 331 | logrus.Debugf("w @ %d", w.bufferedLen()) 332 | 333 | return nil 334 | } 335 | -------------------------------------------------------------------------------- /table_name_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "os" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestNameTable(t *testing.T) { 17 | testcases := []struct { 18 | fontPath string 19 | numEntries int 20 | expected map[int]string 21 | }{ 22 | { 23 | "./testdata/FreeSans.ttf", 24 | 24, 25 | map[int]string{ 26 | 0: "Copyleft 2002, 2003, 2005 Free Software Foundation.", 27 | 1: "FreeSans", 28 | 2: "Medium", 29 | 4: "Free Sans", 30 | 13: "The use of this font is granted subject to GNU General Public License.", 31 | 19: "The quick brown fox jumps over the lazy dog.", 32 | }, 33 | }, 34 | { 35 | "./testdata/wts11.ttf", 36 | 44, 37 | map[int]string{ 38 | 0: "(C)Copyright Dr. Hann-Tzong Wang, 2002-2004.", 39 | 1: "HanWang KaiBold-Gb5", 40 | 2: "Regular", 41 | 3: "HanWang KaiBold-Gb5", 42 | 4: "HanWang KaiBold-Gb5", 43 | 6: "HanWang KaiBold-Gb5", 44 | 7: "HanWang KaiBold-Gb5 is a registered trademark of HtWang Graphics Laboratory", 45 | 14: "http://www.gnu.org/licenses/gpl.txt", 46 | }, 47 | }, 48 | { 49 | "./testdata/roboto/Roboto-BoldItalic.ttf", 50 | 26, 51 | map[int]string{ 52 | 0: "Copyright 2011 Google Inc. All Rights Reserved.", 53 | 1: "Roboto", 54 | 2: "Bold Italic", 55 | 3: "Roboto Bold Italic", 56 | 4: "Roboto Bold Italic", 57 | 5: "Version 2.137; 2017", 58 | 6: "Roboto-BoldItalic", 59 | 14: "http://www.apache.org/licenses/LICENSE-2.0", 60 | }, 61 | }, 62 | } 63 | 64 | for _, tcase := range testcases { 65 | t.Run(tcase.fontPath, func(t *testing.T) { 66 | f, err := os.Open(tcase.fontPath) 67 | assert.Equal(t, nil, err) 68 | defer f.Close() 69 | 70 | br := newByteReader(f) 71 | fnt, err := parseFont(br) 72 | assert.Equal(t, nil, err) 73 | require.NoError(t, err) 74 | 75 | require.NotNil(t, fnt) 76 | require.NotNil(t, fnt.name) 77 | require.NotNil(t, fnt.name.nameRecords) 78 | 79 | assert.Equal(t, tcase.numEntries, len(fnt.name.nameRecords)) 80 | for nameID, expStr := range tcase.expected { 81 | assert.Equal(t, expStr, fnt.GetNameByID(nameID)) 82 | } 83 | 84 | for _, nr := range fnt.name.nameRecords { 85 | t.Logf("%d/%d/%d - '%s'", nr.platformID, nr.encodingID, nr.nameID, nr.Decoded()) 86 | } 87 | }) 88 | } 89 | } 90 | 91 | func TestGetNameRecords(t *testing.T) { 92 | testcases := []struct { 93 | fontPath string 94 | numNames int 95 | expected map[uint16]map[uint16]string 96 | }{ 97 | { 98 | "./testdata/FreeSans.ttf", 99 | 3, 100 | map[uint16]map[uint16]string{ 101 | 0: map[uint16]string{ 102 | 0: "Copyleft 2002, 2003, 2005 Free Software Foundation.", 103 | 1: "FreeSans", 104 | 2: "Medium", 105 | 3: "FontForge 2.0 : Free Sans : 18-5-2007", 106 | 4: "Free Sans", 107 | 5: "Version $Revision: 1.79 $ ", 108 | 6: "FreeSans", 109 | 13: "The use of this font is granted subject to GNU General Public License.", 110 | 14: "http://www.gnu.org/copyleft/gpl.html", 111 | 19: "The quick brown fox jumps over the lazy dog.", 112 | }, 113 | 1033: map[uint16]string{ 114 | 0: "Copyleft 2002, 2003, 2005 Free Software Foundation.", 115 | 1: "FreeSans", 116 | 2: "Medium", 117 | 3: "FontForge 2.0 : Free Sans : 18-5-2007", 118 | 4: "Free Sans", 119 | 5: "Version $Revision: 1.79 $ ", 120 | 6: "FreeSans", 121 | 13: "The use of this font is granted subject to GNU General Public License.", 122 | 14: "http://www.gnu.org/copyleft/gpl.html", 123 | 19: "The quick brown fox jumps over the lazy dog.", 124 | }, 125 | 1060: map[uint16]string{ 126 | 2: "navadno", 127 | 13: "Dovoljena je uporaba v skladu z licenco GNU General Public License.", 128 | 14: "http://www.gnu.org/copyleft/gpl.html", 129 | 19: "Šerif bo za vajo spet kuhal domače žgance.", 130 | }, 131 | }, 132 | }, 133 | { 134 | "./testdata/wts11.ttf", 135 | 4, 136 | map[uint16]map[uint16]string{ 137 | 0: map[uint16]string{ 138 | 0: "(C)Copyright Dr. Hann-Tzong Wang, 2002-2004.", 139 | 1: "HanWang KaiBold-Gb5", 140 | 2: "Regular", 141 | 3: "HanWang KaiBold-Gb5", 142 | 4: "HanWang KaiBold-Gb5", 143 | 5: "Version 1.3(license under GNU GPL)", 144 | 6: "HanWang KaiBold-Gb5", 145 | 7: "HanWang KaiBold-Gb5 is a registered trademark of HtWang Graphics Laboratory", 146 | 10: "HtWang Fonts(1), March 8, 2002; 1.00, initial release; HtWang Fonts(17), March 5, 2004; GJL(040519). Maintain by CLE Project.", 147 | 13: "(C)Copyright Dr. Hann-Tzong Wang, 2002-2004.\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or any later version.", 148 | 14: "http://www.gnu.org/licenses/gpl.txt", 149 | }, 150 | 1028: map[uint16]string{ 151 | 0: "(C)Copyright Dr. Hann-Tzong Wang, 2002-2004.", 152 | 1: "王漢宗粗楷體簡", 153 | 2: "Regular", 154 | 3: "王漢宗粗楷體簡", 155 | 4: "王漢宗粗楷體簡", 156 | 5: "Version 1.3(license under GNU GPL)", 157 | 6: "王漢宗粗楷體簡", 158 | 7: "王漢宗粗楷體簡 is a registered trademark of HtWang Graphics Laboratory", 159 | 10: "HtWang Fonts(1), March 8, 2002; 1.00, initial release; HtWang Fonts(17), March 5, 2004; GJL(040519). Maintain by CLE Project.", 160 | 13: "(C)Copyright Dr. Hann-Tzong Wang, 2002-2004.\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or any later version.", 161 | 14: "http://www.gnu.org/licenses/gpl.txt", 162 | }, 163 | 1033: map[uint16]string{ 164 | 0: "(C)Copyright Dr. Hann-Tzong Wang, 2002-2004.", 165 | 1: "HanWang KaiBold-Gb5", 166 | 2: "Regular", 167 | 3: "HanWang KaiBold-Gb5", 168 | 4: "HanWang KaiBold-Gb5", 169 | 5: "Version 1.3(license under GNU GPL)", 170 | 6: "HanWang KaiBold-Gb5", 171 | 7: "HanWang KaiBold-Gb5 is a registered trademark of HtWang Graphics Laboratory", 172 | 10: "HtWang Fonts(1), March 8, 2002; 1.00, initial release; HtWang Fonts(17), March 5, 2004; GJL(040519). Maintain by CLE Project.", 173 | 13: "(C)Copyright Dr. Hann-Tzong Wang, 2002-2004.\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or any later version.", 174 | 14: "http://www.gnu.org/licenses/gpl.txt", 175 | }, 176 | 2052: map[uint16]string{ 177 | 0: "(C)Copyright Dr. Hann-Tzong Wang, 2002-2004.", 178 | 1: "王汉宗粗楷体简", 179 | 2: "Regular", 180 | 3: "王汉宗粗楷体简", 181 | 4: "王汉宗粗楷体简", 182 | 5: "Version 1.3(license under GNU GPL)", 183 | 6: "王汉宗粗楷体简", 184 | 7: "王汉宗粗楷体简 is a registered trademark of HtWang Graphics Laboratory", 185 | 10: "HtWang Fonts(1), March 8, 2002; 1.00, initial release; HtWang Fonts(17), March 5, 2004; GJL(040519). Maintain by CLE Project.", 186 | 13: "(C)Copyright Dr. Hann-Tzong Wang, 2002-2004.\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or any later version.", 187 | 14: "http://www.gnu.org/licenses/gpl.txt", 188 | }, 189 | }, 190 | }, 191 | } 192 | 193 | for _, tc := range testcases { 194 | f, err := os.Open(tc.fontPath) 195 | require.NoError(t, err) 196 | defer f.Close() 197 | 198 | br := newByteReader(f) 199 | fnt, err := parseFont(br) 200 | require.NoError(t, err) 201 | 202 | // Get name records. 203 | nameRecords := fnt.GetNameRecords() 204 | 205 | assert.Equal(t, tc.numNames, len(nameRecords)) 206 | assert.Equal(t, tc.expected, nameRecords) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /table_offset.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | type offsetTable struct { 9 | sfntVersion uint32 10 | numTables uint16 11 | searchRange uint16 12 | entrySelector uint16 13 | rangeShift uint16 14 | } 15 | 16 | // Size returns size of `t` in bytes. 17 | func (t *offsetTable) Size() int64 { 18 | return 4 + 4*2 // 4+8=12 19 | } 20 | 21 | func (f *font) parseOffsetTable(r *byteReader) (*offsetTable, error) { 22 | ot := &offsetTable{} 23 | 24 | err := r.read(&ot.sfntVersion, &ot.numTables, &ot.searchRange) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | err = r.read(&ot.entrySelector, &ot.rangeShift) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return ot, nil 35 | } 36 | 37 | func (f *font) writeOffsetTable(w *byteWriter) error { 38 | if f.ot == nil { 39 | return errRequiredField 40 | } 41 | return w.write(f.ot.sfntVersion, f.ot.numTables, f.ot.searchRange, f.ot.entrySelector, f.ot.rangeShift) 42 | } 43 | -------------------------------------------------------------------------------- /table_offset_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "bytes" 10 | "testing" 11 | 12 | "github.com/sirupsen/logrus" 13 | 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | // Test unmarshalling and marshalling offset table. 19 | func TestOffsetTableReadWrite(t *testing.T) { 20 | testcases := []struct { 21 | fontPath string 22 | // Expected offset table parameters. 23 | expected offsetTable 24 | }{ 25 | { 26 | "./testdata/FreeSans.ttf", 27 | offsetTable{ 28 | sfntVersion: 0x10000, // opentype 29 | numTables: 16, 30 | searchRange: 256, 31 | entrySelector: 4, 32 | rangeShift: 0, 33 | }, 34 | }, 35 | { 36 | "./testdata/wts11.ttf", 37 | offsetTable{ 38 | sfntVersion: 0x10000, // opentype 39 | numTables: 15, 40 | searchRange: 128, 41 | entrySelector: 3, 42 | rangeShift: 112, 43 | }, 44 | }, 45 | { 46 | "./testdata/roboto/Roboto-BoldItalic.ttf", 47 | offsetTable{ 48 | sfntVersion: 0x10000, // opentype 49 | numTables: 18, 50 | searchRange: 256, 51 | entrySelector: 4, 52 | rangeShift: 32, 53 | }, 54 | }, 55 | } 56 | 57 | for _, tcase := range testcases { 58 | t.Logf("%s", tcase.fontPath) 59 | fnt, err := ParseFile(tcase.fontPath) 60 | require.NoError(t, err) 61 | assert.Equal(t, tcase.expected, *fnt.ot) 62 | 63 | logrus.Debug("Write offset table") 64 | // Marshall to buffer. 65 | var buf bytes.Buffer 66 | bw := newByteWriter(&buf) 67 | err = fnt.writeOffsetTable(bw) 68 | require.NoError(t, err) 69 | bw.flush() 70 | 71 | // Reload from buffer. 72 | br := newByteReader(bytes.NewReader(buf.Bytes())) 73 | ot, err := fnt.parseOffsetTable(br) 74 | require.NoError(t, err) 75 | assert.Equal(t, fnt.ot, ot) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /table_os2.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // os2Table represents the OS/2 metrics table. It consists of metrics and other data that are required. 13 | type os2Table struct { 14 | // Version 0+ 15 | version uint16 16 | xAvgCharWidth int16 17 | usWeightClass uint16 18 | usWidthClass uint16 19 | fsType uint16 20 | ySubscriptXSize int16 21 | ySubscriptYSize int16 22 | ySubscriptXOffset int16 23 | ySubscriptYOffset int16 24 | ySuperscriptXSize int16 25 | ySuperscriptYSize int16 26 | ySuperscriptXOffset int16 27 | ySuperscriptYOffset int16 28 | yStrikeoutSize int16 29 | yStrikeoutPosition int16 30 | sFamilyClass int16 31 | panose10 []uint8 // panose10 len = 10 32 | ulUnicodeRange1 uint32 // Bits 0-31. 33 | ulUnicodeRange2 uint32 // Bits 32-63. 34 | ulUnicodeRange3 uint32 // Bits 64-95. 35 | ulUnicodeRange4 uint32 // Bits 96-127. 36 | achVendID tag 37 | fsSelection uint16 38 | usFirstCharIndex uint16 39 | usLastCharIndex uint16 40 | sTypoAscender int16 41 | sTypoDescender int16 42 | sTypoLineGap int16 43 | usWinAscent uint16 44 | usWinDescent uint16 45 | 46 | // Version 1-5. 47 | ulCodePageRange1 uint32 // Bits 0-31 48 | ulCodePageRange2 uint32 // Bits 32-63. 49 | 50 | // Version 2-5 51 | sxHeight int16 52 | sCapHeight int16 53 | usDefaultChar uint16 54 | usBreakChar uint16 55 | usMaxContext uint16 56 | 57 | // Version 5 58 | usLowerOpticalPointSize uint16 59 | usUpperOpticalPointSize uint16 60 | } 61 | 62 | func (f *font) parseOS2Table(r *byteReader) (*os2Table, error) { 63 | _, has, err := f.seekToTable(r, "OS/2") 64 | if err != nil { 65 | return nil, err 66 | } 67 | if !has { 68 | logrus.Debug("OS/2 table not present") 69 | return nil, nil 70 | } 71 | 72 | t := &os2Table{} 73 | err = r.read(&t.version, &t.xAvgCharWidth, &t.usWeightClass, &t.usWidthClass, &t.fsType) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | if t.version > 10 { 79 | logrus.Debug("OS/2 table version range error") 80 | return nil, errRangeCheck 81 | } 82 | 83 | err = r.read(&t.ySubscriptXSize, &t.ySubscriptYSize, &t.ySubscriptXOffset, &t.ySubscriptYOffset) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | err = r.read(&t.ySuperscriptXSize, &t.ySuperscriptYSize, &t.ySuperscriptXOffset, &t.ySuperscriptYOffset) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | err = r.read(&t.yStrikeoutSize, &t.yStrikeoutPosition, &t.sFamilyClass) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | err = r.readSlice(&t.panose10, 10) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | err = r.read(&t.ulUnicodeRange1, &t.ulUnicodeRange2, &t.ulUnicodeRange3, &t.ulUnicodeRange4) 104 | if err != nil { 105 | return nil, err 106 | } 107 | err = r.read(&t.achVendID, &t.fsSelection, &t.usFirstCharIndex, &t.usLastCharIndex, &t.sTypoAscender) 108 | if err != nil { 109 | return nil, err 110 | } 111 | err = r.read(&t.sTypoDescender, &t.sTypoLineGap, &t.usWinAscent, &t.usWinDescent) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | if t.version == 0 { 117 | return t, nil 118 | } 119 | 120 | // version >= 1. 121 | err = r.read(&t.ulCodePageRange1, &t.ulCodePageRange2) 122 | if err != nil { 123 | return nil, err 124 | } 125 | if t.version == 1 { 126 | return t, nil 127 | } 128 | 129 | // version 2-5. 130 | err = r.read(&t.sxHeight, &t.sCapHeight, &t.usDefaultChar, &t.usBreakChar, &t.usMaxContext) 131 | if err != nil { 132 | return nil, err 133 | } 134 | if t.version < 5 { 135 | return t, nil 136 | } 137 | 138 | // version >= 5. 139 | err = r.read(&t.usLowerOpticalPointSize, &t.usUpperOpticalPointSize) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | return t, nil 145 | } 146 | 147 | func (f *font) writeOS2(w *byteWriter) error { 148 | if f.os2 == nil { 149 | return nil 150 | } 151 | t := f.os2 152 | 153 | err := w.write(t.version, t.xAvgCharWidth, t.usWeightClass, t.usWidthClass, t.fsType) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | err = w.write(t.ySubscriptXSize, t.ySubscriptYSize, t.ySubscriptXOffset, t.ySubscriptYOffset) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | err = w.write(t.ySuperscriptXSize, t.ySuperscriptYSize, t.ySuperscriptXOffset, t.ySuperscriptYOffset) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | err = w.write(t.yStrikeoutSize, t.yStrikeoutPosition, t.sFamilyClass) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | err = w.writeSlice(t.panose10) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | err = w.write(t.ulUnicodeRange1, t.ulUnicodeRange2, t.ulUnicodeRange3, t.ulUnicodeRange4) 179 | if err != nil { 180 | return err 181 | } 182 | err = w.write(t.achVendID, t.fsSelection, t.usFirstCharIndex, t.usLastCharIndex, t.sTypoAscender) 183 | if err != nil { 184 | return err 185 | } 186 | err = w.write(t.sTypoDescender, t.sTypoLineGap, t.usWinAscent, t.usWinDescent) 187 | if err != nil { 188 | return err 189 | } 190 | 191 | if t.version == 0 { 192 | return nil 193 | } 194 | 195 | // version >= 1. 196 | err = w.write(t.ulCodePageRange1, t.ulCodePageRange2) 197 | if err != nil { 198 | return err 199 | } 200 | if t.version == 1 { 201 | return nil 202 | } 203 | 204 | // version 2-5. 205 | err = w.write(t.sxHeight, t.sCapHeight, t.usDefaultChar, t.usBreakChar, t.usMaxContext) 206 | if err != nil { 207 | return err 208 | } 209 | if t.version < 5 { 210 | return nil 211 | } 212 | 213 | // version >= 5. 214 | return w.write(t.usLowerOpticalPointSize, t.usUpperOpticalPointSize) 215 | } 216 | -------------------------------------------------------------------------------- /table_post.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "errors" 10 | 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // postTable represents a PostScript (post) table. 15 | // This table contains additional information needed for use on PostScript printers. 16 | // Includes FontInfo dictionary entries and the PostScript names of all glyphs. 17 | // 18 | // - version 1.0 is used the font file contains exactly the 258 glyphs in the standard Macintosh TrueType font file. 19 | // Glyph list on: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html 20 | // - version 2.0 is used for fonts that contain some glyphs not in the standard set or have different ordering. 21 | // - version 2.5 can handle nonstandard ordering of the standard mac glyphs via offsets. 22 | // - other versions do not contain post glyph name data. 23 | // 24 | type postTable struct { 25 | // header (all versions). 26 | version fixed 27 | italicAngle fixed // in degrees. 28 | underlinePosition fword 29 | underlineThickness fword 30 | isFixedPitch uint32 31 | minMemType42 uint32 32 | maxMemType42 uint32 33 | minMemType1 uint32 34 | maxMemType1 uint32 35 | 36 | // version 2.0 and 2.5 (partly). 37 | numGlyphs uint16 // should equal maxp.numGlyphs 38 | glyphNameIndex []uint16 // len = numGlyphs 39 | 40 | // version 2.5. 41 | offsets []int8 // len = numGlyphs 42 | 43 | // Processed data: 44 | // TODO: Check `len = glyphNames` below, should be numGlyphs ? 45 | glyphNames []GlyphName // len = glyphNames, index is GlyphID (GID), glyphNames[GlyphID] -> GlyphName. 46 | } 47 | 48 | /* 49 | See https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html 50 | and https://docs.microsoft.com/en-us/typography/opentype/spec/post 51 | for details regarding the format. 52 | */ 53 | 54 | func (f *font) parsePost(r *byteReader) (*postTable, error) { 55 | logrus.Debug("Parsing post table") 56 | if f.maxp == nil { 57 | // maxp table required for numGlyphs check. Could probably be omitted, can consider 58 | // if run into those cases where post is present and maxp is not (and all other information present). 59 | logrus.Debug("Required maxp table missing") 60 | return nil, errRequiredField 61 | } 62 | 63 | tr, has, err := f.seekToTable(r, "post") 64 | if err != nil { 65 | return nil, err 66 | } 67 | if !has { 68 | logrus.Debug("Post table not present") 69 | return nil, nil 70 | } 71 | 72 | start := r.Offset() 73 | 74 | t := &postTable{} 75 | err = r.read(&t.version, &t.italicAngle, &t.underlinePosition, &t.underlineThickness, &t.isFixedPitch) 76 | if err != nil { 77 | return nil, err 78 | } 79 | err = r.read(&t.minMemType42, &t.maxMemType42, &t.minMemType1, &t.maxMemType1) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | logrus.Debugf("Version: %v %v 0x%X", t.version, t.version.Float64(), t.version) 85 | switch uint32(t.version) { 86 | case 0x00010000: // 1.0 - font files contains exactly the 258 standard Macintosh glyphs. 87 | if t.numGlyphs != 258 { 88 | logrus.Debug("Should have the mac number of glyph names") 89 | // TODO(gunnsth): If this is too strict, can just set the first 258 glyphnames. 90 | return nil, errRangeCheck 91 | } 92 | t.glyphNames = make([]GlyphName, int(t.numGlyphs)) 93 | for i := range macGlyphNames { 94 | t.glyphNames[i] = macGlyphNames[i] 95 | } 96 | 97 | case 0x00020000: // 2.0 98 | logrus.Trace("Version: 2.0") 99 | err = r.read(&t.numGlyphs) 100 | if err != nil { 101 | return nil, err 102 | } 103 | logrus.Debugf("numGlyphs: %d", t.numGlyphs) 104 | if t.numGlyphs != f.maxp.numGlyphs { 105 | logrus.Debugf("post numGlyphs != maxp.numGlyphs (%d != %d)", t.numGlyphs, f.maxp.numGlyphs) 106 | return nil, errRangeCheck 107 | } 108 | err = r.readSlice(&t.glyphNameIndex, int(t.numGlyphs)) 109 | if err != nil { 110 | return nil, err 111 | } 112 | newGlyphs := 0 113 | for _, ni := range t.glyphNameIndex { 114 | if ni >= 258 && ni <= 32767 { 115 | newGlyphs++ 116 | } 117 | } 118 | logrus.Tracef("newGlyphs: %d", newGlyphs) 119 | var names []string 120 | for i := 0; i < newGlyphs; i++ { 121 | if r.Offset()-start >= int64(tr.length) { 122 | logrus.Debug("ERROR: Reading outside post table") 123 | logrus.Debugf("%d > %d", r.Offset()-start, tr.length) 124 | return nil, errors.New("reading outside table") 125 | } 126 | var numChars int8 127 | err = r.read(&numChars) 128 | if err != nil { 129 | return nil, err 130 | } 131 | if numChars == 0 { 132 | break 133 | } 134 | 135 | name := make([]byte, numChars) 136 | err = r.readBytes(&name, int(numChars)) 137 | if err != nil { 138 | logrus.Debugf("ERROR: %v", err) 139 | return nil, err 140 | } 141 | 142 | names = append(names, string(name)) 143 | } 144 | if len(names) != newGlyphs { 145 | logrus.Debugf("newGlyphs != len(names) (%d != %d)", len(names), newGlyphs) 146 | return nil, errors.New("mismatching number of names loaded") 147 | } 148 | 149 | t.glyphNames = make([]GlyphName, int(t.numGlyphs)) 150 | for i := 0; i < int(t.numGlyphs); i++ { 151 | var name GlyphName 152 | 153 | ni := t.glyphNameIndex[i] 154 | if ni < 258 { 155 | name = macGlyphNames[ni] 156 | } else if ni <= 32767 { 157 | ni -= 258 158 | if int(ni) >= len(names) { 159 | logrus.Debugf("ERROR: Glyph %d referring to outside name list (%d)", i, ni) 160 | // Let's be strict initially and slack if we find that it is needed. 161 | return nil, errRangeCheck 162 | } 163 | name = GlyphName(names[ni]) 164 | } 165 | logrus.Tracef("GID %d -> '%s'", i, name) 166 | t.glyphNames[i] = name 167 | } 168 | logrus.Debugf("len(names) = %d", len(names)) 169 | 170 | case 0x00025000: // 2.5 171 | logrus.Trace("Version: 2.5") 172 | err = r.read(&t.numGlyphs) 173 | if err != nil { 174 | return nil, err 175 | } 176 | if t.numGlyphs != f.maxp.numGlyphs { 177 | logrus.Debugf("post numGlyphs != maxp.numGlyphs (%d != %d)", t.numGlyphs, f.maxp.numGlyphs) 178 | return nil, errRangeCheck 179 | } 180 | err = r.readSlice(&t.offsets, int(t.numGlyphs)) 181 | if err != nil { 182 | return nil, err 183 | } 184 | t.glyphNames = make([]GlyphName, int(t.numGlyphs)) 185 | for i := 0; i < int(t.numGlyphs); i++ { 186 | nameIndex := i + 1 + int(t.offsets[i]) 187 | if nameIndex < 0 || nameIndex > 257 { 188 | logrus.Debugf("ERROR: name index outside range (%d)", nameIndex) 189 | continue 190 | } 191 | t.glyphNames[i] = macGlyphNames[nameIndex] 192 | logrus.Tracef("2.5 I: %d -> %s", i, t.glyphNames[i]) 193 | } 194 | 195 | case 0x00030000: // 3.0 196 | logrus.Debug("Version 3.0 - no postscript data") 197 | default: 198 | logrus.Debugf("Unsupported version of post (%d) - no post data loaded", t.version) 199 | } 200 | 201 | return t, nil 202 | } 203 | 204 | func (f *font) writePost(w *byteWriter) error { 205 | if f.post == nil { 206 | return nil 207 | } 208 | t := f.post 209 | 210 | // TODO(gunnsth): Write out with v1.0 or v2.0. 211 | version := t.version 212 | if version != 0x00010000 { 213 | // Include no postscript data. 214 | // TODO(gunnsth): support writing v2.0. 215 | version = 0x00030000 216 | t.version = version 217 | } 218 | 219 | err := w.write(t.version, t.italicAngle, t.underlinePosition, t.underlineThickness, t.isFixedPitch) 220 | if err != nil { 221 | return err 222 | } 223 | err = w.write(t.minMemType42, t.maxMemType42, t.minMemType1, t.maxMemType1) 224 | if err != nil { 225 | return err 226 | } 227 | 228 | return nil 229 | } 230 | -------------------------------------------------------------------------------- /table_prep.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // prepTable represents a Control Value Program table (prep). 13 | // Consists of a set of TrueType instructions that will be executed whenever the font or point size 14 | // or transformation matrix change and before each glyph is interpreted. 15 | // Used for preparation (hence the name "prep"). 16 | type prepTable struct { 17 | // number of instructions - the number of uint8 that fit the size of the table. 18 | instructions []uint8 19 | } 20 | 21 | func (f *font) parsePrep(r *byteReader) (*prepTable, error) { 22 | tr, has, err := f.seekToTable(r, "prep") 23 | if err != nil { 24 | return nil, err 25 | } 26 | if !has || tr == nil { 27 | logrus.Debug("prep table absent") 28 | return nil, nil 29 | } 30 | 31 | t := &prepTable{} 32 | numInstructions := int(tr.length) 33 | err = r.readSlice(&t.instructions, numInstructions) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return t, nil 38 | } 39 | 40 | func (f *font) writePrep(w *byteWriter) error { 41 | if f.prep == nil { 42 | return nil 43 | } 44 | 45 | return w.writeSlice(f.prep.instructions) 46 | } 47 | -------------------------------------------------------------------------------- /table_record.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "bytes" 10 | "fmt" 11 | "strings" 12 | 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // tableRecord represents table records, including name (tag) and file offset, size 17 | // and checksum for integrity checking. 18 | type tableRecord struct { 19 | tableTag tag // len=4 20 | checksum uint32 // len=4 21 | offset offset32 // len=4 22 | length uint32 // len=4 23 | } 24 | 25 | func (tr *tableRecord) read(r *byteReader) error { 26 | return r.read(&tr.tableTag, &tr.checksum, &tr.offset, &tr.length) 27 | } 28 | 29 | func (tr *tableRecord) write(w *byteWriter) error { 30 | return w.write(tr.tableTag, tr.checksum, tr.offset, tr.length) 31 | } 32 | 33 | // tableRecords represents a set of table records in a truetype font file. 34 | // Includes a map by table name for quick lookup of records. 35 | type tableRecords struct { 36 | list []*tableRecord 37 | trMap map[string]*tableRecord 38 | } 39 | 40 | func (trs *tableRecords) Set(table string, offset int64, length int, checksum uint32) { 41 | if trs.trMap == nil { 42 | trs.trMap = map[string]*tableRecord{} 43 | } 44 | newRec := &tableRecord{ 45 | tableTag: makeTag(table), 46 | offset: offset32(offset), 47 | length: uint32(length), 48 | checksum: uint32(checksum), 49 | } 50 | 51 | found := false 52 | for i := range trs.list { 53 | if trs.list[i].tableTag.String() == table { 54 | trs.list[i] = newRec 55 | found = true 56 | } 57 | } 58 | if !found { 59 | trs.list = append(trs.list, newRec) 60 | } 61 | trs.trMap[table] = newRec 62 | } 63 | 64 | func (f *font) parseTableRecords(r *byteReader) (*tableRecords, error) { 65 | trs := &tableRecords{} 66 | 67 | numTables := int(f.ot.numTables) 68 | if numTables < 0 { 69 | logrus.Debug("Invalid number of tables") 70 | return nil, errRangeCheck 71 | } 72 | 73 | if trs.trMap == nil { 74 | trs.trMap = map[string]*tableRecord{} 75 | } 76 | 77 | for i := 0; i < numTables; i++ { 78 | var rec tableRecord 79 | err := rec.read(r) 80 | if err != nil { 81 | return nil, err 82 | } 83 | trs.list = append(trs.list, &rec) 84 | trs.trMap[rec.tableTag.String()] = &rec 85 | } 86 | 87 | return trs, nil 88 | } 89 | 90 | // seekToTable seeks to position font table `tableName` in `r` if it has the table. 91 | // The table record is returned back when successful, otherwise is meaningless. 92 | // The bool flag indicates that the table exists and should be at that position if there 93 | // was no error. 94 | func (f *font) seekToTable(r *byteReader, tableName string) (tr *tableRecord, has bool, err error) { 95 | tr, has = f.trec.trMap[tableName] 96 | if !has { 97 | return tr, false, nil 98 | } 99 | 100 | err = r.SeekTo(int64(tr.offset)) 101 | if err != nil { 102 | return tr, false, err 103 | } 104 | 105 | return tr, true, nil 106 | } 107 | 108 | func (f *font) writeTableRecords(w *byteWriter) error { 109 | if f.trec == nil { 110 | logrus.Debug("Table records not set") 111 | return errRequiredField 112 | } 113 | 114 | logrus.Debugf("Writing (len:%d):", len(f.trec.list)) 115 | for _, tr := range f.trec.list { 116 | logrus.Debugf("%s - off: %d (len: %d)", tr.tableTag.String(), tr.offset, tr.length) 117 | err := tr.write(w) 118 | if err != nil { 119 | return err 120 | } 121 | } 122 | return nil 123 | } 124 | 125 | // HasTable returns true if there is a record of `tableName` in table records `trs`. 126 | func (trs *tableRecords) HasTable(tableName string) bool { 127 | _, has := trs.trMap[strings.TrimSpace(tableName)] 128 | return has 129 | } 130 | 131 | func (trs *tableRecords) String() string { 132 | var buf bytes.Buffer 133 | for i, tr := range trs.list { 134 | buf.WriteString(fmt.Sprintf("Table record %d: %+v\n", i+1, tr)) 135 | buf.WriteString(fmt.Sprintf("%s\n", tr.tableTag)) 136 | } 137 | return buf.String() 138 | } 139 | -------------------------------------------------------------------------------- /table_record_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "bytes" 10 | "testing" 11 | 12 | "github.com/sirupsen/logrus" 13 | 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | // Test unmarshalling and marshalling table records. 19 | func TestTableRecordsReadWrite(t *testing.T) { 20 | testcases := []struct { 21 | fontPath string 22 | expected []*tableRecord 23 | }{ 24 | { 25 | "./testdata/FreeSans.ttf", 26 | []*tableRecord{ 27 | { 28 | tableTag: makeTag("FFTM"), // FontForge specific table. 29 | checksum: 1195616530, 30 | offset: 459736, 31 | length: 28, 32 | }, 33 | { 34 | tableTag: makeTag("GDEF"), 35 | checksum: 31456477, 36 | offset: 433972, 37 | length: 1632, 38 | }, 39 | { 40 | tableTag: makeTag("GPOS"), 41 | checksum: 4278766266, 42 | offset: 447632, 43 | length: 12102, 44 | }, 45 | { 46 | tableTag: makeTag("GSUB"), 47 | checksum: 3391961157, 48 | offset: 435604, 49 | length: 12026, 50 | }, 51 | { 52 | tableTag: makeTag("OS/2"), 53 | checksum: 3829110115, 54 | offset: 392, 55 | length: 86, 56 | }, 57 | { 58 | tableTag: makeTag("cmap"), 59 | checksum: 4271469241, 60 | offset: 15376, 61 | length: 2526, 62 | }, 63 | { 64 | tableTag: makeTag("cvt"), 65 | checksum: 2163321, 66 | offset: 17904, 67 | length: 4, 68 | }, 69 | { 70 | tableTag: makeTag("gasp"), 71 | checksum: 4294901763, 72 | offset: 433964, 73 | length: 8, 74 | }, 75 | { 76 | tableTag: makeTag("glyf"), 77 | checksum: 843000928, 78 | offset: 32816, 79 | length: 354716, 80 | }, 81 | { 82 | tableTag: makeTag("head"), 83 | checksum: 3924650013, 84 | offset: 268, 85 | length: 54, 86 | }, 87 | { 88 | tableTag: makeTag("hhea"), 89 | checksum: 124129540, 90 | offset: 324, 91 | length: 36, 92 | }, 93 | 94 | { 95 | tableTag: makeTag("hmtx"), 96 | checksum: 2335681020, 97 | offset: 480, 98 | length: 14896, 99 | }, 100 | { 101 | tableTag: makeTag("loca"), 102 | checksum: 537012616, 103 | offset: 17908, 104 | length: 14908, 105 | }, 106 | { 107 | tableTag: makeTag("maxp"), 108 | checksum: 262341762, 109 | offset: 360, 110 | length: 32, 111 | }, 112 | { 113 | tableTag: makeTag("name"), 114 | checksum: 2006447137, 115 | offset: 387532, 116 | length: 1521, 117 | }, 118 | { 119 | tableTag: makeTag("post"), 120 | checksum: 964072869, 121 | offset: 389056, 122 | length: 44907, 123 | }, 124 | }, 125 | }, 126 | } 127 | 128 | for _, tcase := range testcases { 129 | t.Logf("%s", tcase.fontPath) 130 | fnt, err := ParseFile(tcase.fontPath) 131 | if err != nil { 132 | t.Fatalf("Error: %v", err) 133 | } 134 | assert.Equal(t, tcase.expected, fnt.trec.list) 135 | 136 | logrus.Debug("Write table records") 137 | // Marshall to buffer. 138 | var buf bytes.Buffer 139 | bw := newByteWriter(&buf) 140 | err = fnt.writeTableRecords(bw) 141 | require.NoError(t, err) 142 | bw.flush() 143 | 144 | // Reload from buffer and check equality. 145 | br := newByteReader(bytes.NewReader(buf.Bytes())) 146 | trs, err := fnt.parseTableRecords(br) 147 | require.NoError(t, err) 148 | assert.Equal(t, fnt.trec.list, trs.list) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /testdata/FreeSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/FreeSans.ttf -------------------------------------------------------------------------------- /testdata/roboto/COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Copyright 2011 Google Inc. All Rights Reserved. -------------------------------------------------------------------------------- /testdata/roboto/DESCRIPTION.en_us.html: -------------------------------------------------------------------------------- 1 |

2 | Roboto has a dual nature. 3 | It has a mechanical skeleton and the forms are largely geometric. 4 | At the same time, the font features friendly and open curves. 5 | While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesn’t compromise, allowing letters to be settled into their natural width. 6 | This makes for a more natural reading rhythm more commonly found in humanist and serif types. 7 |

8 |

9 | This is the regular family, which can be used alongside the Roboto Condensed family and the Roboto Slab family. 10 |

11 |

12 | To contribute, see github.com/google/roboto 13 |

14 | -------------------------------------------------------------------------------- /testdata/roboto/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /testdata/roboto/METADATA.pb: -------------------------------------------------------------------------------- 1 | name: "Roboto" 2 | designer: "Christian Robertson" 3 | license: "APACHE2" 4 | category: "SANS_SERIF" 5 | date_added: "2013-01-09" 6 | fonts { 7 | name: "Roboto" 8 | style: "normal" 9 | weight: 100 10 | filename: "Roboto-Thin.ttf" 11 | post_script_name: "Roboto-Thin" 12 | full_name: "Roboto Thin" 13 | copyright: "Copyright 2011 Google Inc. All Rights Reserved." 14 | } 15 | fonts { 16 | name: "Roboto" 17 | style: "italic" 18 | weight: 100 19 | filename: "Roboto-ThinItalic.ttf" 20 | post_script_name: "Roboto-ThinItalic" 21 | full_name: "Roboto Thin Italic" 22 | copyright: "Copyright 2011 Google Inc. All Rights Reserved." 23 | } 24 | fonts { 25 | name: "Roboto" 26 | style: "normal" 27 | weight: 300 28 | filename: "Roboto-Light.ttf" 29 | post_script_name: "Roboto-Light" 30 | full_name: "Roboto Light" 31 | copyright: "Copyright 2011 Google Inc. All Rights Reserved." 32 | } 33 | fonts { 34 | name: "Roboto" 35 | style: "italic" 36 | weight: 300 37 | filename: "Roboto-LightItalic.ttf" 38 | post_script_name: "Roboto-LightItalic" 39 | full_name: "Roboto Light Italic" 40 | copyright: "Copyright 2011 Google Inc. All Rights Reserved." 41 | } 42 | fonts { 43 | name: "Roboto" 44 | style: "normal" 45 | weight: 400 46 | filename: "Roboto-Regular.ttf" 47 | post_script_name: "Roboto-Regular" 48 | full_name: "Roboto" 49 | copyright: "Copyright 2011 Google Inc. All Rights Reserved." 50 | } 51 | fonts { 52 | name: "Roboto" 53 | style: "italic" 54 | weight: 400 55 | filename: "Roboto-Italic.ttf" 56 | post_script_name: "Roboto-Italic" 57 | full_name: "Roboto Italic" 58 | copyright: "Copyright 2011 Google Inc. All Rights Reserved." 59 | } 60 | fonts { 61 | name: "Roboto" 62 | style: "normal" 63 | weight: 500 64 | filename: "Roboto-Medium.ttf" 65 | post_script_name: "Roboto-Medium" 66 | full_name: "Roboto Medium" 67 | copyright: "Copyright 2011 Google Inc. All Rights Reserved." 68 | } 69 | fonts { 70 | name: "Roboto" 71 | style: "italic" 72 | weight: 500 73 | filename: "Roboto-MediumItalic.ttf" 74 | post_script_name: "Roboto-MediumItalic" 75 | full_name: "Roboto Medium Italic" 76 | copyright: "Copyright 2011 Google Inc. All Rights Reserved." 77 | } 78 | fonts { 79 | name: "Roboto" 80 | style: "normal" 81 | weight: 700 82 | filename: "Roboto-Bold.ttf" 83 | post_script_name: "Roboto-Bold" 84 | full_name: "Roboto Bold" 85 | copyright: "Copyright 2011 Google Inc. All Rights Reserved." 86 | } 87 | fonts { 88 | name: "Roboto" 89 | style: "italic" 90 | weight: 700 91 | filename: "Roboto-BoldItalic.ttf" 92 | post_script_name: "Roboto-BoldItalic" 93 | full_name: "Roboto Bold Italic" 94 | copyright: "Copyright 2011 Google Inc. All Rights Reserved." 95 | } 96 | fonts { 97 | name: "Roboto" 98 | style: "normal" 99 | weight: 900 100 | filename: "Roboto-Black.ttf" 101 | post_script_name: "Roboto-Black" 102 | full_name: "Roboto Black" 103 | copyright: "Copyright 2011 Google Inc. All Rights Reserved." 104 | } 105 | fonts { 106 | name: "Roboto" 107 | style: "italic" 108 | weight: 900 109 | filename: "Roboto-BlackItalic.ttf" 110 | post_script_name: "Roboto-BlackItalic" 111 | full_name: "Roboto Black Italic" 112 | copyright: "Copyright 2011 Google Inc. All Rights Reserved." 113 | } 114 | subsets: "menu" 115 | subsets: "cyrillic" 116 | subsets: "cyrillic-ext" 117 | subsets: "greek" 118 | subsets: "greek-ext" 119 | subsets: "latin" 120 | subsets: "latin-ext" 121 | subsets: "vietnamese" 122 | -------------------------------------------------------------------------------- /testdata/roboto/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/roboto/Roboto-Black.ttf -------------------------------------------------------------------------------- /testdata/roboto/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/roboto/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /testdata/roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /testdata/roboto/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/roboto/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /testdata/roboto/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/roboto/Roboto-Italic.ttf -------------------------------------------------------------------------------- /testdata/roboto/Roboto-Light#1.ttx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /testdata/roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /testdata/roboto/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/roboto/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /testdata/roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /testdata/roboto/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/roboto/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /testdata/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /testdata/roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /testdata/roboto/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/roboto/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /testdata/table_gdef.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package truetype 7 | 8 | type glyphClass int 9 | 10 | const ( 11 | baseGlyph glyphClass = 1 + iota 12 | ligatureGlyph 13 | markGlyph 14 | componentGlyph 15 | ) 16 | 17 | type gdefTable struct { 18 | offset int64 19 | 20 | // Version 1.0+ 21 | majorVersion uint16 22 | minorVersion uint16 23 | 24 | // Offset of subtables. Offset specified with respect to 25 | // beginning of GDEF table header. 26 | glyphClassDefOffset offset16 27 | attachListOffset offset16 28 | ligCaretListOffset offset16 29 | markAttachClassDefOffset offset16 30 | 31 | // Additional for 1.2 (minorVersion>=2). 32 | markGlyphSetsDefOffset offset16 33 | 34 | // Additional for 1.3 (minorVersion>=3). 35 | itemVarStoreOffset offset32 36 | } 37 | 38 | func (gd *gdefTable) Unmarshal(br *byteReader) error { 39 | gd.offset = br.Offset() 40 | 41 | err := br.read(&gd.majorVersion, &gd.minorVersion) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | err = br.read(&gd.glyphClassDefOffset, &gd.attachListOffset, &gd.ligCaretListOffset, &gd.markAttachClassDefOffset) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | if gd.minorVersion < 2 { 52 | return nil 53 | } 54 | 55 | // Version 1.2 and above: 56 | err = br.read(&gd.markGlyphSetsDefOffset) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | if gd.minorVersion < 3 { 62 | return nil 63 | } 64 | 65 | // Version 1.3 and above: 66 | return br.read(&gd.itemVarStoreOffset) 67 | 68 | /* 69 | var err error 70 | gd.majorVersion, err = br.readUint16() 71 | if err != nil { 72 | return err 73 | } 74 | 75 | gd.minorVersion, err = br.readUint16() 76 | if err != nil { 77 | return err 78 | } 79 | 80 | gd.glyphClassDefOffset, err = br.readOffset16() 81 | if err != nil { 82 | return err 83 | } 84 | 85 | gd.attachListOffset, err = br.readOffset16() 86 | if err != nil { 87 | return err 88 | } 89 | 90 | gd.ligCaretListOffset, err = br.readOffset16() 91 | if err != nil { 92 | return err 93 | } 94 | 95 | gd.markAttachClassDefOffset, err = br.readOffset16() 96 | if err != nil { 97 | return err 98 | } 99 | 100 | if gd.minorVersion < 2 { 101 | return nil 102 | } 103 | // For 1.2+ 104 | 105 | gd.markGlyphSetsDefOffset, err = br.readOffset16() 106 | if err != nil { 107 | return err 108 | } 109 | 110 | if gd.minorVersion < 3 { 111 | return nil 112 | } 113 | // For 1.3+ 114 | 115 | gd.itemVarStoreOffset, err = br.readOffset32() 116 | if err != nil { 117 | return err 118 | } 119 | 120 | return nil 121 | */ 122 | } 123 | 124 | // GDEF GlyphClassDef subtable. 125 | type gdefGlyphClassDef struct { 126 | } 127 | 128 | // GDEF AttachmentList subtable. 129 | type gdefAttachmentList struct { 130 | } 131 | 132 | // GDEF LigatureCaretList subtable. 133 | type gdefLigatureCaretList struct { 134 | } 135 | 136 | // GDEF MarkAttachClassDef subtable. 137 | type gdefMarkAttachClassDef struct { 138 | } 139 | 140 | // GDEF MarkGlyphSetsTable subtable. 141 | type gdefMarkGlyphSetsTable struct { 142 | } 143 | 144 | // GDEF ItemVariationStore subtable. 145 | type gdefItemVariationStore struct { 146 | } 147 | -------------------------------------------------------------------------------- /testdata/table_glyf_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package truetype 7 | 8 | import ( 9 | "bytes" 10 | "testing" 11 | ) 12 | 13 | /* 14 | TODO: There seems to be little point in trying to make each table independent, there are dependencies between 15 | the tables and makes more sense to load together with full access to the font. 16 | font.parseGlyf(r *byteReader) (*glyfTable, error) 17 | font.writeGlyf(w *byteWriter) error 18 | 19 | */ 20 | 21 | func TestGlyfUnmarshalBasic(t *testing.T) { 22 | /* 23 | Mock case. 24 | Number of glyphs: 3. 25 | Number of contours: 1, 1, 3 26 | Points per contour: 2 27 | */ 28 | // TODO (gunnsth): Unclear what is defined and what is for context. 29 | // Seems easier if unmarshaller can have access to everything or scope off context. 30 | // In global context, typically to font.parseGlyf() where font has access to everything. 31 | // Could also make the font the context parameter... 32 | // - Embed *font into tables to provide the context while Read/Writing. 33 | 34 | // Generate mock font for testing glyf table. 35 | makeFont := func() *font { 36 | head := headTable{ 37 | indexToLocFormat: 1, 38 | } 39 | maxp := maxpTable{ 40 | numGlyphs: 1, 41 | } 42 | loca := locaTable{ 43 | offsetsShort: []offset16{0}, 44 | } 45 | glyf := glyfTable{ 46 | descs: []*glyphDescription{ 47 | { 48 | header: glyfGlyphHeader{ 49 | numberOfContours: 1, 50 | }, 51 | simple: &simpleGlyphDescription{ 52 | endPtsOfContours: []uint16{1}, 53 | flags: []uint8{uint8(xShortVector | repeatFlag), uint8(xShortVector | repeatFlag)}, // unpacked. 54 | xCoordinates: []uint16{10, 20}, 55 | yCoordinates: []uint16{30, 50}, 56 | }, 57 | }, 58 | }, 59 | } 60 | 61 | return &font{ 62 | head: &head, 63 | maxp: &maxp, 64 | loca: &loca, 65 | glyf: &glyf, 66 | } 67 | } 68 | 69 | f := makeFont() 70 | var buf bytes.Buffer 71 | bw := newByteWriter(&buf) 72 | bw.flush() 73 | t.Logf("@0 checksum: %d", bw.checksum()) 74 | t.Logf("@0 Cur bufLen: %d", bw.bufferedLen()) 75 | f.writeGlyf(bw) 76 | /*( 77 | checksum := bw.checksum() 78 | 79 | fmt.Printf("checksum: %d\n", checksum) 80 | fmt.Printf("Cur bufLen: %d\n", bw.bufferedLen()) 81 | fmt.Printf("@0 %d\n", buf.Len()) 82 | bw.flush() 83 | fmt.Printf("@1 %d\n", buf.Len()) 84 | */ 85 | 86 | _ = f 87 | 88 | /* 89 | // starts with glyph data 90 | //expected := []byte{} 91 | 92 | var buf bytes.Buffer 93 | bw := newByteWriter(&buf) 94 | 95 | err := f.writeGlyf(bw) 96 | if err != nil { 97 | t.Fatalf("Error: %v", err) 98 | } 99 | 100 | b := buf.Bytes() 101 | fmt.Printf("b(%d): % X\n", len(b), b) 102 | 103 | // pad with a single byte. 104 | b = append(b, 0, 0, 0, 0) 105 | 106 | // TODO: Check marshalled output and see if it makes sense. 107 | br := newByteReader(bytes.NewReader(b)) 108 | //font2, err := parseFont(br) 109 | glyf2, err := f.ParseGlyf(br) 110 | if err != nil { 111 | t.Fatalf("Error: %v", err) 112 | } 113 | 114 | fmt.Printf("%+v\n", glyf2) 115 | */ 116 | } 117 | -------------------------------------------------------------------------------- /testdata/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package truetype 7 | 8 | // maxUint16 returns the maximium value in `sl` or 0 if empty. 9 | func maxUint16(sl []uint16) uint16 { 10 | var max uint16 11 | for i, val := range sl { 12 | if i == 0 { 13 | max = val 14 | continue 15 | } 16 | 17 | if val > max { 18 | max = val 19 | } 20 | } 21 | return max 22 | } 23 | -------------------------------------------------------------------------------- /testdata/wts11.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unidoc/unitype/0db9a036fca6c7b0ea6c507cdeed3a11b8da01b6/testdata/wts11.ttf -------------------------------------------------------------------------------- /truecli/cmd/info.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package cmd 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "sort" 12 | "strings" 13 | 14 | "github.com/spf13/cobra" 15 | 16 | "github.com/unidoc/unitype" 17 | ) 18 | 19 | const infoCmdDesc = `Information from font file.` 20 | 21 | var infoCmdExamples = []string{ 22 | fmt.Sprintf("%s info --trec font.ttf", appName), 23 | } 24 | 25 | // infoCmd represents the font info command. 26 | var infoCmd = &cobra.Command{ 27 | Use: "info ", 28 | Short: "Get font file info", 29 | Long: infoCmdDesc, 30 | Example: strings.Join(infoCmdExamples, "\n"), 31 | Args: func(cmd *cobra.Command, args []string) error { 32 | if len(args) < 1 { 33 | return errors.New("must provide an input font file") 34 | } 35 | return nil 36 | }, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | fnt, err := unitype.ParseFile(args[0]) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | showTrec, _ := cmd.Flags().GetBool("trec") 44 | showHead, _ := cmd.Flags().GetBool("head") 45 | showOS2, _ := cmd.Flags().GetBool("os2") 46 | showHhea, _ := cmd.Flags().GetBool("hhea") 47 | showHmtx, _ := cmd.Flags().GetBool("hmtx") 48 | showCmap, _ := cmd.Flags().GetBool("cmap") 49 | showLoca, _ := cmd.Flags().GetBool("loca") 50 | showGlyf, _ := cmd.Flags().GetBool("glyf") 51 | showPost, _ := cmd.Flags().GetBool("post") 52 | showName, _ := cmd.Flags().GetBool("name") 53 | showCmappings, _ := cmd.Flags().GetBool("cmappings") 54 | 55 | if showTrec { 56 | fmt.Print(fnt.TableInfo("trec")) 57 | } 58 | if showHead { 59 | fmt.Print(fnt.TableInfo("head")) 60 | } 61 | if showOS2 { 62 | fmt.Print(fnt.TableInfo("os2")) 63 | } 64 | if showHhea { 65 | fmt.Print(fnt.TableInfo("hhea")) 66 | } 67 | if showHmtx { 68 | fmt.Print(fnt.TableInfo("hmtx")) 69 | } 70 | if showCmap { 71 | fmt.Print(fnt.TableInfo("cmap")) 72 | } 73 | if showLoca { 74 | fmt.Print(fnt.TableInfo("loca")) 75 | } 76 | if showGlyf { 77 | fmt.Print(fnt.TableInfo("glyf")) 78 | } 79 | if showPost { 80 | fmt.Print(fnt.TableInfo("post")) 81 | } 82 | if showName { 83 | fmt.Print(fnt.TableInfo("name")) 84 | } 85 | 86 | if showCmappings { 87 | var maps []map[rune]unitype.GlyphIndex 88 | var mapNames []string 89 | maps = append(maps, fnt.GetCmap(0, 3)) 90 | mapNames = append(mapNames, "0,3") 91 | maps = append(maps, fnt.GetCmap(1, 0)) 92 | mapNames = append(mapNames, "1,0") 93 | maps = append(maps, fnt.GetCmap(3, 1)) 94 | mapNames = append(mapNames, "3,1") 95 | 96 | for i := range maps { 97 | var gids []unitype.GlyphIndex 98 | gidMap := map[unitype.GlyphIndex]rune{} 99 | for rune, gid := range maps[i] { 100 | gidMap[gid] = rune 101 | gids = append(gids, gid) 102 | } 103 | sort.Slice(gids, func(i, j int) bool { 104 | return gids[i] < gids[j] 105 | }) 106 | cnt := 0 107 | for _, gid := range gids { 108 | //if cnt > 100 { 109 | // break 110 | // } 111 | fmt.Printf("%d/%s: %d - %c\n", i, mapNames[i], gid, gidMap[gid]) 112 | cnt++ 113 | } 114 | } 115 | } 116 | }, 117 | } 118 | 119 | func init() { 120 | infoCmd.Flags().Bool("trec", false, "Show info for trec table") 121 | infoCmd.Flags().Bool("head", false, "Show info for head table") 122 | infoCmd.Flags().Bool("os2", false, "Show info for os2 table") 123 | infoCmd.Flags().Bool("hhea", false, "Show info for hhea table") 124 | infoCmd.Flags().Bool("hmtx", false, "Show info for hmtx table") 125 | infoCmd.Flags().Bool("cmap", false, "Show info for cmap table") 126 | infoCmd.Flags().Bool("loca", false, "Show info for loca table") 127 | infoCmd.Flags().Bool("glyf", false, "Show info for glyf table") 128 | infoCmd.Flags().Bool("post", false, "Show info for post table") 129 | infoCmd.Flags().Bool("name", false, "Show info for name table") 130 | infoCmd.Flags().Bool("cmappings", false, "List cmap mapping entries") 131 | rootCmd.AddCommand(infoCmd) 132 | } 133 | -------------------------------------------------------------------------------- /truecli/cmd/readwrite.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package cmd 7 | 8 | import ( 9 | "bytes" 10 | "errors" 11 | "fmt" 12 | "log" 13 | "strings" 14 | 15 | "github.com/spf13/cobra" 16 | 17 | "github.com/unidoc/unitype" 18 | ) 19 | 20 | const readwriteCmdDesc = `Reads and write font file back out. 21 | 22 | Loads the font file and writes back out. Great for testing the capability 23 | for loading a font file and serializing back. 24 | 25 | The input file is loaded from the output argument and the output is 26 | written to "readwrite.ttf". 27 | ` 28 | 29 | var readwriteCmdExamples = []string{ 30 | fmt.Sprintf("%s readwrite font.ttf", appName), 31 | } 32 | 33 | // readwriteCmd represents the font readwrite command. 34 | var readwriteCmd = &cobra.Command{ 35 | Use: "readwrite ", 36 | Short: "Read and write font file", 37 | Long: readwriteCmdDesc, 38 | Example: strings.Join(readwriteCmdExamples, "\n"), 39 | Args: func(cmd *cobra.Command, args []string) error { 40 | if len(args) < 1 { 41 | return errors.New("must provide an input font file") 42 | } 43 | return nil 44 | }, 45 | Run: func(cmd *cobra.Command, args []string) { 46 | tfnt, err := unitype.ParseFile(args[0]) 47 | if err != nil { 48 | log.Fatalf("Error: %+v", err) 49 | } 50 | 51 | fmt.Printf("tfnt----\n") 52 | fmt.Printf("%s\n", tfnt.String()) 53 | 54 | var buf bytes.Buffer 55 | err = tfnt.Write(&buf) 56 | if err != nil { 57 | fmt.Printf("Error writing: %+v\n", err) 58 | return 59 | } 60 | 61 | err = unitype.ValidateBytes(buf.Bytes()) 62 | if err != nil { 63 | fmt.Printf("Invalid font: %+v\n", err) 64 | panic(err) 65 | } else { 66 | fmt.Printf("Font is valid\n") 67 | } 68 | 69 | err = tfnt.WriteFile("readwrite.ttf") 70 | if err != nil { 71 | panic(err) 72 | } 73 | }, 74 | } 75 | 76 | func init() { 77 | rootCmd.AddCommand(readwriteCmd) 78 | } 79 | -------------------------------------------------------------------------------- /truecli/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/sirupsen/logrus" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | const appName = "truecli" 13 | const appVersion = "0.0.1" 14 | 15 | const rootCmdDesc = `TrueCLI` 16 | 17 | var rootCmd = &cobra.Command{ 18 | Use: appName, 19 | Long: appName + " - " + rootCmdDesc, 20 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 21 | ll, _ := cmd.Flags().GetString("loglevel") 22 | switch ll { 23 | case "debug": 24 | logrus.SetFormatter(&logrus.TextFormatter{ 25 | ForceColors: true, 26 | FullTimestamp: true, 27 | DisableLevelTruncation: true, 28 | }) 29 | logrus.SetLevel(logrus.DebugLevel) 30 | logrus.SetOutput(os.Stdout) 31 | } 32 | }, 33 | } 34 | 35 | func fatalf(format string, a ...interface{}) { 36 | fmt.Printf(format, a...) 37 | os.Exit(1) 38 | } 39 | 40 | func printUsageErr(cmd *cobra.Command, format string, a ...interface{}) { 41 | fmt.Printf("Error: "+format+"\n", a...) 42 | cmd.Help() 43 | os.Exit(1) 44 | } 45 | 46 | // Execute represents the entry point of the application. 47 | // The method parses the command line arguments and executes the appropriate 48 | // action. 49 | func Execute() { 50 | rootCmd.PersistentFlags().String("loglevel", "", "Log level 'debug' and 'trace' give debug information") 51 | if err := rootCmd.Execute(); err != nil { 52 | fatalf("Error: %v\n", err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /truecli/cmd/subset.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | const subsetCmdDesc = `Subset font.` 6 | 7 | // subset represents font subsetting commands root. 8 | var subsetCmd = &cobra.Command{ 9 | Use: "subset [FLAG]... COMMAND", 10 | Short: "Subset font", 11 | Long: subsetCmdDesc, 12 | } 13 | 14 | func init() { 15 | rootCmd.AddCommand(subsetCmd) 16 | } 17 | -------------------------------------------------------------------------------- /truecli/cmd/subset_gids.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package cmd 7 | 8 | import ( 9 | "bytes" 10 | "errors" 11 | "fmt" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/spf13/cobra" 16 | 17 | "github.com/unidoc/unitype" 18 | ) 19 | 20 | const subsetGIDsCmdDesc = `Subset a font file to a set of GIDs. 21 | 22 | Works by removing data for any GIDs outside the subset. Does not 23 | change GID order. 24 | 25 | Advantage is that the GIDs are maintained, less risky, and gives 26 | good size reduction as the glyf table is usually biggest by far. 27 | ` 28 | 29 | var subsetGIDsCmdExamples = []string{ 30 | fmt.Sprintf("%s subset gids font.ttf 10 20 30", appName), 31 | } 32 | 33 | // subsetGIDsCmd represents the font subsetting by GIDs command. 34 | var subsetGIDsCmd = &cobra.Command{ 35 | Use: "gids ...", 36 | Short: "Subset font file to specific GID subset", 37 | Long: subsetGIDsCmdDesc, 38 | Example: strings.Join(subsetGIDsCmdExamples, "\n"), 39 | Args: func(cmd *cobra.Command, args []string) error { 40 | if len(args) < 1 { 41 | return errors.New("must provide an input font file") 42 | } 43 | if len(args) < 2 { 44 | return errors.New("must provide at least one GID") 45 | } 46 | return nil 47 | }, 48 | Run: func(cmd *cobra.Command, args []string) { 49 | var gids []unitype.GlyphIndex 50 | for i := 1; i < len(args); i++ { 51 | gid, err := strconv.ParseUint(args[i], 10, 32) 52 | if err != nil { 53 | fatalf("Invalid gid: %v\n", err) 54 | } 55 | gids = append(gids, unitype.GlyphIndex(gid)) 56 | } 57 | 58 | tfnt, err := unitype.ParseFile(args[0]) 59 | if err != nil { 60 | fatalf("Error: %+v\n", err) 61 | } 62 | 63 | fmt.Printf("tfnt----\n") 64 | fmt.Printf("%s\n", tfnt.String()) 65 | 66 | var buf bytes.Buffer 67 | err = tfnt.Write(&buf) 68 | if err != nil { 69 | fmt.Printf("Error writing: %+v\n", err) 70 | return 71 | } 72 | 73 | err = unitype.ValidateBytes(buf.Bytes()) 74 | if err != nil { 75 | fmt.Printf("Invalid font: %+v\n", err) 76 | panic(err) 77 | } else { 78 | fmt.Printf("Font is valid\n") 79 | } 80 | 81 | // Try subsetting font. 82 | subfnt, err := tfnt.SubsetKeepIndices(gids) 83 | if err != nil { 84 | panic(err) 85 | } 86 | fmt.Printf("Subset font: %s\n", subfnt.String()) 87 | 88 | buf.Reset() 89 | err = subfnt.Write(&buf) 90 | if err != nil { 91 | fmt.Printf("Failed writing: %+v\n", err) 92 | panic(err) 93 | } 94 | fmt.Printf("Subset font length: %d\n", buf.Len()) 95 | err = unitype.ValidateBytes(buf.Bytes()) 96 | if err != nil { 97 | fmt.Printf("Invalid subfnt: %+v\n", err) 98 | panic(err) 99 | } else { 100 | fmt.Printf("subset font is valid\n") 101 | } 102 | 103 | err = subfnt.WriteFile("subset_gids.ttf") 104 | if err != nil { 105 | fatalf("ERROR: %v\n", err) 106 | } 107 | }, 108 | } 109 | 110 | func init() { 111 | subsetCmd.AddCommand(subsetGIDsCmd) 112 | } 113 | -------------------------------------------------------------------------------- /truecli/cmd/subset_runes.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package cmd 7 | 8 | import ( 9 | "bytes" 10 | "errors" 11 | "fmt" 12 | "strings" 13 | 14 | "github.com/spf13/cobra" 15 | 16 | "github.com/unidoc/unitype" 17 | ) 18 | 19 | const subsetRunesCmdDesc = `Subset a font file to a set of runes. 20 | 21 | Works by removing data for any runes corresponding to GIDs outside the subset. 22 | Maintains GID order and other information. 23 | 24 | Advantage is that the GIDs are maintained, less risky, and gives 25 | good size reduction as the glyf table is usually biggest by far. 26 | ` 27 | 28 | var subsetRunesCmdExamples = []string{ 29 | fmt.Sprintf("%s subset runes font.ttf abcefgh", appName), 30 | } 31 | 32 | // subsetRunesCmd represents the font subsetting by runes command. 33 | var subsetRunesCmd = &cobra.Command{ 34 | Use: "runes ", 35 | Short: "Subset font file to specific rune subset", 36 | Long: subsetRunesCmdDesc, 37 | Example: strings.Join(subsetRunesCmdExamples, "\n"), 38 | Args: func(cmd *cobra.Command, args []string) error { 39 | if len(args) < 1 { 40 | return errors.New("must provide an input font file") 41 | } 42 | if len(args) < 2 { 43 | return errors.New("must provide runes to subset") 44 | } 45 | return nil 46 | }, 47 | Run: func(cmd *cobra.Command, args []string) { 48 | tfnt, err := unitype.ParseFile(args[0]) 49 | if err != nil { 50 | fatalf("Error: %+v\n", err) 51 | } 52 | 53 | outpath, _ := cmd.Flags().GetString("outfile") 54 | 55 | fmt.Printf("Original %s: %s\n", args[0], tfnt.String()) 56 | 57 | runes := []rune(args[1]) 58 | 59 | var buf bytes.Buffer 60 | err = tfnt.Write(&buf) 61 | if err != nil { 62 | fmt.Printf("Error writing: %+v\n", err) 63 | return 64 | } 65 | origSize := buf.Len() 66 | 67 | err = unitype.ValidateBytes(buf.Bytes()) 68 | if err != nil { 69 | fmt.Printf("Invalid font: %+v\n", err) 70 | panic(err) 71 | } else { 72 | fmt.Printf("Font is valid\n") 73 | } 74 | 75 | // Try subsetting font. 76 | subfnt, err := tfnt.SubsetKeepRunes(runes) 77 | if err != nil { 78 | panic(err) 79 | } 80 | 81 | buf.Reset() 82 | err = subfnt.Write(&buf) 83 | if err != nil { 84 | fmt.Printf("Failed writing: %+v\n", err) 85 | panic(err) 86 | } 87 | subsetSize := buf.Len() 88 | 89 | fmt.Printf("Original size: %d\n", origSize) 90 | fmt.Printf("Subset size: %d (%.2f X)\n", buf.Len(), float64(origSize)/float64(subsetSize)) 91 | err = unitype.ValidateBytes(buf.Bytes()) 92 | if err != nil { 93 | fmt.Printf("Invalid subfnt: %+v\n", err) 94 | panic(err) 95 | } else { 96 | fmt.Printf("subset font is valid\n") 97 | } 98 | 99 | err = subfnt.WriteFile(outpath) 100 | if err != nil { 101 | fatalf("ERROR: %v\n", err) 102 | } 103 | fmt.Printf("Output written: %s\n", outpath) 104 | }, 105 | } 106 | 107 | func init() { 108 | subsetRunesCmd.Flags().StringP("outfile", "o", "subset_runes.ttf", "Output file name") 109 | subsetCmd.AddCommand(subsetRunesCmd) 110 | } 111 | -------------------------------------------------------------------------------- /truecli/cmd/subset_simple.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package cmd 7 | 8 | import ( 9 | "bytes" 10 | "errors" 11 | "fmt" 12 | "log" 13 | "strings" 14 | 15 | "github.com/spf13/cobra" 16 | 17 | "github.com/unidoc/unitype" 18 | ) 19 | 20 | const subsetSimpleCmdDesc = `Subset a font file to a simple subset (256 glyphs). 21 | 22 | Outputs a new font file "subset.ttf" that contains only 23 | the first 256 glyphs from the input font file. 24 | 25 | TODO: In the future add options to select what glyphs are 26 | picked, like a set of GID ranges or lists of runes. 27 | ` 28 | 29 | var subsetSimpleCmdExamples = []string{ 30 | fmt.Sprintf("%s subset simple font.ttf", appName), 31 | } 32 | 33 | // subsetSimpleCmd represents the font subsetting command. 34 | var subsetSimpleCmd = &cobra.Command{ 35 | Use: "simple ", 36 | Short: "Subset font file to simple subset", 37 | Long: subsetSimpleCmdDesc, 38 | Example: strings.Join(subsetSimpleCmdExamples, "\n"), 39 | Args: func(cmd *cobra.Command, args []string) error { 40 | if len(args) < 1 { 41 | return errors.New("must provide an input font file") 42 | } 43 | return nil 44 | }, 45 | Run: func(cmd *cobra.Command, args []string) { 46 | tfnt, err := unitype.ParseFile(args[0]) 47 | if err != nil { 48 | log.Fatalf("Error: %+v", err) 49 | } 50 | 51 | fmt.Printf("tfnt----\n") 52 | fmt.Printf("%s\n", tfnt.String()) 53 | 54 | var buf bytes.Buffer 55 | err = tfnt.Write(&buf) 56 | if err != nil { 57 | fmt.Printf("Error writing: %+v\n", err) 58 | return 59 | } 60 | 61 | err = unitype.ValidateBytes(buf.Bytes()) 62 | if err != nil { 63 | fmt.Printf("Invalid font: %+v\n", err) 64 | panic(err) 65 | } else { 66 | fmt.Printf("Font is valid\n") 67 | } 68 | 69 | // Try subsetting font. 70 | subfnt, err := tfnt.SubsetFirst(256) 71 | if err != nil { 72 | panic(err) 73 | } 74 | fmt.Printf("Subset font: %s\n", subfnt.String()) 75 | 76 | buf.Reset() 77 | err = subfnt.Write(&buf) 78 | if err != nil { 79 | fmt.Printf("Failed writing: %+v\n", err) 80 | panic(err) 81 | } 82 | fmt.Printf("Subset font length: %d\n", buf.Len()) 83 | err = unitype.ValidateBytes(buf.Bytes()) 84 | if err != nil { 85 | fmt.Printf("Invalid subfnt: %+v\n", err) 86 | panic(err) 87 | } else { 88 | fmt.Printf("subset font is valid\n") 89 | } 90 | 91 | err = subfnt.WriteFile("subset.ttf") 92 | if err != nil { 93 | fatalf("ERROR: %v\n", err) 94 | } 95 | }, 96 | } 97 | 98 | func init() { 99 | subsetCmd.AddCommand(subsetSimpleCmd) 100 | } 101 | -------------------------------------------------------------------------------- /truecli/cmd/validate.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package cmd 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | 12 | "github.com/spf13/cobra" 13 | 14 | "github.com/unidoc/unitype" 15 | ) 16 | 17 | const validateCmdDesc = `Validate font file.` 18 | 19 | // validateCmd represents the font validation command. 20 | var validateCmd = &cobra.Command{ 21 | Use: "validate ", 22 | Short: "Validate font file", 23 | Long: validateCmdDesc, 24 | Args: func(cmd *cobra.Command, args []string) error { 25 | if len(args) < 1 { 26 | return errors.New("must provide an input font file") 27 | } 28 | return nil 29 | }, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | // Validate. 32 | err := unitype.ValidateFile(args[0]) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | // Parse and output info. 38 | fnt, err := unitype.ParseFile(args[0]) 39 | if err != nil { 40 | panic(err) 41 | } 42 | fmt.Printf("%s\n", fnt.String()) 43 | }, 44 | } 45 | 46 | func init() { 47 | rootCmd.AddCommand(validateCmd) 48 | } 49 | -------------------------------------------------------------------------------- /truecli/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/unidoc/unitype/truecli 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/sirupsen/logrus v1.9.3 7 | github.com/spf13/cobra v1.8.1 8 | github.com/unidoc/unitype v0.5.1 9 | ) 10 | 11 | require ( 12 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 13 | github.com/spf13/pflag v1.0.5 // indirect 14 | golang.org/x/sys v0.29.0 // indirect 15 | golang.org/x/text v0.21.0 // indirect 16 | ) 17 | 18 | replace github.com/unidoc/unitype => ../ 19 | -------------------------------------------------------------------------------- /truecli/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 6 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 10 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 11 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 12 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 13 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 14 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 15 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 16 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 17 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 18 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 19 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 21 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 22 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 23 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 25 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 27 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 28 | -------------------------------------------------------------------------------- /truecli/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package main 7 | 8 | import "github.com/unidoc/unitype/truecli/cmd" 9 | 10 | func main() { 11 | cmd.Execute() 12 | } 13 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "encoding/binary" 10 | "strings" 11 | ) 12 | 13 | // CharCode is an internal typically 1-2 byte representation of a code. Its meaning depends on encoding context. 14 | // Requires an accompanying cmapEncoder for mapping from/to runes. 15 | type CharCode uint32 16 | 17 | // GlyphName is a representation of a glyph name, e.g. from Adobe's glyph list. 18 | type GlyphName string 19 | 20 | // GlyphIndex or Glyph ID (GID) represent each glyph within a font. 21 | type GlyphIndex uint16 22 | 23 | /* 24 | Types in truetype fonts: 25 | https://docs.microsoft.com/en-us/typography/opentype/spec/otff 26 | 27 | Data Type Description 28 | -------------------------------------------------------- 29 | uint8 8-bit unsigned integer. 30 | int8 8-bit signed integer. 31 | uint16 16-bit unsigned integer. 32 | int16 16-bit signed integer. 33 | uint24 24-bit unsigned integer. 34 | uint32 32-bit unsigned integer. 35 | int32 32-bit signed integer. 36 | Fixed 32-bit signed fixed-point number (16.16) 37 | FWORD int16 that describes a quantity in font design units. 38 | UFWORD uint16 that describes a quantity in font design units. 39 | F2DOT14 16-bit signed fixed number with the low 14 bits of fraction (2.14). 40 | LONGDATETIME 41 | Date represented in number of seconds since 12:00 midnight, January 1, 1904. 42 | The value is represented as a signed 64-bit integer. 43 | Tag Array of four uint8s (length = 32 bits) used to identify a table, 44 | design-variation axis, script, language system, feature, or baseline 45 | Offset16 Short offset to a table, same as uint16, NULL offset = 0x0000 46 | Offset32 Long offset to a table, same as uint32, NULL offset = 0x00000000 47 | */ 48 | 49 | type fixed int32 50 | type fword int16 51 | type ufword uint16 52 | type f2dot14 int16 53 | type longdatetime int64 54 | type tag [4]uint8 55 | type offset16 uint16 56 | type offset32 uint32 57 | 58 | func (t tag) String() string { 59 | return strings.TrimSpace(string(t[:])) 60 | } 61 | 62 | // Parts returns the integral and decimal portions of `f`. 63 | func (f fixed) Parts() (uint16, uint16) { 64 | b := make([]byte, 4) 65 | binary.BigEndian.PutUint32(b, uint32(f)) 66 | return binary.BigEndian.Uint16(b[0:2]), binary.BigEndian.Uint16(b[2:4]) 67 | } 68 | 69 | // Float64 returns `f` as a float64. 70 | func (f fixed) Float64() float64 { 71 | b := make([]byte, 4) 72 | binary.BigEndian.PutUint32(b, uint32(f)) 73 | l, r := binary.BigEndian.Uint16(b[0:2]), binary.BigEndian.Uint16(b[2:4]) 74 | integral := float64(int16(l)) 75 | fraction := float64(r) / 65536.0 76 | return integral + fraction 77 | } 78 | 79 | func makeTag(s string) tag { 80 | bb := []byte(s[:]) 81 | if len(bb) > 4 { 82 | // Trim to 4 bytes. 83 | bb = bb[:4] 84 | } 85 | if len(bb) < 4 { 86 | // Pad with spaces to fill 4 bytes. 87 | for i := 0; i < 4-len(bb); i++ { 88 | bb = append(bb, ' ') 89 | } 90 | } 91 | 92 | var t tag 93 | copy(t[:], bb) 94 | return t 95 | } 96 | -------------------------------------------------------------------------------- /type_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "testing" 10 | ) 11 | 12 | func TestFixedParts(t *testing.T) { 13 | tcases := []struct { 14 | val fixed 15 | a uint16 16 | b uint16 17 | f64 float64 18 | }{ 19 | { 20 | fixed(0x00011000), 21 | 0x0001, 22 | 0x1000, 23 | 1.0625, // FIXME/TODO(gunnsth): Should be 1.1 ? 24 | }, 25 | { 26 | fixed(0x00005000), 27 | 0x0000, 28 | 0x5000, 29 | 0.3125, // FIXME/TODO(gunnsth): Should be 0.5 ? 30 | }, 31 | { 32 | fixed(0x00025000), 33 | 0x0002, 34 | 0x5000, 35 | 2.3125, // FIXME/TODO(gunnsth): Should be 2.5? 36 | }, 37 | } 38 | 39 | for _, tcase := range tcases { 40 | a, b := tcase.val.Parts() 41 | if a != tcase.a { 42 | t.Fatalf("%d != %d", a, tcase.a) 43 | } 44 | if b != tcase.b { 45 | t.Fatalf("%d != %d", b, tcase.b) 46 | } 47 | f64 := tcase.val.Float64() 48 | if f64 != tcase.f64 { 49 | t.Fatalf("%v != %v", f64, tcase.f64) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "bytes" 10 | "errors" 11 | "io" 12 | 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // validate font data model `f` in `r`. Checks if required tables are present and whether 17 | // table checksums are correct. 18 | func (f *font) validate(r *byteReader) error { 19 | if f.trec == nil { 20 | logrus.Debug("Table records missing") 21 | return errRequiredField 22 | } 23 | if f.ot == nil { 24 | logrus.Debug("Offsets table missing") 25 | return errRequiredField 26 | } 27 | if f.head == nil { 28 | logrus.Debug("head table missing") 29 | return errRequiredField 30 | } 31 | 32 | // Validate the font. 33 | logrus.Debug("Validating entire font") 34 | { 35 | err := r.SeekTo(0) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | var buf bytes.Buffer 41 | _, err = io.Copy(&buf, r.reader) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | data := buf.Bytes() 47 | 48 | headRec, ok := f.trec.trMap["head"] 49 | if !ok { 50 | logrus.Debug("head not set") 51 | return errRequiredField 52 | } 53 | hoff := headRec.offset 54 | 55 | // set checksumAdjustment data to 0 in the head table. 56 | data[hoff+8] = 0 57 | data[hoff+9] = 0 58 | data[hoff+10] = 0 59 | data[hoff+11] = 0 60 | 61 | bw := newByteWriter(&bytes.Buffer{}) 62 | bw.buffer.Write(data) 63 | 64 | checksum := bw.checksum() 65 | adjustment := 0xB1B0AFBA - checksum 66 | if f.head.checksumAdjustment != adjustment { 67 | return errors.New("file checksum mismatch") 68 | } 69 | } 70 | 71 | // Validate each table. 72 | logrus.Debug("Validating font tables") 73 | for _, tr := range f.trec.list { 74 | logrus.Debugf("Validating %s", tr.tableTag.String()) 75 | logrus.Debugf("%+v", tr) 76 | 77 | bw := newByteWriter(&bytes.Buffer{}) 78 | 79 | if tr.offset < 0 || tr.length < 0 { 80 | logrus.Debug("Range check error") 81 | return errRangeCheck 82 | } 83 | 84 | logrus.Debugf("Seeking to %d, to read %d bytes", tr.offset, tr.length) 85 | err := r.SeekTo(int64(tr.offset)) 86 | if err != nil { 87 | return err 88 | } 89 | logrus.Debugf("Offset: %d", r.Offset()) 90 | 91 | b := make([]byte, tr.length) 92 | _, err = io.ReadFull(r.reader, b) 93 | if err != nil { 94 | return err 95 | } 96 | logrus.Debugf("Read (%d)", len(b)) 97 | // TODO(gunnsth): Validate head. 98 | if tr.tableTag.String() == "head" { 99 | // Set the checksumAdjustment to 0 so that head checksum is valid. 100 | if len(b) < 12 { 101 | return errors.New("head too short") 102 | } 103 | b[8], b[9], b[10], b[11] = 0, 0, 0, 0 104 | } 105 | 106 | _, err = bw.buffer.Write(b) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | checksum := bw.checksum() 112 | if tr.checksum != checksum { 113 | logrus.Debugf("Invalid checksum (%d != %d)", checksum, tr.checksum) 114 | return errors.New("checksum incorrect") 115 | } 116 | 117 | if int(tr.length) != bw.bufferedLen() { 118 | logrus.Debug("Length mismatch") 119 | return errRangeCheck 120 | } 121 | } 122 | 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /validate_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is subject to the terms and conditions defined in 3 | * file 'LICENSE.md', which is part of this source code package. 4 | */ 5 | 6 | package unitype 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | "time" 12 | 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func TestFontValidation(t *testing.T) { 17 | testcases := []struct { 18 | fontPath string 19 | }{ 20 | { 21 | "./testdata/FreeSans.ttf", 22 | }, 23 | { 24 | "./testdata/wts11.ttf", 25 | }, 26 | { 27 | "./testdata/roboto/Roboto-BoldItalic.ttf", 28 | }, 29 | } 30 | 31 | for _, tcase := range testcases { 32 | t.Logf("%s", tcase.fontPath) 33 | fmt.Printf("==== %s\n", tcase.fontPath) 34 | logrus.Debugf("==== %s", tcase.fontPath) 35 | start := time.Now() 36 | err := ValidateFile(tcase.fontPath) 37 | if err != nil { 38 | t.Fatalf("Error: %v", err) 39 | } 40 | diff := time.Now().Sub(start) 41 | t.Logf("- took: %s", diff.String()) 42 | } 43 | 44 | } 45 | --------------------------------------------------------------------------------