|
2 | 2 | A PostScript backend, which can produce both PostScript .ps and .eps.
|
3 | 3 | """
|
4 | 4 |
|
| 5 | +import binascii |
| 6 | +import bisect |
5 | 7 | import codecs
|
6 | 8 | import datetime
|
7 | 9 | from enum import Enum
|
8 | 10 | import functools
|
9 | 11 | import glob
|
10 |
| -from io import StringIO, TextIOWrapper |
| 12 | +from io import BytesIO, StringIO, TextIOWrapper |
11 | 13 | import logging
|
12 | 14 | import math
|
13 | 15 | import os
|
14 | 16 | import pathlib
|
15 | 17 | import tempfile
|
| 18 | +import textwrap |
16 | 19 | import re
|
17 | 20 | import shutil
|
| 21 | +import struct |
18 | 22 | from tempfile import TemporaryDirectory
|
19 | 23 | import time
|
20 | 24 |
|
| 25 | +import fontTools |
21 | 26 | import numpy as np
|
22 | 27 |
|
23 | 28 | import matplotlib as mpl
|
|
29 | 34 | from matplotlib.cbook import is_writable_file_like, file_requires_unicode
|
30 | 35 | from matplotlib.font_manager import get_font
|
31 | 36 | from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_NO_SCALE, FT2Font
|
32 |
| -from matplotlib._ttconv import convert_ttf_to_ps |
33 | 37 | from matplotlib.mathtext import MathTextParser
|
34 | 38 | from matplotlib._mathtext_data import uni2type1
|
35 | 39 | from matplotlib.path import Path
|
@@ -218,27 +222,175 @@ def _font_to_ps_type42(font_path, chars, fh):
|
218 | 222 | subset_str = ''.join(chr(c) for c in chars)
|
219 | 223 | _log.debug("SUBSET %s characters: %s", font_path, subset_str)
|
220 | 224 | try:
|
221 |
| - fontdata = _backend_pdf_ps.get_glyphs_subset(font_path, subset_str) |
222 |
| - _log.debug("SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size, |
223 |
| - fontdata.getbuffer().nbytes) |
| 225 | + with _backend_pdf_ps.get_glyphs_subset(font_path, subset_str) as subset: |
| 226 | + fontdata = _backend_pdf_ps.font_as_file(subset).getvalue() |
| 227 | + _log.debug("SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size, |
| 228 | + len(fontdata)) |
| 229 | + fh.write(_serialize_type42(subset, fontdata)) |
| 230 | + except RuntimeError: |
| 231 | + print("XXX too bad") |
| 232 | + raise |
224 | 233 |
|
225 |
| - # Give ttconv a subsetted font along with updated glyph_ids. |
226 |
| - font = FT2Font(fontdata) |
227 |
| - glyph_ids = [font.get_char_index(c) for c in chars] |
228 |
| - with TemporaryDirectory() as tmpdir: |
229 |
| - tmpfile = os.path.join(tmpdir, "tmp.ttf") |
230 | 234 |
|
231 |
| - with open(tmpfile, 'wb') as tmp: |
232 |
| - tmp.write(fontdata.getvalue()) |
| 235 | +def _serialize_type42(font, fontdata): |
| 236 | + """ |
| 237 | + XXX |
| 238 | + """ |
| 239 | + version, breakpoints = _version_and_breakpoints(font['loca'], fontdata) |
| 240 | + post, name = font['post'], font['name'] |
| 241 | + fmt = textwrap.dedent(f""" |
| 242 | + %%!PS-TrueTypeFont-{version[0]}.{version[1]}-{font['head'].fontRevision:.7f} |
| 243 | + 10 dict begin |
| 244 | + /FontType 42 def |
| 245 | + /FontMatrix [1 0 0 1 0 0] def |
| 246 | + /FontName /{name.getDebugName(6)} def |
| 247 | + /FontInfo 7 dict dup begin |
| 248 | + /FullName ({name.getDebugName(4)}) def |
| 249 | + /FamilyName ({name.getDebugName(1)}) def |
| 250 | + /Version ({name.getDebugName(5)}) def |
| 251 | + /ItalicAngle {post.italicAngle} def |
| 252 | + /isFixedPitch {'true' if post.isFixedPitch else 'false'} def |
| 253 | + /UnderlinePosition {post.underlinePosition} def |
| 254 | + /UnderlineThickness {post.underlineThickness} def |
| 255 | + end readonly def |
| 256 | + /Encoding StandardEncoding def |
| 257 | + /FontBBox [{' '.join(str(x) for x in _bounds(font))}] def |
| 258 | + /PaintType 0 def |
| 259 | + /CIDMap 0 def |
| 260 | + %s |
| 261 | + %s |
| 262 | + FontName currentdict end definefont pop |
| 263 | + """) |
| 264 | + |
| 265 | + return fmt % (_charstrings(font), _sfnts(fontdata, font, breakpoints)) |
| 266 | + |
| 267 | + |
| 268 | +def _version_and_breakpoints(loca, fontdata): |
| 269 | + """ |
| 270 | + Read the version number of the font and determine sfnts breakpoints. |
| 271 | + When a TrueType font file is written as a Type 42 font, it has to be |
| 272 | + broken into substrings of at most 65535 bytes. These substrings must |
| 273 | + begin at font table boundaries or glyph boundaries in the glyf table. |
| 274 | + This function determines all possible breakpoints and it is the caller's |
| 275 | + responsibility to do the splitting. |
233 | 276 |
|
234 |
| - # TODO: allow convert_ttf_to_ps to input file objects (BytesIO) |
235 |
| - convert_ttf_to_ps(os.fsencode(tmpfile), fh, 42, glyph_ids) |
236 |
| - except RuntimeError: |
237 |
| - _log.warning( |
238 |
| - "The PostScript backend does not currently " |
239 |
| - "support the selected font.") |
240 |
| - raise |
| 277 | + Helper function for _font_to_ps_type42. |
| 278 | +
|
| 279 | + Parameters |
| 280 | + ---------- |
| 281 | + loca : fontTools.ttLib._l_o_c_a.table__l_o_c_a |
| 282 | + The loca table of the font |
| 283 | + fontdata : bytes |
| 284 | + The raw data of the font |
| 285 | + |
| 286 | + Returns |
| 287 | + ------- |
| 288 | + tuple |
| 289 | + ((v1, v2), breakpoints) where v1 is the major version number, |
| 290 | + v2 is the minor version number and breakpoints is a sorted list |
| 291 | + of offsets into fontdata |
| 292 | + """ |
| 293 | + v1, v2, numTables = struct.unpack('>3h', fontdata[:6]) |
| 294 | + version = (v1, v2) |
| 295 | + |
| 296 | + tables = {} |
| 297 | + for i in range(numTables): |
| 298 | + tag, _, offset, _ = struct.unpack('>4sIII', fontdata[12 + i*16:12 + (i+1)*16]) |
| 299 | + tables[tag.decode('ascii')] = offset |
| 300 | + breakpoints = sorted( |
| 301 | + set(tables.values()) |
| 302 | + | { tables['glyf'] + offset for offset in loca.locations[:-1] } |
| 303 | + | { len(fontdata) } |
| 304 | + ) |
| 305 | + |
| 306 | + return version, breakpoints |
| 307 | + |
| 308 | + |
| 309 | +def _bounds(font): |
| 310 | + """ |
| 311 | + Compute the font bounding box, as if all glyphs were written |
| 312 | + at the same start position. |
| 313 | +
|
| 314 | + Helper function for _font_to_ps_type42. |
241 | 315 |
|
| 316 | + Parameters |
| 317 | + ---------- |
| 318 | + font : fontTools.ttLib.ttFont.TTFont |
| 319 | + The font |
| 320 | +
|
| 321 | + Returns |
| 322 | + ------- |
| 323 | + tuple |
| 324 | + (xMin, yMin, xMax, yMax) of the combined bounding box |
| 325 | + of all the glyphs in the font |
| 326 | + """ |
| 327 | + gs = font.getGlyphSet(False) |
| 328 | + pen = fontTools.pens.boundsPen.BoundsPen(gs) |
| 329 | + for name in gs.keys(): |
| 330 | + gs[name].draw(pen) |
| 331 | + return pen.bounds |
| 332 | + |
| 333 | + |
| 334 | +def _charstrings(font): |
| 335 | + """ |
| 336 | + Transform font glyphs into CharStrings |
| 337 | +
|
| 338 | + Helper function for _font_to_ps_type42. |
| 339 | +
|
| 340 | + Parameters |
| 341 | + ---------- |
| 342 | + font : fontTools.ttLib.ttFont.TTFont |
| 343 | + The font |
| 344 | +
|
| 345 | + Returns |
| 346 | + ------- |
| 347 | + str |
| 348 | + A definition of the CharStrings dictionary in PostScript |
| 349 | + """ |
| 350 | + s = StringIO() |
| 351 | + go = font.getGlyphOrder() |
| 352 | + s.write(f'/CharStrings {len(go)} dict dup begin\n') |
| 353 | + for i, name in enumerate(go): |
| 354 | + s.write(f'/{name} {i} def\n') |
| 355 | + s.write('end readonly def') |
| 356 | + return s.getvalue() |
| 357 | + |
| 358 | + |
| 359 | +def _sfnts(fontdata, font, breakpoints): |
| 360 | + """ |
| 361 | + Transform font data into PostScript sfnts format. |
| 362 | +
|
| 363 | + Helper function for _font_to_ps_type42. |
| 364 | +
|
| 365 | + Parameters |
| 366 | + ---------- |
| 367 | + fontdata : bytes |
| 368 | + The raw data of the font |
| 369 | + font : fontTools.ttLib.ttFont.TTFont |
| 370 | + The fontTools font object |
| 371 | + breakpoints : list |
| 372 | + Sorted offsets of possible breakpoints |
| 373 | + |
| 374 | + Returns |
| 375 | + ------- |
| 376 | + str |
| 377 | + The sfnts array for the font definition, consisting |
| 378 | + of hex-encoded strings in PostScript format |
| 379 | + """ |
| 380 | + b = BytesIO() |
| 381 | + b.write(b'/sfnts[') |
| 382 | + pos = 0 |
| 383 | + while pos < len(fontdata): |
| 384 | + i = bisect.bisect_left(breakpoints, pos + 65534) |
| 385 | + newpos = breakpoints[i-1] |
| 386 | + b.write(b'<') |
| 387 | + b.write(binascii.hexlify(fontdata[pos:newpos])) |
| 388 | + b.write(b'00>') # need an extra zero byte on every string |
| 389 | + pos = newpos |
| 390 | + b.write(b']def') |
| 391 | + s = b.getvalue().decode('ascii') |
| 392 | + return '\n'.join(s[i:i+100] for i in range(0, len(s), 100)) |
| 393 | + |
242 | 394 |
|
243 | 395 | def _log_if_debug_on(meth):
|
244 | 396 | """
|
|
0 commit comments