This is based on bungeman's https://github.com/fonttools/fonttools/pull/2627 Previously, an entire `SVG ` table would be marked as compressed if any of the decoded SVG documents in it were compressed. Then on encoding all SVG documents would be considered for compression. The XML format had no means to indicate if compression was desired. Instead, mark each svgDoc with its compression status. When decoding mark the svgDoc as compressed if the data was compressed. When encoding try to compress the svgDoc if it is marked as compressed. In the XML format the data itself is always uncompressed, but allow an optional `compressed` boolean attribute (defaults to false) to indicate the svgDoc should be compressed when encoded. We also try to make sure that older code that relies on docList containing sequences of three items (doc, startGID, endGID) will continue to work without modification.
196 lines
5.8 KiB
Python
196 lines
5.8 KiB
Python
"""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):
|
|
|
|
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):
|
|
docIndexEntry, data2 = sstruct.unpack2(doc_index_entry_format_0, data2, 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()
|
|
with gzip.GzipFile(None, "w", fileobj=bytesIO) 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
|