1from fontTools.voltLib.error import VoltLibError 2from typing import NamedTuple 3 4 5class Pos(NamedTuple): 6 adv: int 7 dx: int 8 dy: int 9 adv_adjust_by: dict 10 dx_adjust_by: dict 11 dy_adjust_by: dict 12 13 def __str__(self): 14 res = ' POS' 15 for attr in ('adv', 'dx', 'dy'): 16 value = getattr(self, attr) 17 if value is not None: 18 res += f' {attr.upper()} {value}' 19 adjust_by = getattr(self, f'{attr}_adjust_by', {}) 20 for size, adjustment in adjust_by.items(): 21 res += f' ADJUST_BY {adjustment} AT {size}' 22 res += ' END_POS' 23 return res 24 25 26class Element(object): 27 def __init__(self, location=None): 28 self.location = location 29 30 def build(self, builder): 31 pass 32 33 def __str__(self): 34 raise NotImplementedError 35 36 37class Statement(Element): 38 pass 39 40 41class Expression(Element): 42 pass 43 44 45class VoltFile(Statement): 46 def __init__(self): 47 Statement.__init__(self, location=None) 48 self.statements = [] 49 50 def build(self, builder): 51 for s in self.statements: 52 s.build(builder) 53 54 def __str__(self): 55 return '\n' + '\n'.join(str(s) for s in self.statements) + ' END\n' 56 57 58class GlyphDefinition(Statement): 59 def __init__(self, name, gid, gunicode, gtype, components, location=None): 60 Statement.__init__(self, location) 61 self.name = name 62 self.id = gid 63 self.unicode = gunicode 64 self.type = gtype 65 self.components = components 66 67 def __str__(self): 68 res = f'DEF_GLYPH "{self.name}" ID {self.id}' 69 if self.unicode is not None: 70 if len(self.unicode) > 1: 71 unicodes = ','.join(f'U+{u:04X}' for u in self.unicode) 72 res += f' UNICODEVALUES "{unicodes}"' 73 else: 74 res += f' UNICODE {self.unicode[0]}' 75 if self.type is not None: 76 res += f' TYPE {self.type}' 77 if self.components is not None: 78 res += f' COMPONENTS {self.components}' 79 res += ' END_GLYPH' 80 return res 81 82 83class GroupDefinition(Statement): 84 def __init__(self, name, enum, location=None): 85 Statement.__init__(self, location) 86 self.name = name 87 self.enum = enum 88 self.glyphs_ = None 89 90 def glyphSet(self, groups=None): 91 if groups is not None and self.name in groups: 92 raise VoltLibError( 93 'Group "%s" contains itself.' % (self.name), 94 self.location) 95 if self.glyphs_ is None: 96 if groups is None: 97 groups = set({self.name}) 98 else: 99 groups.add(self.name) 100 self.glyphs_ = self.enum.glyphSet(groups) 101 return self.glyphs_ 102 103 def __str__(self): 104 enum = self.enum and str(self.enum) or '' 105 return f'DEF_GROUP "{self.name}"\n{enum}\nEND_GROUP' 106 107 108class GlyphName(Expression): 109 """A single glyph name, such as cedilla.""" 110 def __init__(self, glyph, location=None): 111 Expression.__init__(self, location) 112 self.glyph = glyph 113 114 def glyphSet(self): 115 return (self.glyph,) 116 117 def __str__(self): 118 return f' GLYPH "{self.glyph}"' 119 120 121class Enum(Expression): 122 """An enum""" 123 def __init__(self, enum, location=None): 124 Expression.__init__(self, location) 125 self.enum = enum 126 127 def __iter__(self): 128 for e in self.glyphSet(): 129 yield e 130 131 def glyphSet(self, groups=None): 132 glyphs = [] 133 for element in self.enum: 134 if isinstance(element, (GroupName, Enum)): 135 glyphs.extend(element.glyphSet(groups)) 136 else: 137 glyphs.extend(element.glyphSet()) 138 return tuple(glyphs) 139 140 def __str__(self): 141 enum = ''.join(str(e) for e in self.enum) 142 return f' ENUM{enum} END_ENUM' 143 144 145class GroupName(Expression): 146 """A glyph group""" 147 def __init__(self, group, parser, location=None): 148 Expression.__init__(self, location) 149 self.group = group 150 self.parser_ = parser 151 152 def glyphSet(self, groups=None): 153 group = self.parser_.resolve_group(self.group) 154 if group is not None: 155 self.glyphs_ = group.glyphSet(groups) 156 return self.glyphs_ 157 else: 158 raise VoltLibError( 159 'Group "%s" is used but undefined.' % (self.group), 160 self.location) 161 162 def __str__(self): 163 return f' GROUP "{self.group}"' 164 165 166class Range(Expression): 167 """A glyph range""" 168 def __init__(self, start, end, parser, location=None): 169 Expression.__init__(self, location) 170 self.start = start 171 self.end = end 172 self.parser = parser 173 174 def glyphSet(self): 175 return tuple(self.parser.glyph_range(self.start, self.end)) 176 177 def __str__(self): 178 return f' RANGE "{self.start}" TO "{self.end}"' 179 180 181class ScriptDefinition(Statement): 182 def __init__(self, name, tag, langs, location=None): 183 Statement.__init__(self, location) 184 self.name = name 185 self.tag = tag 186 self.langs = langs 187 188 def __str__(self): 189 res = 'DEF_SCRIPT' 190 if self.name is not None: 191 res += f' NAME "{self.name}"' 192 res += f' TAG "{self.tag}"\n\n' 193 for lang in self.langs: 194 res += f'{lang}' 195 res += 'END_SCRIPT' 196 return res 197 198 199class LangSysDefinition(Statement): 200 def __init__(self, name, tag, features, location=None): 201 Statement.__init__(self, location) 202 self.name = name 203 self.tag = tag 204 self.features = features 205 206 def __str__(self): 207 res = 'DEF_LANGSYS' 208 if self.name is not None: 209 res += f' NAME "{self.name}"' 210 res += f' TAG "{self.tag}"\n\n' 211 for feature in self.features: 212 res += f'{feature}' 213 res += 'END_LANGSYS\n' 214 return res 215 216 217class FeatureDefinition(Statement): 218 def __init__(self, name, tag, lookups, location=None): 219 Statement.__init__(self, location) 220 self.name = name 221 self.tag = tag 222 self.lookups = lookups 223 224 def __str__(self): 225 res = f'DEF_FEATURE NAME "{self.name}" TAG "{self.tag}"\n' 226 res += ' ' + ' '.join(f'LOOKUP "{l}"' for l in self.lookups) + '\n' 227 res += 'END_FEATURE\n' 228 return res 229 230 231class LookupDefinition(Statement): 232 def __init__(self, name, process_base, process_marks, mark_glyph_set, 233 direction, reversal, comments, context, sub, pos, 234 location=None): 235 Statement.__init__(self, location) 236 self.name = name 237 self.process_base = process_base 238 self.process_marks = process_marks 239 self.mark_glyph_set = mark_glyph_set 240 self.direction = direction 241 self.reversal = reversal 242 self.comments = comments 243 self.context = context 244 self.sub = sub 245 self.pos = pos 246 247 def __str__(self): 248 res = f'DEF_LOOKUP "{self.name}"' 249 res += f' {self.process_base and "PROCESS_BASE" or "SKIP_BASE"}' 250 if self.process_marks: 251 res += ' PROCESS_MARKS ' 252 if self.mark_glyph_set: 253 res += f'MARK_GLYPH_SET "{self.mark_glyph_set}"' 254 elif isinstance(self.process_marks, str): 255 res += f'"{self.process_marks}"' 256 else: 257 res += 'ALL' 258 else: 259 res += ' SKIP_MARKS' 260 if self.direction is not None: 261 res += f' DIRECTION {self.direction}' 262 if self.reversal: 263 res += ' REVERSAL' 264 if self.comments is not None: 265 comments = self.comments.replace('\n', r'\n') 266 res += f'\nCOMMENTS "{comments}"' 267 if self.context: 268 res += '\n' + '\n'.join(str(c) for c in self.context) 269 else: 270 res += '\nIN_CONTEXT\nEND_CONTEXT' 271 if self.sub: 272 res += f'\n{self.sub}' 273 if self.pos: 274 res += f'\n{self.pos}' 275 return res 276 277 278class SubstitutionDefinition(Statement): 279 def __init__(self, mapping, location=None): 280 Statement.__init__(self, location) 281 self.mapping = mapping 282 283 def __str__(self): 284 res = 'AS_SUBSTITUTION\n' 285 for src, dst in self.mapping.items(): 286 src = ''.join(str(s) for s in src) 287 dst = ''.join(str(d) for d in dst) 288 res += f'SUB{src}\nWITH{dst}\nEND_SUB\n' 289 res += 'END_SUBSTITUTION' 290 return res 291 292 293class SubstitutionSingleDefinition(SubstitutionDefinition): 294 pass 295 296 297class SubstitutionMultipleDefinition(SubstitutionDefinition): 298 pass 299 300 301class SubstitutionLigatureDefinition(SubstitutionDefinition): 302 pass 303 304 305class SubstitutionReverseChainingSingleDefinition(SubstitutionDefinition): 306 pass 307 308 309class PositionAttachDefinition(Statement): 310 def __init__(self, coverage, coverage_to, location=None): 311 Statement.__init__(self, location) 312 self.coverage = coverage 313 self.coverage_to = coverage_to 314 315 def __str__(self): 316 coverage = ''.join(str(c) for c in self.coverage) 317 res = f'AS_POSITION\nATTACH{coverage}\nTO' 318 for coverage, anchor in self.coverage_to: 319 coverage = ''.join(str(c) for c in coverage) 320 res += f'{coverage} AT ANCHOR "{anchor}"' 321 res += '\nEND_ATTACH\nEND_POSITION' 322 return res 323 324 325class PositionAttachCursiveDefinition(Statement): 326 def __init__(self, coverages_exit, coverages_enter, location=None): 327 Statement.__init__(self, location) 328 self.coverages_exit = coverages_exit 329 self.coverages_enter = coverages_enter 330 331 def __str__(self): 332 res = 'AS_POSITION\nATTACH_CURSIVE' 333 for coverage in self.coverages_exit: 334 coverage = ''.join(str(c) for c in coverage) 335 res += f'\nEXIT {coverage}' 336 for coverage in self.coverages_enter: 337 coverage = ''.join(str(c) for c in coverage) 338 res += f'\nENTER {coverage}' 339 res += '\nEND_ATTACH\nEND_POSITION' 340 return res 341 342 343class PositionAdjustPairDefinition(Statement): 344 def __init__(self, coverages_1, coverages_2, adjust_pair, location=None): 345 Statement.__init__(self, location) 346 self.coverages_1 = coverages_1 347 self.coverages_2 = coverages_2 348 self.adjust_pair = adjust_pair 349 350 def __str__(self): 351 res = 'AS_POSITION\nADJUST_PAIR\n' 352 for coverage in self.coverages_1: 353 coverage = ' '.join(str(c) for c in coverage) 354 res += f' FIRST {coverage}' 355 res += '\n' 356 for coverage in self.coverages_2: 357 coverage = ' '.join(str(c) for c in coverage) 358 res += f' SECOND {coverage}' 359 res += '\n' 360 for (id_1, id_2), (pos_1, pos_2) in self.adjust_pair.items(): 361 res += f' {id_1} {id_2} BY{pos_1}{pos_2}\n' 362 res += '\nEND_ADJUST\nEND_POSITION' 363 return res 364 365 366class PositionAdjustSingleDefinition(Statement): 367 def __init__(self, adjust_single, location=None): 368 Statement.__init__(self, location) 369 self.adjust_single = adjust_single 370 371 def __str__(self): 372 res = 'AS_POSITION\nADJUST_SINGLE' 373 for coverage, pos in self.adjust_single: 374 coverage = ''.join(str(c) for c in coverage) 375 res += f'{coverage} BY{pos}' 376 res += '\nEND_ADJUST\nEND_POSITION' 377 return res 378 379 380 381class ContextDefinition(Statement): 382 def __init__(self, ex_or_in, left=None, right=None, location=None): 383 Statement.__init__(self, location) 384 self.ex_or_in = ex_or_in 385 self.left = left if left is not None else [] 386 self.right = right if right is not None else [] 387 388 def __str__(self): 389 res = self.ex_or_in + '\n' 390 for coverage in self.left: 391 coverage = ''.join(str(c) for c in coverage) 392 res += f' LEFT{coverage}\n' 393 for coverage in self.right: 394 coverage = ''.join(str(c) for c in coverage) 395 res += f' RIGHT{coverage}\n' 396 res += 'END_CONTEXT' 397 return res 398 399 400class AnchorDefinition(Statement): 401 def __init__(self, name, gid, glyph_name, component, locked, 402 pos, location=None): 403 Statement.__init__(self, location) 404 self.name = name 405 self.gid = gid 406 self.glyph_name = glyph_name 407 self.component = component 408 self.locked = locked 409 self.pos = pos 410 411 def __str__(self): 412 locked = self.locked and ' LOCKED' or '' 413 return (f'DEF_ANCHOR "{self.name}"' 414 f' ON {self.gid}' 415 f' GLYPH {self.glyph_name}' 416 f' COMPONENT {self.component}' 417 f'{locked}' 418 f' AT {self.pos} END_ANCHOR') 419 420 421class SettingDefinition(Statement): 422 def __init__(self, name, value, location=None): 423 Statement.__init__(self, location) 424 self.name = name 425 self.value = value 426 427 def __str__(self): 428 if self.value is True: 429 return f'{self.name}' 430 if isinstance(self.value, (tuple, list)): 431 value = " ".join(str(v) for v in self.value) 432 return f'{self.name} {value}' 433 return f'{self.name} {self.value}' 434