xref: /aosp_15_r20/external/fonttools/Lib/fontTools/ttLib/tables/S_V_G_.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1"""Compiles/decompiles SVG table.
2
3https://docs.microsoft.com/en-us/typography/opentype/spec/svg
4
5The XML format is:
6
7.. code-block:: xml
8
9	<SVG>
10		<svgDoc endGlyphID="1" startGlyphID="1">
11			<![CDATA[ <complete SVG doc> ]]
12		</svgDoc>
13	...
14		<svgDoc endGlyphID="n" startGlyphID="m">
15			<![CDATA[ <complete SVG doc> ]]
16		</svgDoc>
17	</SVG>
18"""
19
20from fontTools.misc.textTools import bytesjoin, safeEval, strjoin, tobytes, tostr
21from fontTools.misc import sstruct
22from . import DefaultTable
23from collections.abc import Sequence
24from dataclasses import dataclass, astuple
25from io import BytesIO
26import struct
27import logging
28
29
30log = logging.getLogger(__name__)
31
32
33SVG_format_0 = """
34	>   # big endian
35	version:                  H
36	offsetToSVGDocIndex:      L
37	reserved:                 L
38"""
39
40SVG_format_0Size = sstruct.calcsize(SVG_format_0)
41
42doc_index_entry_format_0 = """
43	>   # big endian
44	startGlyphID:             H
45	endGlyphID:               H
46	svgDocOffset:             L
47	svgDocLength:             L
48"""
49
50doc_index_entry_format_0Size = sstruct.calcsize(doc_index_entry_format_0)
51
52
53class table_S_V_G_(DefaultTable.DefaultTable):
54    def decompile(self, data, ttFont):
55        self.docList = []
56        # Version 0 is the standardized version of the table; and current.
57        # https://www.microsoft.com/typography/otspec/svg.htm
58        sstruct.unpack(SVG_format_0, data[:SVG_format_0Size], self)
59        if self.version != 0:
60            log.warning(
61                "Unknown SVG table version '%s'. Decompiling as version 0.",
62                self.version,
63            )
64        # read in SVG Documents Index
65        # data starts with the first entry of the entry list.
66        pos = subTableStart = self.offsetToSVGDocIndex
67        self.numEntries = struct.unpack(">H", data[pos : pos + 2])[0]
68        pos += 2
69        if self.numEntries > 0:
70            data2 = data[pos:]
71            entries = []
72            for i in range(self.numEntries):
73                record_data = data2[
74                    i
75                    * doc_index_entry_format_0Size : (i + 1)
76                    * doc_index_entry_format_0Size
77                ]
78                docIndexEntry = sstruct.unpack(
79                    doc_index_entry_format_0, record_data, DocumentIndexEntry()
80                )
81                entries.append(docIndexEntry)
82
83            for entry in entries:
84                start = entry.svgDocOffset + subTableStart
85                end = start + entry.svgDocLength
86                doc = data[start:end]
87                compressed = False
88                if doc.startswith(b"\x1f\x8b"):
89                    import gzip
90
91                    bytesIO = BytesIO(doc)
92                    with gzip.GzipFile(None, "r", fileobj=bytesIO) as gunzipper:
93                        doc = gunzipper.read()
94                    del bytesIO
95                    compressed = True
96                doc = tostr(doc, "utf_8")
97                self.docList.append(
98                    SVGDocument(doc, entry.startGlyphID, entry.endGlyphID, compressed)
99                )
100
101    def compile(self, ttFont):
102        version = 0
103        offsetToSVGDocIndex = (
104            SVG_format_0Size  # I start the SVGDocIndex right after the header.
105        )
106        # get SGVDoc info.
107        docList = []
108        entryList = []
109        numEntries = len(self.docList)
110        datum = struct.pack(">H", numEntries)
111        entryList.append(datum)
112        curOffset = len(datum) + doc_index_entry_format_0Size * numEntries
113        seenDocs = {}
114        allCompressed = getattr(self, "compressed", False)
115        for i, doc in enumerate(self.docList):
116            if isinstance(doc, (list, tuple)):
117                doc = SVGDocument(*doc)
118                self.docList[i] = doc
119            docBytes = tobytes(doc.data, encoding="utf_8")
120            if (allCompressed or doc.compressed) and not docBytes.startswith(
121                b"\x1f\x8b"
122            ):
123                import gzip
124
125                bytesIO = BytesIO()
126                # mtime=0 strips the useless timestamp and makes gzip output reproducible;
127                # equivalent to `gzip -n`
128                with gzip.GzipFile(None, "w", fileobj=bytesIO, mtime=0) as gzipper:
129                    gzipper.write(docBytes)
130                gzipped = bytesIO.getvalue()
131                if len(gzipped) < len(docBytes):
132                    docBytes = gzipped
133                del gzipped, bytesIO
134            docLength = len(docBytes)
135            if docBytes in seenDocs:
136                docOffset = seenDocs[docBytes]
137            else:
138                docOffset = curOffset
139                curOffset += docLength
140                seenDocs[docBytes] = docOffset
141                docList.append(docBytes)
142            entry = struct.pack(
143                ">HHLL", doc.startGlyphID, doc.endGlyphID, docOffset, docLength
144            )
145            entryList.append(entry)
146        entryList.extend(docList)
147        svgDocData = bytesjoin(entryList)
148
149        reserved = 0
150        header = struct.pack(">HLL", version, offsetToSVGDocIndex, reserved)
151        data = [header, svgDocData]
152        data = bytesjoin(data)
153        return data
154
155    def toXML(self, writer, ttFont):
156        for i, doc in enumerate(self.docList):
157            if isinstance(doc, (list, tuple)):
158                doc = SVGDocument(*doc)
159                self.docList[i] = doc
160            attrs = {"startGlyphID": doc.startGlyphID, "endGlyphID": doc.endGlyphID}
161            if doc.compressed:
162                attrs["compressed"] = 1
163            writer.begintag("svgDoc", **attrs)
164            writer.newline()
165            writer.writecdata(doc.data)
166            writer.newline()
167            writer.endtag("svgDoc")
168            writer.newline()
169
170    def fromXML(self, name, attrs, content, ttFont):
171        if name == "svgDoc":
172            if not hasattr(self, "docList"):
173                self.docList = []
174            doc = strjoin(content)
175            doc = doc.strip()
176            startGID = int(attrs["startGlyphID"])
177            endGID = int(attrs["endGlyphID"])
178            compressed = bool(safeEval(attrs.get("compressed", "0")))
179            self.docList.append(SVGDocument(doc, startGID, endGID, compressed))
180        else:
181            log.warning("Unknown %s %s", name, content)
182
183
184class DocumentIndexEntry(object):
185    def __init__(self):
186        self.startGlyphID = None  # USHORT
187        self.endGlyphID = None  # USHORT
188        self.svgDocOffset = None  # ULONG
189        self.svgDocLength = None  # ULONG
190
191    def __repr__(self):
192        return (
193            "startGlyphID: %s, endGlyphID: %s, svgDocOffset: %s, svgDocLength: %s"
194            % (self.startGlyphID, self.endGlyphID, self.svgDocOffset, self.svgDocLength)
195        )
196
197
198@dataclass
199class SVGDocument(Sequence):
200    data: str
201    startGlyphID: int
202    endGlyphID: int
203    compressed: bool = False
204
205    # Previously, the SVG table's docList attribute contained a lists of 3 items:
206    # [doc, startGlyphID, endGlyphID]; later, we added a `compressed` attribute.
207    # For backward compatibility with code that depends of them being sequences of
208    # fixed length=3, we subclass the Sequence abstract base class and pretend only
209    # the first three items are present. 'compressed' is only accessible via named
210    # attribute lookup like regular dataclasses: i.e. `doc.compressed`, not `doc[3]`
211    def __getitem__(self, index):
212        return astuple(self)[:3][index]
213
214    def __len__(self):
215        return 3
216