Coverage for /usr/lib/python3/dist-packages/matplotlib/_type1font.py: 19%

401 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1""" 

2A class representing a Type 1 font. 

3 

4This version reads pfa and pfb files and splits them for embedding in 

5pdf files. It also supports SlantFont and ExtendFont transformations, 

6similarly to pdfTeX and friends. There is no support yet for subsetting. 

7 

8Usage:: 

9 

10 font = Type1Font(filename) 

11 clear_part, encrypted_part, finale = font.parts 

12 slanted_font = font.transform({'slant': 0.167}) 

13 extended_font = font.transform({'extend': 1.2}) 

14 

15Sources: 

16 

17* Adobe Technical Note #5040, Supporting Downloadable PostScript 

18 Language Fonts. 

19 

20* Adobe Type 1 Font Format, Adobe Systems Incorporated, third printing, 

21 v1.1, 1993. ISBN 0-201-57044-0. 

22""" 

23 

24import binascii 

25import functools 

26import logging 

27import re 

28import string 

29import struct 

30 

31import numpy as np 

32 

33from matplotlib.cbook import _format_approx 

34from . import _api 

35 

36_log = logging.getLogger(__name__) 

37 

38 

39class _Token: 

40 """ 

41 A token in a PostScript stream. 

42 

43 Attributes 

44 ---------- 

45 pos : int 

46 Position, i.e. offset from the beginning of the data. 

47 raw : str 

48 Raw text of the token. 

49 kind : str 

50 Description of the token (for debugging or testing). 

51 """ 

52 __slots__ = ('pos', 'raw') 

53 kind = '?' 

54 

55 def __init__(self, pos, raw): 

56 _log.debug('type1font._Token %s at %d: %r', self.kind, pos, raw) 

57 self.pos = pos 

58 self.raw = raw 

59 

60 def __str__(self): 

61 return f"<{self.kind} {self.raw} @{self.pos}>" 

62 

63 def endpos(self): 

64 """Position one past the end of the token""" 

65 return self.pos + len(self.raw) 

66 

67 def is_keyword(self, *names): 

68 """Is this a name token with one of the names?""" 

69 return False 

70 

71 def is_slash_name(self): 

72 """Is this a name token that starts with a slash?""" 

73 return False 

74 

75 def is_delim(self): 

76 """Is this a delimiter token?""" 

77 return False 

78 

79 def is_number(self): 

80 """Is this a number token?""" 

81 return False 

82 

83 def value(self): 

84 return self.raw 

85 

86 

87class _NameToken(_Token): 

88 kind = 'name' 

89 

90 def is_slash_name(self): 

91 return self.raw.startswith('/') 

92 

93 def value(self): 

94 return self.raw[1:] 

95 

96 

97class _BooleanToken(_Token): 

98 kind = 'boolean' 

99 

100 def value(self): 

101 return self.raw == 'true' 

102 

103 

104class _KeywordToken(_Token): 

105 kind = 'keyword' 

106 

107 def is_keyword(self, *names): 

108 return self.raw in names 

109 

110 

111class _DelimiterToken(_Token): 

112 kind = 'delimiter' 

113 

114 def is_delim(self): 

115 return True 

116 

117 def opposite(self): 

118 return {'[': ']', ']': '[', 

119 '{': '}', '}': '{', 

120 '<<': '>>', '>>': '<<' 

121 }[self.raw] 

122 

123 

124class _WhitespaceToken(_Token): 

125 kind = 'whitespace' 

126 

127 

128class _StringToken(_Token): 

129 kind = 'string' 

130 _escapes_re = re.compile(r'\\([\\()nrtbf]|[0-7]{1,3})') 

131 _replacements = {'\\': '\\', '(': '(', ')': ')', 'n': '\n', 

132 'r': '\r', 't': '\t', 'b': '\b', 'f': '\f'} 

133 _ws_re = re.compile('[\0\t\r\f\n ]') 

134 

135 @classmethod 

136 def _escape(cls, match): 

137 group = match.group(1) 

138 try: 

139 return cls._replacements[group] 

140 except KeyError: 

141 return chr(int(group, 8)) 

142 

143 @functools.lru_cache() 

144 def value(self): 

145 if self.raw[0] == '(': 

146 return self._escapes_re.sub(self._escape, self.raw[1:-1]) 

147 else: 

148 data = self._ws_re.sub('', self.raw[1:-1]) 

149 if len(data) % 2 == 1: 

150 data += '0' 

151 return binascii.unhexlify(data) 

152 

153 

154class _BinaryToken(_Token): 

155 kind = 'binary' 

156 

157 def value(self): 

158 return self.raw[1:] 

159 

160 

161class _NumberToken(_Token): 

162 kind = 'number' 

163 

164 def is_number(self): 

165 return True 

166 

167 def value(self): 

168 if '.' not in self.raw: 

169 return int(self.raw) 

170 else: 

171 return float(self.raw) 

172 

173 

174def _tokenize(data: bytes, skip_ws: bool): 

175 """ 

176 A generator that produces _Token instances from Type-1 font code. 

177 

178 The consumer of the generator may send an integer to the tokenizer to 

179 indicate that the next token should be _BinaryToken of the given length. 

180 

181 Parameters 

182 ---------- 

183 data : bytes 

184 The data of the font to tokenize. 

185 

186 skip_ws : bool 

187 If true, the generator will drop any _WhitespaceTokens from the output. 

188 """ 

189 

190 text = data.decode('ascii', 'replace') 

191 whitespace_or_comment_re = re.compile(r'[\0\t\r\f\n ]+|%[^\r\n]*') 

192 token_re = re.compile(r'/{0,2}[^]\0\t\r\f\n ()<>{}/%[]+') 

193 instring_re = re.compile(r'[()\\]') 

194 hex_re = re.compile(r'^<[0-9a-fA-F\0\t\r\f\n ]*>$') 

195 oct_re = re.compile(r'[0-7]{1,3}') 

196 pos = 0 

197 next_binary = None 

198 

199 while pos < len(text): 

200 if next_binary is not None: 

201 n = next_binary 

202 next_binary = (yield _BinaryToken(pos, data[pos:pos+n])) 

203 pos += n 

204 continue 

205 match = whitespace_or_comment_re.match(text, pos) 

206 if match: 

207 if not skip_ws: 

208 next_binary = (yield _WhitespaceToken(pos, match.group())) 

209 pos = match.end() 

210 elif text[pos] == '(': 

211 # PostScript string rules: 

212 # - parentheses must be balanced 

213 # - backslashes escape backslashes and parens 

214 # - also codes \n\r\t\b\f and octal escapes are recognized 

215 # - other backslashes do not escape anything 

216 start = pos 

217 pos += 1 

218 depth = 1 

219 while depth: 

220 match = instring_re.search(text, pos) 

221 if match is None: 

222 raise ValueError( 

223 f'Unterminated string starting at {start}') 

224 pos = match.end() 

225 if match.group() == '(': 

226 depth += 1 

227 elif match.group() == ')': 

228 depth -= 1 

229 else: # a backslash 

230 char = text[pos] 

231 if char in r'\()nrtbf': 

232 pos += 1 

233 else: 

234 octal = oct_re.match(text, pos) 

235 if octal: 

236 pos = octal.end() 

237 else: 

238 pass # non-escaping backslash 

239 next_binary = (yield _StringToken(start, text[start:pos])) 

240 elif text[pos:pos + 2] in ('<<', '>>'): 

241 next_binary = (yield _DelimiterToken(pos, text[pos:pos + 2])) 

242 pos += 2 

243 elif text[pos] == '<': 

244 start = pos 

245 try: 

246 pos = text.index('>', pos) + 1 

247 except ValueError as e: 

248 raise ValueError(f'Unterminated hex string starting at {start}' 

249 ) from e 

250 if not hex_re.match(text[start:pos]): 

251 raise ValueError(f'Malformed hex string starting at {start}') 

252 next_binary = (yield _StringToken(pos, text[start:pos])) 

253 else: 

254 match = token_re.match(text, pos) 

255 if match: 

256 raw = match.group() 

257 if raw.startswith('/'): 

258 next_binary = (yield _NameToken(pos, raw)) 

259 elif match.group() in ('true', 'false'): 

260 next_binary = (yield _BooleanToken(pos, raw)) 

261 else: 

262 try: 

263 float(raw) 

264 next_binary = (yield _NumberToken(pos, raw)) 

265 except ValueError: 

266 next_binary = (yield _KeywordToken(pos, raw)) 

267 pos = match.end() 

268 else: 

269 next_binary = (yield _DelimiterToken(pos, text[pos])) 

270 pos += 1 

271 

272 

273class _BalancedExpression(_Token): 

274 pass 

275 

276 

277def _expression(initial, tokens, data): 

278 """ 

279 Consume some number of tokens and return a balanced PostScript expression. 

280 

281 Parameters 

282 ---------- 

283 initial : _Token 

284 The token that triggered parsing a balanced expression. 

285 tokens : iterator of _Token 

286 Following tokens. 

287 data : bytes 

288 Underlying data that the token positions point to. 

289 

290 Returns 

291 ------- 

292 _BalancedExpression 

293 """ 

294 delim_stack = [] 

295 token = initial 

296 while True: 

297 if token.is_delim(): 

298 if token.raw in ('[', '{'): 

299 delim_stack.append(token) 

300 elif token.raw in (']', '}'): 

301 if not delim_stack: 

302 raise RuntimeError(f"unmatched closing token {token}") 

303 match = delim_stack.pop() 

304 if match.raw != token.opposite(): 

305 raise RuntimeError( 

306 f"opening token {match} closed by {token}" 

307 ) 

308 if not delim_stack: 

309 break 

310 else: 

311 raise RuntimeError(f'unknown delimiter {token}') 

312 elif not delim_stack: 

313 break 

314 token = next(tokens) 

315 return _BalancedExpression( 

316 initial.pos, 

317 data[initial.pos:token.endpos()].decode('ascii', 'replace') 

318 ) 

319 

320 

321class Type1Font: 

322 """ 

323 A class representing a Type-1 font, for use by backends. 

324 

325 Attributes 

326 ---------- 

327 parts : tuple 

328 A 3-tuple of the cleartext part, the encrypted part, and the finale of 

329 zeros. 

330 

331 decrypted : bytes 

332 The decrypted form of ``parts[1]``. 

333 

334 prop : dict[str, Any] 

335 A dictionary of font properties. Noteworthy keys include: 

336 

337 - FontName: PostScript name of the font 

338 - Encoding: dict from numeric codes to glyph names 

339 - FontMatrix: bytes object encoding a matrix 

340 - UniqueID: optional font identifier, dropped when modifying the font 

341 - CharStrings: dict from glyph names to byte code 

342 - Subrs: array of byte code subroutines 

343 - OtherSubrs: bytes object encoding some PostScript code 

344 """ 

345 __slots__ = ('parts', 'decrypted', 'prop', '_pos', '_abbr') 

346 # the _pos dict contains (begin, end) indices to parts[0] + decrypted 

347 # so that they can be replaced when transforming the font; 

348 # but since sometimes a definition appears in both parts[0] and decrypted, 

349 # _pos[name] is an array of such pairs 

350 # 

351 # _abbr maps three standard abbreviations to their particular names in 

352 # this font (e.g. 'RD' is named '-|' in some fonts) 

353 

354 def __init__(self, input): 

355 """ 

356 Initialize a Type-1 font. 

357 

358 Parameters 

359 ---------- 

360 input : str or 3-tuple 

361 Either a pfb file name, or a 3-tuple of already-decoded Type-1 

362 font `~.Type1Font.parts`. 

363 """ 

364 if isinstance(input, tuple) and len(input) == 3: 

365 self.parts = input 

366 else: 

367 with open(input, 'rb') as file: 

368 data = self._read(file) 

369 self.parts = self._split(data) 

370 

371 self.decrypted = self._decrypt(self.parts[1], 'eexec') 

372 self._abbr = {'RD': 'RD', 'ND': 'ND', 'NP': 'NP'} 

373 self._parse() 

374 

375 def _read(self, file): 

376 """Read the font from a file, decoding into usable parts.""" 

377 rawdata = file.read() 

378 if not rawdata.startswith(b'\x80'): 

379 return rawdata 

380 

381 data = b'' 

382 while rawdata: 

383 if not rawdata.startswith(b'\x80'): 

384 raise RuntimeError('Broken pfb file (expected byte 128, ' 

385 'got %d)' % rawdata[0]) 

386 type = rawdata[1] 

387 if type in (1, 2): 

388 length, = struct.unpack('<i', rawdata[2:6]) 

389 segment = rawdata[6:6 + length] 

390 rawdata = rawdata[6 + length:] 

391 

392 if type == 1: # ASCII text: include verbatim 

393 data += segment 

394 elif type == 2: # binary data: encode in hexadecimal 

395 data += binascii.hexlify(segment) 

396 elif type == 3: # end of file 

397 break 

398 else: 

399 raise RuntimeError('Unknown segment type %d in pfb file' % 

400 type) 

401 

402 return data 

403 

404 def _split(self, data): 

405 """ 

406 Split the Type 1 font into its three main parts. 

407 

408 The three parts are: (1) the cleartext part, which ends in a 

409 eexec operator; (2) the encrypted part; (3) the fixed part, 

410 which contains 512 ASCII zeros possibly divided on various 

411 lines, a cleartomark operator, and possibly something else. 

412 """ 

413 

414 # Cleartext part: just find the eexec and skip whitespace 

415 idx = data.index(b'eexec') 

416 idx += len(b'eexec') 

417 while data[idx] in b' \t\r\n': 

418 idx += 1 

419 len1 = idx 

420 

421 # Encrypted part: find the cleartomark operator and count 

422 # zeros backward 

423 idx = data.rindex(b'cleartomark') - 1 

424 zeros = 512 

425 while zeros and data[idx] in b'0' or data[idx] in b'\r\n': 

426 if data[idx] in b'0': 

427 zeros -= 1 

428 idx -= 1 

429 if zeros: 

430 # this may have been a problem on old implementations that 

431 # used the zeros as necessary padding 

432 _log.info('Insufficiently many zeros in Type 1 font') 

433 

434 # Convert encrypted part to binary (if we read a pfb file, we may end 

435 # up converting binary to hexadecimal to binary again; but if we read 

436 # a pfa file, this part is already in hex, and I am not quite sure if 

437 # even the pfb format guarantees that it will be in binary). 

438 idx1 = len1 + ((idx - len1 + 2) & ~1) # ensure an even number of bytes 

439 binary = binascii.unhexlify(data[len1:idx1]) 

440 

441 return data[:len1], binary, data[idx+1:] 

442 

443 @staticmethod 

444 def _decrypt(ciphertext, key, ndiscard=4): 

445 """ 

446 Decrypt ciphertext using the Type-1 font algorithm. 

447 

448 The algorithm is described in Adobe's "Adobe Type 1 Font Format". 

449 The key argument can be an integer, or one of the strings 

450 'eexec' and 'charstring', which map to the key specified for the 

451 corresponding part of Type-1 fonts. 

452 

453 The ndiscard argument should be an integer, usually 4. 

454 That number of bytes is discarded from the beginning of plaintext. 

455 """ 

456 

457 key = _api.check_getitem({'eexec': 55665, 'charstring': 4330}, key=key) 

458 plaintext = [] 

459 for byte in ciphertext: 

460 plaintext.append(byte ^ (key >> 8)) 

461 key = ((key+byte) * 52845 + 22719) & 0xffff 

462 

463 return bytes(plaintext[ndiscard:]) 

464 

465 @staticmethod 

466 def _encrypt(plaintext, key, ndiscard=4): 

467 """ 

468 Encrypt plaintext using the Type-1 font algorithm. 

469 

470 The algorithm is described in Adobe's "Adobe Type 1 Font Format". 

471 The key argument can be an integer, or one of the strings 

472 'eexec' and 'charstring', which map to the key specified for the 

473 corresponding part of Type-1 fonts. 

474 

475 The ndiscard argument should be an integer, usually 4. That 

476 number of bytes is prepended to the plaintext before encryption. 

477 This function prepends NUL bytes for reproducibility, even though 

478 the original algorithm uses random bytes, presumably to avoid 

479 cryptanalysis. 

480 """ 

481 

482 key = _api.check_getitem({'eexec': 55665, 'charstring': 4330}, key=key) 

483 ciphertext = [] 

484 for byte in b'\0' * ndiscard + plaintext: 

485 c = byte ^ (key >> 8) 

486 ciphertext.append(c) 

487 key = ((key + c) * 52845 + 22719) & 0xffff 

488 

489 return bytes(ciphertext) 

490 

491 def _parse(self): 

492 """ 

493 Find the values of various font properties. This limited kind 

494 of parsing is described in Chapter 10 "Adobe Type Manager 

495 Compatibility" of the Type-1 spec. 

496 """ 

497 # Start with reasonable defaults 

498 prop = {'Weight': 'Regular', 'ItalicAngle': 0.0, 'isFixedPitch': False, 

499 'UnderlinePosition': -100, 'UnderlineThickness': 50} 

500 pos = {} 

501 data = self.parts[0] + self.decrypted 

502 

503 source = _tokenize(data, True) 

504 while True: 

505 # See if there is a key to be assigned a value 

506 # e.g. /FontName in /FontName /Helvetica def 

507 try: 

508 token = next(source) 

509 except StopIteration: 

510 break 

511 if token.is_delim(): 

512 # skip over this - we want top-level keys only 

513 _expression(token, source, data) 

514 if token.is_slash_name(): 

515 key = token.value() 

516 keypos = token.pos 

517 else: 

518 continue 

519 

520 # Some values need special parsing 

521 if key in ('Subrs', 'CharStrings', 'Encoding', 'OtherSubrs'): 

522 prop[key], endpos = { 

523 'Subrs': self._parse_subrs, 

524 'CharStrings': self._parse_charstrings, 

525 'Encoding': self._parse_encoding, 

526 'OtherSubrs': self._parse_othersubrs 

527 }[key](source, data) 

528 pos.setdefault(key, []).append((keypos, endpos)) 

529 continue 

530 

531 try: 

532 token = next(source) 

533 except StopIteration: 

534 break 

535 

536 if isinstance(token, _KeywordToken): 

537 # constructs like 

538 # FontDirectory /Helvetica known {...} {...} ifelse 

539 # mean the key was not really a key 

540 continue 

541 

542 if token.is_delim(): 

543 value = _expression(token, source, data).raw 

544 else: 

545 value = token.value() 

546 

547 # look for a 'def' possibly preceded by access modifiers 

548 try: 

549 kw = next( 

550 kw for kw in source 

551 if not kw.is_keyword('readonly', 'noaccess', 'executeonly') 

552 ) 

553 except StopIteration: 

554 break 

555 

556 # sometimes noaccess def and readonly def are abbreviated 

557 if kw.is_keyword('def', self._abbr['ND'], self._abbr['NP']): 

558 prop[key] = value 

559 pos.setdefault(key, []).append((keypos, kw.endpos())) 

560 

561 # detect the standard abbreviations 

562 if value == '{noaccess def}': 

563 self._abbr['ND'] = key 

564 elif value == '{noaccess put}': 

565 self._abbr['NP'] = key 

566 elif value == '{string currentfile exch readstring pop}': 

567 self._abbr['RD'] = key 

568 

569 # Fill in the various *Name properties 

570 if 'FontName' not in prop: 

571 prop['FontName'] = (prop.get('FullName') or 

572 prop.get('FamilyName') or 

573 'Unknown') 

574 if 'FullName' not in prop: 

575 prop['FullName'] = prop['FontName'] 

576 if 'FamilyName' not in prop: 

577 extras = ('(?i)([ -](regular|plain|italic|oblique|(semi)?bold|' 

578 '(ultra)?light|extra|condensed))+$') 

579 prop['FamilyName'] = re.sub(extras, '', prop['FullName']) 

580 # Decrypt the encrypted parts 

581 ndiscard = prop.get('lenIV', 4) 

582 cs = prop['CharStrings'] 

583 for key, value in cs.items(): 

584 cs[key] = self._decrypt(value, 'charstring', ndiscard) 

585 if 'Subrs' in prop: 

586 prop['Subrs'] = [ 

587 self._decrypt(value, 'charstring', ndiscard) 

588 for value in prop['Subrs'] 

589 ] 

590 

591 self.prop = prop 

592 self._pos = pos 

593 

594 def _parse_subrs(self, tokens, _data): 

595 count_token = next(tokens) 

596 if not count_token.is_number(): 

597 raise RuntimeError( 

598 f"Token following /Subrs must be a number, was {count_token}" 

599 ) 

600 count = count_token.value() 

601 array = [None] * count 

602 next(t for t in tokens if t.is_keyword('array')) 

603 for _ in range(count): 

604 next(t for t in tokens if t.is_keyword('dup')) 

605 index_token = next(tokens) 

606 if not index_token.is_number(): 

607 raise RuntimeError( 

608 "Token following dup in Subrs definition must be a " 

609 f"number, was {index_token}" 

610 ) 

611 nbytes_token = next(tokens) 

612 if not nbytes_token.is_number(): 

613 raise RuntimeError( 

614 "Second token following dup in Subrs definition must " 

615 f"be a number, was {nbytes_token}" 

616 ) 

617 token = next(tokens) 

618 if not token.is_keyword(self._abbr['RD']): 

619 raise RuntimeError( 

620 f"Token preceding subr must be {self._abbr['RD']}, " 

621 f"was {token}" 

622 ) 

623 binary_token = tokens.send(1+nbytes_token.value()) 

624 array[index_token.value()] = binary_token.value() 

625 

626 return array, next(tokens).endpos() 

627 

628 @staticmethod 

629 def _parse_charstrings(tokens, _data): 

630 count_token = next(tokens) 

631 if not count_token.is_number(): 

632 raise RuntimeError( 

633 "Token following /CharStrings must be a number, " 

634 f"was {count_token}" 

635 ) 

636 count = count_token.value() 

637 charstrings = {} 

638 next(t for t in tokens if t.is_keyword('begin')) 

639 while True: 

640 token = next(t for t in tokens 

641 if t.is_keyword('end') or t.is_slash_name()) 

642 if token.raw == 'end': 

643 return charstrings, token.endpos() 

644 glyphname = token.value() 

645 nbytes_token = next(tokens) 

646 if not nbytes_token.is_number(): 

647 raise RuntimeError( 

648 f"Token following /{glyphname} in CharStrings definition " 

649 f"must be a number, was {nbytes_token}" 

650 ) 

651 next(tokens) # usually RD or |- 

652 binary_token = tokens.send(1+nbytes_token.value()) 

653 charstrings[glyphname] = binary_token.value() 

654 

655 @staticmethod 

656 def _parse_encoding(tokens, _data): 

657 # this only works for encodings that follow the Adobe manual 

658 # but some old fonts include non-compliant data - we log a warning 

659 # and return a possibly incomplete encoding 

660 encoding = {} 

661 while True: 

662 token = next(t for t in tokens 

663 if t.is_keyword('StandardEncoding', 'dup', 'def')) 

664 if token.is_keyword('StandardEncoding'): 

665 return _StandardEncoding, token.endpos() 

666 if token.is_keyword('def'): 

667 return encoding, token.endpos() 

668 index_token = next(tokens) 

669 if not index_token.is_number(): 

670 _log.warning( 

671 f"Parsing encoding: expected number, got {index_token}" 

672 ) 

673 continue 

674 name_token = next(tokens) 

675 if not name_token.is_slash_name(): 

676 _log.warning( 

677 f"Parsing encoding: expected slash-name, got {name_token}" 

678 ) 

679 continue 

680 encoding[index_token.value()] = name_token.value() 

681 

682 @staticmethod 

683 def _parse_othersubrs(tokens, data): 

684 init_pos = None 

685 while True: 

686 token = next(tokens) 

687 if init_pos is None: 

688 init_pos = token.pos 

689 if token.is_delim(): 

690 _expression(token, tokens, data) 

691 elif token.is_keyword('def', 'ND', '|-'): 

692 return data[init_pos:token.endpos()], token.endpos() 

693 

694 def transform(self, effects): 

695 """ 

696 Return a new font that is slanted and/or extended. 

697 

698 Parameters 

699 ---------- 

700 effects : dict 

701 A dict with optional entries: 

702 

703 - 'slant' : float, default: 0 

704 Tangent of the angle that the font is to be slanted to the 

705 right. Negative values slant to the left. 

706 - 'extend' : float, default: 1 

707 Scaling factor for the font width. Values less than 1 condense 

708 the glyphs. 

709 

710 Returns 

711 ------- 

712 `Type1Font` 

713 """ 

714 fontname = self.prop['FontName'] 

715 italicangle = self.prop['ItalicAngle'] 

716 

717 array = [ 

718 float(x) for x in (self.prop['FontMatrix'] 

719 .lstrip('[').rstrip(']').split()) 

720 ] 

721 oldmatrix = np.eye(3, 3) 

722 oldmatrix[0:3, 0] = array[::2] 

723 oldmatrix[0:3, 1] = array[1::2] 

724 modifier = np.eye(3, 3) 

725 

726 if 'slant' in effects: 

727 slant = effects['slant'] 

728 fontname += '_Slant_%d' % int(1000 * slant) 

729 italicangle = round( 

730 float(italicangle) - np.arctan(slant) / np.pi * 180, 

731 5 

732 ) 

733 modifier[1, 0] = slant 

734 

735 if 'extend' in effects: 

736 extend = effects['extend'] 

737 fontname += '_Extend_%d' % int(1000 * extend) 

738 modifier[0, 0] = extend 

739 

740 newmatrix = np.dot(modifier, oldmatrix) 

741 array[::2] = newmatrix[0:3, 0] 

742 array[1::2] = newmatrix[0:3, 1] 

743 fontmatrix = ( 

744 '[%s]' % ' '.join(_format_approx(x, 6) for x in array) 

745 ) 

746 replacements = ( 

747 [(x, '/FontName/%s def' % fontname) 

748 for x in self._pos['FontName']] 

749 + [(x, '/ItalicAngle %a def' % italicangle) 

750 for x in self._pos['ItalicAngle']] 

751 + [(x, '/FontMatrix %s readonly def' % fontmatrix) 

752 for x in self._pos['FontMatrix']] 

753 + [(x, '') for x in self._pos.get('UniqueID', [])] 

754 ) 

755 

756 data = bytearray(self.parts[0]) 

757 data.extend(self.decrypted) 

758 len0 = len(self.parts[0]) 

759 for (pos0, pos1), value in sorted(replacements, reverse=True): 

760 data[pos0:pos1] = value.encode('ascii', 'replace') 

761 if pos0 < len(self.parts[0]): 

762 if pos1 >= len(self.parts[0]): 

763 raise RuntimeError( 

764 f"text to be replaced with {value} spans " 

765 "the eexec boundary" 

766 ) 

767 len0 += len(value) - pos1 + pos0 

768 

769 data = bytes(data) 

770 return Type1Font(( 

771 data[:len0], 

772 self._encrypt(data[len0:], 'eexec'), 

773 self.parts[2] 

774 )) 

775 

776 

777_StandardEncoding = { 

778 **{ord(letter): letter for letter in string.ascii_letters}, 

779 0: '.notdef', 

780 32: 'space', 

781 33: 'exclam', 

782 34: 'quotedbl', 

783 35: 'numbersign', 

784 36: 'dollar', 

785 37: 'percent', 

786 38: 'ampersand', 

787 39: 'quoteright', 

788 40: 'parenleft', 

789 41: 'parenright', 

790 42: 'asterisk', 

791 43: 'plus', 

792 44: 'comma', 

793 45: 'hyphen', 

794 46: 'period', 

795 47: 'slash', 

796 48: 'zero', 

797 49: 'one', 

798 50: 'two', 

799 51: 'three', 

800 52: 'four', 

801 53: 'five', 

802 54: 'six', 

803 55: 'seven', 

804 56: 'eight', 

805 57: 'nine', 

806 58: 'colon', 

807 59: 'semicolon', 

808 60: 'less', 

809 61: 'equal', 

810 62: 'greater', 

811 63: 'question', 

812 64: 'at', 

813 91: 'bracketleft', 

814 92: 'backslash', 

815 93: 'bracketright', 

816 94: 'asciicircum', 

817 95: 'underscore', 

818 96: 'quoteleft', 

819 123: 'braceleft', 

820 124: 'bar', 

821 125: 'braceright', 

822 126: 'asciitilde', 

823 161: 'exclamdown', 

824 162: 'cent', 

825 163: 'sterling', 

826 164: 'fraction', 

827 165: 'yen', 

828 166: 'florin', 

829 167: 'section', 

830 168: 'currency', 

831 169: 'quotesingle', 

832 170: 'quotedblleft', 

833 171: 'guillemotleft', 

834 172: 'guilsinglleft', 

835 173: 'guilsinglright', 

836 174: 'fi', 

837 175: 'fl', 

838 177: 'endash', 

839 178: 'dagger', 

840 179: 'daggerdbl', 

841 180: 'periodcentered', 

842 182: 'paragraph', 

843 183: 'bullet', 

844 184: 'quotesinglbase', 

845 185: 'quotedblbase', 

846 186: 'quotedblright', 

847 187: 'guillemotright', 

848 188: 'ellipsis', 

849 189: 'perthousand', 

850 191: 'questiondown', 

851 193: 'grave', 

852 194: 'acute', 

853 195: 'circumflex', 

854 196: 'tilde', 

855 197: 'macron', 

856 198: 'breve', 

857 199: 'dotaccent', 

858 200: 'dieresis', 

859 202: 'ring', 

860 203: 'cedilla', 

861 205: 'hungarumlaut', 

862 206: 'ogonek', 

863 207: 'caron', 

864 208: 'emdash', 

865 225: 'AE', 

866 227: 'ordfeminine', 

867 232: 'Lslash', 

868 233: 'Oslash', 

869 234: 'OE', 

870 235: 'ordmasculine', 

871 241: 'ae', 

872 245: 'dotlessi', 

873 248: 'lslash', 

874 249: 'oslash', 

875 250: 'oe', 

876 251: 'germandbls', 

877}