aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/ttLib/tables/S_V_G_.py
blob: 49e98d034b290172ab00dc0704650ed5d80f8cd9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
"""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()
				# 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