Skip to content

Commit ebc0525

Browse files
committed
add models module
1 parent ed86897 commit ebc0525

File tree

1 file changed

+301
-0
lines changed

1 file changed

+301
-0
lines changed

src/slice/models.py

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
# This file is part of Slice.
2+
#
3+
# Slice is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# Slice is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with Slice. If not, see <https://www.gnu.org/licenses/>.
15+
16+
from fontTools.ttLib import TTFont
17+
from PyQt5.QtCore import QAbstractTableModel, Qt
18+
19+
20+
class SliceBaseTableModel(QAbstractTableModel):
21+
def __init__(self, *args):
22+
QAbstractTableModel.__init__(self, *args)
23+
self._data = [[]]
24+
self._v_header = []
25+
26+
def data(self, index, role):
27+
if role in (Qt.DisplayRole, Qt.EditRole):
28+
return self._data[index.row()][index.column()]
29+
30+
def setData(self, index, value, role):
31+
if index.isValid() and role == Qt.EditRole:
32+
self._data[index.row()][index.column()] = value
33+
self.dataChanged.emit(index, index, [role])
34+
return True
35+
else:
36+
return False
37+
38+
def rowCount(self, index):
39+
return len(self._data)
40+
41+
def columnCount(self, index):
42+
return len(self._data[0])
43+
44+
def get_data(self):
45+
return self._data
46+
47+
48+
class FontNameModel(SliceBaseTableModel):
49+
def __init__(self, *args):
50+
SliceBaseTableModel.__init__(self, *args)
51+
self.font_version = None
52+
self.font_family_name = None
53+
self._data = [
54+
[""], # nameID 1 (index 0)
55+
[""], # nameID 2 (index 1)
56+
[""], # nameID 3 (index 2)
57+
[""], # nameID 4 (index 3)
58+
[""], # nameID 6 (index 4)
59+
[""], # nameID 16 (index 5)
60+
[""], # nameID 17 (index 6)
61+
[""], # nameID 21 (index 7)
62+
[""], # nameID 22 (index 8)
63+
]
64+
self._v_header = [
65+
"01 Family",
66+
"02 Subfamily",
67+
"03 Unique",
68+
"04 Full",
69+
"06 Postscript",
70+
"16 Typo Family",
71+
"17 Typo Subfamily",
72+
"21 WWS Family",
73+
"22 WWS Subfamily",
74+
]
75+
76+
def load_font(self, font_model):
77+
ttfont = TTFont(font_model.fontpath)
78+
name = ttfont["name"]
79+
plat_id = 3
80+
plat_enc_id = 1
81+
lang_id = 1033
82+
for record in name.names:
83+
if (
84+
record.nameID == 1
85+
and record.platformID == plat_id
86+
and record.platEncID == plat_enc_id
87+
and record.langID == lang_id
88+
):
89+
self._data[0][0] = record.toUnicode()
90+
self.font_family_name = record.toUnicode()
91+
elif (
92+
record.nameID == 2
93+
and record.platformID == plat_id
94+
and record.platEncID == plat_enc_id
95+
and record.langID == lang_id
96+
):
97+
self._data[1][0] = record.toUnicode()
98+
elif (
99+
record.nameID == 3
100+
and record.platformID == plat_id
101+
and record.platEncID == plat_enc_id
102+
and record.langID == lang_id
103+
):
104+
self._data[2][0] = record.toUnicode()
105+
elif (
106+
record.nameID == 4
107+
and record.platformID == plat_id
108+
and record.platEncID == plat_enc_id
109+
and record.langID == lang_id
110+
):
111+
self._data[3][0] = record.toUnicode()
112+
elif (
113+
record.nameID == 5
114+
and record.platformID == plat_id
115+
and record.platEncID == plat_enc_id
116+
and record.langID == lang_id
117+
):
118+
self.font_version = record.toUnicode()
119+
elif (
120+
record.nameID == 6
121+
and record.platformID == plat_id
122+
and record.platEncID == plat_enc_id
123+
and record.langID == lang_id
124+
):
125+
self._data[4][0] = record.toUnicode()
126+
127+
self.layoutChanged.emit()
128+
return True
129+
130+
def headerData(self, section, orientation, role):
131+
if orientation == Qt.Vertical and role == Qt.DisplayRole:
132+
return self._v_header[section]
133+
elif orientation == Qt.Horizontal and role == Qt.DisplayRole:
134+
return "Instance Values"
135+
136+
def flags(self, index):
137+
# all indices are editable in this table
138+
return super().flags(index) | Qt.ItemIsEditable
139+
140+
def get_version(self):
141+
return self.font_version.split(";")[0]
142+
143+
def get_family_name(self):
144+
return self.font_family_name
145+
146+
def get_instance_data(self):
147+
return {
148+
"nameID1": self._data[0][0],
149+
"nameID2": self._data[1][0],
150+
"nameID3": self._data[2][0],
151+
"nameID4": self._data[3][0],
152+
"nameID6": self._data[4][0],
153+
"nameID16": self._data[5][0],
154+
"nameID17": self._data[6][0],
155+
"nameID21": self._data[7][0],
156+
"nameID22": self._data[8][0],
157+
}
158+
159+
160+
class DesignAxisModel(SliceBaseTableModel):
161+
def __init__(self, *args):
162+
SliceBaseTableModel.__init__(self, *args)
163+
self.fvar_axes = {}
164+
self.ordered_axis_tags = []
165+
self._data = [
166+
["", ""],
167+
["", ""],
168+
["", ""],
169+
["", ""],
170+
["", ""],
171+
]
172+
# temp fields on load
173+
self._v_header = [
174+
"Axis 1",
175+
"Axis 2",
176+
"Axis 3",
177+
"Axis 4",
178+
"Axis 5",
179+
]
180+
self._h_header = ["(Min, Max) [Default]", "Instance Values"]
181+
182+
def data(self, index, role):
183+
if role in (Qt.DisplayRole, Qt.EditRole):
184+
return self._data[index.row()][index.column()]
185+
186+
if role == Qt.TextAlignmentRole:
187+
return Qt.AlignCenter
188+
189+
def headerData(self, section, orientation, role):
190+
if orientation == Qt.Vertical and role == Qt.DisplayRole:
191+
return self._v_header[section]
192+
elif orientation == Qt.Horizontal and role == Qt.DisplayRole:
193+
return self._h_header[section]
194+
195+
def flags(self, index):
196+
# column 0 (axis value range and default) is set
197+
# to non-editable
198+
if index.column() == 0:
199+
return super().flags(index) | Qt.ItemIsSelectable
200+
# column 1 (instance value) is set to editable
201+
else:
202+
return super().flags(index) | Qt.ItemIsEditable
203+
204+
def load_font(self, font_model):
205+
ttfont = TTFont(font_model.fontpath)
206+
fvar = ttfont["fvar"]
207+
# used to re-define the model data on each
208+
# new font load
209+
new_data = []
210+
# clear the axis tag list attribute
211+
self.ordered_axis_tags = []
212+
for axis in fvar.axes:
213+
self.ordered_axis_tags.append(axis.axisTag)
214+
self.fvar_axes[axis.axisTag] = [
215+
axis.minValue,
216+
axis.defaultValue,
217+
axis.maxValue,
218+
]
219+
new_data.append(
220+
[f"({axis.minValue}, {axis.maxValue}) [{axis.defaultValue}]", ""]
221+
)
222+
223+
# set header with ordered axis tags
224+
self._v_header = self.ordered_axis_tags
225+
self._data = new_data
226+
self.layoutChanged.emit()
227+
return True
228+
229+
def get_number_of_axes(self):
230+
return len(self.ordered_axis_tags)
231+
232+
def get_instance_data(self):
233+
instance_data = {}
234+
# return a dictionary with map "axis_tag": "value"
235+
# value is cast to a float from a str
236+
for x, axistag in enumerate(self._v_header):
237+
axis_value = self._data[x][1]
238+
# if user did not define the axis value, then
239+
# use the default axis value
240+
if axis_value == "":
241+
instance_data[axistag] = float(self.get_default_axis_value(axistag))
242+
else:
243+
instance_data[axistag] = float(self._data[x][1])
244+
245+
return instance_data
246+
247+
def get_default_axis_value(self, axistag):
248+
if axistag in self.fvar_axes:
249+
# field that contains the default axis value
250+
return self.fvar_axes[axistag][1]
251+
else:
252+
return None
253+
254+
255+
class FontBitFlagModel(object):
256+
def __init__(self, os2_dict, head_dict):
257+
self._os2_dict = os2_dict
258+
self._head_dict = head_dict
259+
260+
def _set_bit(self, int_type, offset):
261+
mask = 1 << offset
262+
return int_type | mask
263+
264+
def _clear_bit(self, int_type, offset):
265+
mask = ~(1 << offset)
266+
return int_type & mask
267+
268+
def _get_bit_offset_from_key(self, bitkey):
269+
# dict key formatted as e.g., `bit0`
270+
# grab the integer portion and cast to int
271+
return int(bitkey.replace("bit", ""))
272+
273+
def _edit_bits(self, integer, bit_dict):
274+
for bitkey, is_set in bit_dict.items():
275+
offset = self._get_bit_offset_from_key(bitkey)
276+
if is_set:
277+
integer = self._set_bit(integer, offset)
278+
else:
279+
integer = self._clear_bit(integer, offset)
280+
return integer
281+
282+
def get_os2_instance_data(self):
283+
return self._os2_dict
284+
285+
def get_head_instance_data(self):
286+
return self._head_dict
287+
288+
def edit_os2_fsselection_bits(self, integer):
289+
return self._edit_bits(integer, self._os2_dict)
290+
291+
def edit_head_macstyle_bits(self, integer):
292+
return self._edit_bits(integer, self._head_dict)
293+
294+
295+
class FontModel(object):
296+
def __init__(self, fontpath):
297+
self.fontpath = fontpath
298+
299+
def is_variable_font(self):
300+
"""Check for fvar table to validate that a TTFont is a variable font"""
301+
return "fvar" in TTFont(self.fontpath)

0 commit comments

Comments
 (0)