123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- """Compiles/decompiles SVG table.
- https://docs.microsoft.com/en-us/typography/opentype/spec/svg
- The XML format is:
- .. code-block:: xml
- <SVG>
- <svgDoc endGlyphID="1" startGlyphID="1">
- <![CDATA[ <complete SVG doc> ]]
- </svgDoc>
- ...
- <svgDoc endGlyphID="n" startGlyphID="m">
- <![CDATA[ <complete SVG doc> ]]
- </svgDoc>
- </SVG>
- """
- from fontTools.misc.textTools import bytesjoin, safeEval, strjoin, tobytes, tostr
- from fontTools.misc import sstruct
- from . import DefaultTable
- from collections.abc import Sequence
- from dataclasses import dataclass, astuple
- from io import BytesIO
- import struct
- import logging
- log = logging.getLogger(__name__)
- SVG_format_0 = """
- > # big endian
- version: H
- offsetToSVGDocIndex: L
- reserved: L
- """
- SVG_format_0Size = sstruct.calcsize(SVG_format_0)
- doc_index_entry_format_0 = """
- > # big endian
- startGlyphID: H
- endGlyphID: H
- svgDocOffset: L
- svgDocLength: L
- """
- doc_index_entry_format_0Size = sstruct.calcsize(doc_index_entry_format_0)
- class table_S_V_G_(DefaultTable.DefaultTable):
- """Scalable Vector Graphics table
- The ``SVG`` table contains representations for glyphs in the SVG
- image format.
- See also https://learn.microsoft.com/en-us/typography/opentype/spec/stat
- """
- def decompile(self, data, ttFont):
- self.docList = []
- # Version 0 is the standardized version of the table; and current.
- # https://www.microsoft.com/typography/otspec/svg.htm
- sstruct.unpack(SVG_format_0, data[:SVG_format_0Size], self)
- if self.version != 0:
- log.warning(
- "Unknown SVG table version '%s'. Decompiling as version 0.",
- self.version,
- )
- # read in SVG Documents Index
- # data starts with the first entry of the entry list.
- pos = subTableStart = self.offsetToSVGDocIndex
- self.numEntries = struct.unpack(">H", data[pos : pos + 2])[0]
- pos += 2
- if self.numEntries > 0:
- data2 = data[pos:]
- entries = []
- for i in range(self.numEntries):
- record_data = data2[
- i
- * doc_index_entry_format_0Size : (i + 1)
- * doc_index_entry_format_0Size
- ]
- docIndexEntry = sstruct.unpack(
- doc_index_entry_format_0, record_data, DocumentIndexEntry()
- )
- entries.append(docIndexEntry)
- for entry in entries:
- start = entry.svgDocOffset + subTableStart
- end = start + entry.svgDocLength
- doc = data[start:end]
- compressed = False
- if doc.startswith(b"\x1f\x8b"):
- import gzip
- bytesIO = BytesIO(doc)
- with gzip.GzipFile(None, "r", fileobj=bytesIO) as gunzipper:
- doc = gunzipper.read()
- del bytesIO
- compressed = True
- doc = tostr(doc, "utf_8")
- self.docList.append(
- SVGDocument(doc, entry.startGlyphID, entry.endGlyphID, compressed)
- )
- def compile(self, ttFont):
- version = 0
- offsetToSVGDocIndex = (
- SVG_format_0Size # I start the SVGDocIndex right after the header.
- )
- # get SGVDoc info.
- docList = []
- entryList = []
- numEntries = len(self.docList)
- datum = struct.pack(">H", numEntries)
- entryList.append(datum)
- curOffset = len(datum) + doc_index_entry_format_0Size * numEntries
- seenDocs = {}
- allCompressed = getattr(self, "compressed", False)
- for i, doc in enumerate(self.docList):
- if isinstance(doc, (list, tuple)):
- doc = SVGDocument(*doc)
- self.docList[i] = doc
- docBytes = tobytes(doc.data, encoding="utf_8")
- if (allCompressed or doc.compressed) and not docBytes.startswith(
- b"\x1f\x8b"
- ):
- import gzip
- bytesIO = BytesIO()
- # mtime=0 strips the useless timestamp and makes gzip output reproducible;
- # equivalent to `gzip -n`
- with gzip.GzipFile(None, "w", fileobj=bytesIO, mtime=0) as gzipper:
- gzipper.write(docBytes)
- gzipped = bytesIO.getvalue()
- if len(gzipped) < len(docBytes):
- docBytes = gzipped
- del gzipped, bytesIO
- docLength = len(docBytes)
- if docBytes in seenDocs:
- docOffset = seenDocs[docBytes]
- else:
- docOffset = curOffset
- curOffset += docLength
- seenDocs[docBytes] = docOffset
- docList.append(docBytes)
- entry = struct.pack(
- ">HHLL", doc.startGlyphID, doc.endGlyphID, docOffset, docLength
- )
- entryList.append(entry)
- entryList.extend(docList)
- svgDocData = bytesjoin(entryList)
- reserved = 0
- header = struct.pack(">HLL", version, offsetToSVGDocIndex, reserved)
- data = [header, svgDocData]
- data = bytesjoin(data)
- return data
- def toXML(self, writer, ttFont):
- for i, doc in enumerate(self.docList):
- if isinstance(doc, (list, tuple)):
- doc = SVGDocument(*doc)
- self.docList[i] = doc
- attrs = {"startGlyphID": doc.startGlyphID, "endGlyphID": doc.endGlyphID}
- if doc.compressed:
- attrs["compressed"] = 1
- writer.begintag("svgDoc", **attrs)
- writer.newline()
- writer.writecdata(doc.data)
- writer.newline()
- writer.endtag("svgDoc")
- writer.newline()
- def fromXML(self, name, attrs, content, ttFont):
- if name == "svgDoc":
- if not hasattr(self, "docList"):
- self.docList = []
- doc = strjoin(content)
- doc = doc.strip()
- startGID = int(attrs["startGlyphID"])
- endGID = int(attrs["endGlyphID"])
- compressed = bool(safeEval(attrs.get("compressed", "0")))
- self.docList.append(SVGDocument(doc, startGID, endGID, compressed))
- else:
- log.warning("Unknown %s %s", name, content)
- class DocumentIndexEntry(object):
- def __init__(self):
- self.startGlyphID = None # USHORT
- self.endGlyphID = None # USHORT
- self.svgDocOffset = None # ULONG
- self.svgDocLength = None # ULONG
- def __repr__(self):
- return (
- "startGlyphID: %s, endGlyphID: %s, svgDocOffset: %s, svgDocLength: %s"
- % (self.startGlyphID, self.endGlyphID, self.svgDocOffset, self.svgDocLength)
- )
- @dataclass
- class SVGDocument(Sequence):
- data: str
- startGlyphID: int
- endGlyphID: int
- compressed: bool = False
- # Previously, the SVG table's docList attribute contained a lists of 3 items:
- # [doc, startGlyphID, endGlyphID]; later, we added a `compressed` attribute.
- # For backward compatibility with code that depends of them being sequences of
- # fixed length=3, we subclass the Sequence abstract base class and pretend only
- # the first three items are present. 'compressed' is only accessible via named
- # attribute lookup like regular dataclasses: i.e. `doc.compressed`, not `doc[3]`
- def __getitem__(self, index):
- return astuple(self)[:3][index]
- def __len__(self):
- return 3
|