fontisan 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +216 -42
  3. data/README.adoc +160 -0
  4. data/lib/fontisan/cli.rb +177 -6
  5. data/lib/fontisan/collection/table_analyzer.rb +88 -3
  6. data/lib/fontisan/commands/convert_command.rb +32 -1
  7. data/lib/fontisan/config/conversion_matrix.yml +132 -4
  8. data/lib/fontisan/constants.rb +12 -0
  9. data/lib/fontisan/conversion_options.rb +378 -0
  10. data/lib/fontisan/converters/cff_table_builder.rb +198 -0
  11. data/lib/fontisan/converters/collection_converter.rb +45 -10
  12. data/lib/fontisan/converters/format_converter.rb +2 -0
  13. data/lib/fontisan/converters/glyf_table_builder.rb +63 -0
  14. data/lib/fontisan/converters/outline_converter.rb +111 -374
  15. data/lib/fontisan/converters/outline_extraction.rb +93 -0
  16. data/lib/fontisan/converters/outline_optimizer.rb +89 -0
  17. data/lib/fontisan/converters/type1_converter.rb +559 -0
  18. data/lib/fontisan/font_loader.rb +46 -3
  19. data/lib/fontisan/glyph_accessor.rb +29 -1
  20. data/lib/fontisan/type1/afm_generator.rb +436 -0
  21. data/lib/fontisan/type1/afm_parser.rb +298 -0
  22. data/lib/fontisan/type1/agl.rb +456 -0
  23. data/lib/fontisan/type1/charstring_converter.rb +240 -0
  24. data/lib/fontisan/type1/charstrings.rb +408 -0
  25. data/lib/fontisan/type1/conversion_options.rb +243 -0
  26. data/lib/fontisan/type1/decryptor.rb +183 -0
  27. data/lib/fontisan/type1/encodings.rb +697 -0
  28. data/lib/fontisan/type1/font_dictionary.rb +514 -0
  29. data/lib/fontisan/type1/generator.rb +220 -0
  30. data/lib/fontisan/type1/inf_generator.rb +332 -0
  31. data/lib/fontisan/type1/pfa_generator.rb +343 -0
  32. data/lib/fontisan/type1/pfa_parser.rb +158 -0
  33. data/lib/fontisan/type1/pfb_generator.rb +291 -0
  34. data/lib/fontisan/type1/pfb_parser.rb +166 -0
  35. data/lib/fontisan/type1/pfm_generator.rb +610 -0
  36. data/lib/fontisan/type1/pfm_parser.rb +433 -0
  37. data/lib/fontisan/type1/private_dict.rb +285 -0
  38. data/lib/fontisan/type1/ttf_to_type1_converter.rb +327 -0
  39. data/lib/fontisan/type1/upm_scaler.rb +118 -0
  40. data/lib/fontisan/type1.rb +73 -0
  41. data/lib/fontisan/type1_font.rb +331 -0
  42. data/lib/fontisan/variation/cache.rb +1 -0
  43. data/lib/fontisan/version.rb +1 -1
  44. data/lib/fontisan/woff2_font.rb +3 -3
  45. data/lib/fontisan.rb +2 -0
  46. metadata +30 -2
@@ -0,0 +1,456 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Type1
5
+ # Adobe Glyph List (AGL) for Unicode to glyph name mapping
6
+ #
7
+ # [`AGL`](lib/fontisan/type1/agl.rb) provides mapping between Unicode codepoints
8
+ # and glyph names according to the Adobe Glyph List Specification.
9
+ #
10
+ # The AGL defines standard names for glyphs to ensure compatibility across
11
+ # font applications and systems.
12
+ #
13
+ # @see https://github.com/adobe-type-tools/agl-specification
14
+ module AGL
15
+ # Unicode to glyph name mapping (subset of AGL)
16
+ # Includes commonly used glyphs from Latin-1 and Latin Extended-A
17
+ UNICODE_TO_NAME = {
18
+ # ASCII control characters and basic Latin
19
+ 0x0020 => "space",
20
+ 0x0021 => "exclam",
21
+ 0x0022 => "quotedbl",
22
+ 0x0023 => "numbersign",
23
+ 0x0024 => "dollar",
24
+ 0x0025 => "percent",
25
+ 0x0026 => "ampersand",
26
+ 0x0027 => "quotesingle",
27
+ 0x0028 => "parenleft",
28
+ 0x0029 => "parenright",
29
+ 0x002A => "asterisk",
30
+ 0x002B => "plus",
31
+ 0x002C => "comma",
32
+ 0x002D => "hyphen",
33
+ 0x002E => "period",
34
+ 0x002F => "slash",
35
+ 0x0030 => "zero",
36
+ 0x0031 => "one",
37
+ 0x0032 => "two",
38
+ 0x0033 => "three",
39
+ 0x0034 => "four",
40
+ 0x0035 => "five",
41
+ 0x0036 => "six",
42
+ 0x0037 => "seven",
43
+ 0x0038 => "eight",
44
+ 0x0039 => "nine",
45
+ 0x003A => "colon",
46
+ 0x003B => "semicolon",
47
+ 0x003C => "less",
48
+ 0x003D => "equal",
49
+ 0x003E => "greater",
50
+ 0x003F => "question",
51
+ 0x0040 => "at",
52
+ 0x0041 => "A",
53
+ 0x0042 => "B",
54
+ 0x0043 => "C",
55
+ 0x0044 => "D",
56
+ 0x0045 => "E",
57
+ 0x0046 => "F",
58
+ 0x0047 => "G",
59
+ 0x0048 => "H",
60
+ 0x0049 => "I",
61
+ 0x004A => "J",
62
+ 0x004B => "K",
63
+ 0x004C => "L",
64
+ 0x004D => "M",
65
+ 0x004E => "N",
66
+ 0x004F => "O",
67
+ 0x0050 => "P",
68
+ 0x0051 => "Q",
69
+ 0x0052 => "R",
70
+ 0x0053 => "S",
71
+ 0x0054 => "T",
72
+ 0x0055 => "U",
73
+ 0x0056 => "V",
74
+ 0x0057 => "W",
75
+ 0x0058 => "X",
76
+ 0x0059 => "Y",
77
+ 0x005A => "Z",
78
+ 0x005B => "bracketleft",
79
+ 0x005C => "backslash",
80
+ 0x005D => "bracketright",
81
+ 0x005E => "asciicircum",
82
+ 0x005F => "underscore",
83
+ 0x0060 => "grave",
84
+ 0x0061 => "a",
85
+ 0x0062 => "b",
86
+ 0x0063 => "c",
87
+ 0x0064 => "d",
88
+ 0x0065 => "e",
89
+ 0x0066 => "f",
90
+ 0x0067 => "g",
91
+ 0x0068 => "h",
92
+ 0x0069 => "i",
93
+ 0x006A => "j",
94
+ 0x006B => "k",
95
+ 0x006C => "l",
96
+ 0x006D => "m",
97
+ 0x006E => "n",
98
+ 0x006F => "o",
99
+ 0x0070 => "p",
100
+ 0x0071 => "q",
101
+ 0x0072 => "r",
102
+ 0x0073 => "s",
103
+ 0x0074 => "t",
104
+ 0x0075 => "u",
105
+ 0x0076 => "v",
106
+ 0x0077 => "w",
107
+ 0x0078 => "x",
108
+ 0x0079 => "y",
109
+ 0x007A => "z",
110
+ 0x007B => "braceleft",
111
+ 0x007C => "bar",
112
+ 0x007D => "braceright",
113
+ 0x007E => "asciitilde",
114
+ # Latin-1 Supplement
115
+ 0x00A0 => "space",
116
+ 0x00A1 => "exclamdown",
117
+ 0x00A2 => "cent",
118
+ 0x00A3 => "sterling",
119
+ 0x00A4 => "currency",
120
+ 0x00A5 => "yen",
121
+ 0x00A6 => "brokenbar",
122
+ 0x00A7 => "section",
123
+ 0x00A8 => "dieresis",
124
+ 0x00A9 => "copyright",
125
+ 0x00AA => "ordfeminine",
126
+ 0x00AB => "guillemotleft",
127
+ 0x00AC => "logicalnot",
128
+ 0x00AD => "hyphen",
129
+ 0x00AE => "registered",
130
+ 0x00AF => "macron",
131
+ 0x00B0 => "degree",
132
+ 0x00B1 => "plusminus",
133
+ 0x00B2 => "twosuperior",
134
+ 0x00B3 => "threesuperior",
135
+ 0x00B4 => "acute",
136
+ 0x00B5 => "mu",
137
+ 0x00B6 => "paragraph",
138
+ 0x00B7 => "periodcentered",
139
+ 0x00B8 => "cedilla",
140
+ 0x00B9 => "onesuperior",
141
+ 0x00BA => "ordmasculine",
142
+ 0x00BB => "guillemotright",
143
+ 0x00BC => "onequarter",
144
+ 0x00BD => "onehalf",
145
+ 0x00BE => "threequarters",
146
+ 0x00BF => "questiondown",
147
+ 0x00C0 => "Agrave",
148
+ 0x00C1 => "Aacute",
149
+ 0x00C2 => "Acircumflex",
150
+ 0x00C3 => "Atilde",
151
+ 0x00C4 => "Adieresis",
152
+ 0x00C5 => "Aring",
153
+ 0x00C6 => "AE",
154
+ 0x00C7 => "Ccedilla",
155
+ 0x00C8 => "Egrave",
156
+ 0x00C9 => "Eacute",
157
+ 0x00CA => "Ecircumflex",
158
+ 0x00CB => "Edieresis",
159
+ 0x00CC => "Igrave",
160
+ 0x00CD => "Iacute",
161
+ 0x00CE => "Icircumflex",
162
+ 0x00CF => "Idieresis",
163
+ 0x00D0 => "Eth",
164
+ 0x00D1 => "Ntilde",
165
+ 0x00D2 => "Ograve",
166
+ 0x00D3 => "Oacute",
167
+ 0x00D4 => "Ocircumflex",
168
+ 0x00D5 => "Otilde",
169
+ 0x00D6 => "Odieresis",
170
+ 0x00D7 => "multiply",
171
+ 0x00D8 => "Oslash",
172
+ 0x00D9 => "Ugrave",
173
+ 0x00DA => "Uacute",
174
+ 0x00DB => "Ucircumflex",
175
+ 0x00DC => "Udieresis",
176
+ 0x00DD => "Yacute",
177
+ 0x00DE => "Thorn",
178
+ 0x00DF => "germandbls",
179
+ 0x00E0 => "agrave",
180
+ 0x00E1 => "aacute",
181
+ 0x00E2 => "acircumflex",
182
+ 0x00E3 => "atilde",
183
+ 0x00E4 => "adieresis",
184
+ 0x00E5 => "aring",
185
+ 0x00E6 => "ae",
186
+ 0x00E7 => "ccedilla",
187
+ 0x00E8 => "egrave",
188
+ 0x00E9 => "eacute",
189
+ 0x00EA => "ecircumflex",
190
+ 0x00EB => "edieresis",
191
+ 0x00EC => "igrave",
192
+ 0x00ED => "iacute",
193
+ 0x00EE => "icircumflex",
194
+ 0x00EF => "idieresis",
195
+ 0x00F0 => "eth",
196
+ 0x00F1 => "ntilde",
197
+ 0x00F2 => "ograve",
198
+ 0x00F3 => "oacute",
199
+ 0x00F4 => "ocircumflex",
200
+ 0x00F5 => "otilde",
201
+ 0x00F6 => "odieresis",
202
+ 0x00F7 => "divide",
203
+ 0x00F8 => "oslash",
204
+ 0x00F9 => "ugrave",
205
+ 0x00FA => "uacute",
206
+ 0x00FB => "ucircumflex",
207
+ 0x00FC => "udieresis",
208
+ 0x00FD => "yacute",
209
+ 0x00FE => "thorn",
210
+ 0x00FF => "ydieresis",
211
+ # Latin Extended-A
212
+ 0x0100 => "Amacron",
213
+ 0x0101 => "amacron",
214
+ 0x0102 => "Abreve",
215
+ 0x0103 => "abreve",
216
+ 0x0104 => "Aogonek",
217
+ 0x0105 => "aogonek",
218
+ 0x0106 => "Cacute",
219
+ 0x0107 => "cacute",
220
+ 0x0108 => "Ccircumflex",
221
+ 0x0109 => "ccircumflex",
222
+ 0x010A => "Cdotaccent",
223
+ 0x010B => "cdotaccent",
224
+ 0x010C => "Ccaron",
225
+ 0x010D => "ccaron",
226
+ 0x010E => "Dcaron",
227
+ 0x010F => "dcaron",
228
+ 0x0110 => "Dcroat",
229
+ 0x0111 => "dcroat",
230
+ 0x0112 => "Emacron",
231
+ 0x0113 => "emacron",
232
+ 0x0114 => "Ebreve",
233
+ 0x0115 => "ebreve",
234
+ 0x0116 => "Edotaccent",
235
+ 0x0117 => "edotaccent",
236
+ 0x0118 => "Eogonek",
237
+ 0x0119 => "eogonek",
238
+ 0x011A => "Ecaron",
239
+ 0x011B => "ecaron",
240
+ 0x011C => "Gcircumflex",
241
+ 0x011D => "gcircumflex",
242
+ 0x011E => "Gbreve",
243
+ 0x011F => "gbreve",
244
+ 0x0120 => "Gdotaccent",
245
+ 0x0121 => "gdotaccent",
246
+ 0x0122 => "Gcommaaccent",
247
+ 0x0123 => "gcommaaccent",
248
+ 0x0124 => "Hcircumflex",
249
+ 0x0125 => "hcircumflex",
250
+ 0x0126 => "Hbar",
251
+ 0x0127 => "hbar",
252
+ 0x0128 => "Itilde",
253
+ 0x0129 => "itilde",
254
+ 0x012A => "Imacron",
255
+ 0x012B => "imacron",
256
+ 0x012C => "Ibreve",
257
+ 0x012D => "ibreve",
258
+ 0x012E => "Iogonek",
259
+ 0x012F => "iogonek",
260
+ 0x0130 => "Idotaccent",
261
+ 0x0131 => "dotlessi",
262
+ 0x0132 => "Lig",
263
+ 0x0133 => "lig",
264
+ 0x0134 => "Lslash",
265
+ 0x0135 => "lslash",
266
+ 0x0136 => "Nacute",
267
+ 0x0137 => "nacute",
268
+ 0x0138 => "kgreenlandic",
269
+ 0x0139 => "Ncommaaccent",
270
+ 0x013A => "ncommaaccent",
271
+ 0x013B => "Ncaron",
272
+ 0x013C => "ncaron",
273
+ 0x013D => "napostrophe",
274
+ 0x013E => "Eng",
275
+ 0x013F => "eng",
276
+ 0x0140 => "Omacron",
277
+ 0x0141 => "omacron",
278
+ 0x0142 => "Obreve",
279
+ 0x0143 => "obreve",
280
+ 0x0144 => "Ohungarumlaut",
281
+ 0x0145 => "ohungarumlaut",
282
+ 0x0146 => "Oogonek",
283
+ 0x0147 => "oogonek",
284
+ 0x0148 => "Racute",
285
+ 0x0149 => "racute",
286
+ 0x014A => "Rcaron",
287
+ 0x014B => "rcaron",
288
+ 0x014C => "Sacute",
289
+ 0x014D => "sacute",
290
+ 0x014E => "Scircumflex",
291
+ 0x014F => "scircumflex",
292
+ 0x0150 => "Scedilla",
293
+ 0x0151 => "scedilla",
294
+ 0x0152 => "Scaron",
295
+ 0x0153 => "scaron",
296
+ 0x0154 => "Tcommaaccent",
297
+ 0x0155 => "tcommaaccent",
298
+ 0x0156 => "Tcaron",
299
+ 0x0157 => "tcaron",
300
+ 0x0158 => "Tbar",
301
+ 0x0159 => "tbar",
302
+ 0x015A => "Utilde",
303
+ 0x015B => "utilde",
304
+ 0x015C => "Umacron",
305
+ 0x015D => "umacron",
306
+ 0x015E => "Ubreve",
307
+ 0x015F => "ubreve",
308
+ 0x0160 => "Uring",
309
+ 0x0161 => "uring",
310
+ 0x0162 => "Uhungarumlaut",
311
+ 0x0163 => "uhungarumlaut",
312
+ 0x0164 => "Uogonek",
313
+ 0x0165 => "uogonek",
314
+ 0x0166 => "Wcircumflex",
315
+ 0x0167 => "wcircumflex",
316
+ 0x0168 => "Ycircumflex",
317
+ 0x0169 => "ycircumflex",
318
+ 0x016A => "Zacute",
319
+ 0x016B => "zacute",
320
+ 0x016C => "Zdotaccent",
321
+ 0x016D => "zdotaccent",
322
+ 0x016E => "Zcaron",
323
+ 0x016F => "zcaron",
324
+ 0x0170 => "longs",
325
+ 0x0171 => "caron",
326
+ 0x0172 => "breve",
327
+ 0x0173 => "dotaccent",
328
+ 0x0174 => "ring",
329
+ 0x0175 => "ogonek",
330
+ 0x0176 => "tilde",
331
+ 0x0177 => "hungarumlaut",
332
+ 0x0178 => "commaaccent",
333
+ 0x0179 => "slash",
334
+ 0x017A => "hyphen",
335
+ 0x017B => "period",
336
+ 0x017F => "florin",
337
+ # Greek (some common)
338
+ 0x0391 => "Alpha",
339
+ 0x0392 => "Beta",
340
+ 0x0393 => "Gamma",
341
+ 0x0394 => "Delta",
342
+ 0x0395 => "Epsilon",
343
+ 0x0396 => "Zeta",
344
+ 0x0397 => "Eta",
345
+ 0x0398 => "Theta",
346
+ 0x0399 => "Iota",
347
+ 0x039A => "Kappa",
348
+ 0x039B => "Lambda",
349
+ 0x039C => "Mu",
350
+ 0x039D => "Nu",
351
+ 0x039E => "Xi",
352
+ 0x039F => "Omicron",
353
+ 0x03A0 => "Pi",
354
+ 0x03A1 => "Rho",
355
+ 0x03A3 => "Sigma",
356
+ 0x03A4 => "Tau",
357
+ 0x03A5 => "Upsilon",
358
+ 0x03A6 => "Phi",
359
+ 0x03A7 => "Chi",
360
+ 0x03A8 => "Psi",
361
+ 0x03A9 => "Omega",
362
+ # Currency and other symbols
363
+ 0x20AC => "Euro",
364
+ 0x2113 => "literalsign",
365
+ 0x2116 => "numerosign",
366
+ 0x2122 => "trademark",
367
+ 0x2126 => "Omega",
368
+ 0x212E => "estimated",
369
+ 0x2202 => "partialdiff",
370
+ 0x2206 => "Delta",
371
+ 0x220F => "product",
372
+ 0x2211 => "summation",
373
+ 0x221A => "radical",
374
+ 0x221E => "infinity",
375
+ 0x222B => "integral",
376
+ 0x2248 => "approxequal",
377
+ 0x2260 => "notequal",
378
+ 0x2264 => "lessequal",
379
+ 0x2265 => "greaterequal",
380
+ }.freeze
381
+
382
+ # Glyph name to Unicode mapping (inverse of UNICODE_TO_NAME)
383
+ # When duplicates exist, uses the first (lowest) codepoint
384
+ NAME_TO_UNICODE = begin
385
+ result = {}
386
+ UNICODE_TO_NAME.each do |codepoint, name|
387
+ result[name] ||= codepoint # Only set first occurrence
388
+ end
389
+ result.freeze
390
+ end
391
+
392
+ # Get glyph name for Unicode codepoint
393
+ #
394
+ # @param codepoint [Integer] Unicode codepoint
395
+ # @return [String] Glyph name from AGL, or uniXXXX format if not found
396
+ def self.glyph_name_for_unicode(codepoint)
397
+ UNICODE_TO_NAME[codepoint] || generate_uni_name(codepoint)
398
+ end
399
+
400
+ # Get Unicode codepoint for glyph name
401
+ #
402
+ # @param name [String] Glyph name
403
+ # @return [Integer, nil] Unicode codepoint or nil if not found
404
+ def self.unicode_for_glyph_name(name)
405
+ # Try direct lookup
406
+ code = NAME_TO_UNICODE[name]
407
+ return code if code
408
+
409
+ # Try parsing uniXXXX or uXXXXX format
410
+ parse_uni_name(name)
411
+ end
412
+
413
+ # Check if a glyph name is in the AGL
414
+ #
415
+ # @param name [String] Glyph name
416
+ # @return [Boolean] True if name is in AGL
417
+ def self.agl_include?(name)
418
+ NAME_TO_UNICODE.key?(name)
419
+ end
420
+
421
+ # Generate uniXXXX name for codepoint not in AGL
422
+ #
423
+ # @param codepoint [Integer] Unicode codepoint
424
+ # @return [String] uniXXXX name
425
+ def self.generate_uni_name(codepoint)
426
+ format("uni%04X", codepoint)
427
+ end
428
+
429
+ # Parse uniXXXX or uXXXXX glyph name
430
+ #
431
+ # @param name [String] Glyph name in uni/u format
432
+ # @return [Integer, nil] Unicode codepoint or nil if not a uni name
433
+ def self.parse_uni_name(name)
434
+ if name =~ /^uni([0-9A-Fa-f]{4})$/
435
+ $1.to_i(16)
436
+ elsif name =~ /^u([0-9A-Fa-f]+)$/
437
+ $1.to_i(16)
438
+ end
439
+ end
440
+
441
+ # Get all AGL glyph names
442
+ #
443
+ # @return [Array<String>] All glyph names in the AGL subset
444
+ def self.all_glyph_names
445
+ NAME_TO_UNICODE.keys.sort
446
+ end
447
+
448
+ # Get all Unicode codepoints in AGL
449
+ #
450
+ # @return [Array<Integer>] All Unicode codepoints in the AGL subset
451
+ def self.all_codepoints
452
+ UNICODE_TO_NAME.keys.sort
453
+ end
454
+ end
455
+ end
456
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Type1
5
+ # Converter for Type 1 CharStrings to CFF CharStrings
6
+ #
7
+ # [`CharStringConverter`](lib/fontisan/type1/charstring_converter.rb) converts
8
+ # Type 1 CharString bytecode to CFF (Compact Font Format) CharString bytecode.
9
+ #
10
+ # Type 1 and CFF use similar stack-based languages but have different operator
11
+ # codes and some structural differences:
12
+ # - Operator codes differ between formats
13
+ # - Type 1 has seac operator for composites; CFF doesn't support it
14
+ # - Hint operators need to be preserved with code translation
15
+ #
16
+ # @example Convert a Type 1 CharString to CFF
17
+ # converter = Fontisan::Type1::CharStringConverter.new
18
+ # cff_charstring = converter.convert(type1_charstring)
19
+ #
20
+ # @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
21
+ # @see https://www.microsoft.com/typography/otspec/cff.htm
22
+ class CharStringConverter
23
+ # Type 1 to CFF operator mapping
24
+ #
25
+ # Maps Type 1 operator codes to CFF operator codes.
26
+ # Some operators have the same code, others differ.
27
+ TYPE1_TO_CFF = {
28
+ # Path construction operators
29
+ hmoveto: 22, # Type 1: 22, CFF: 22
30
+ vmoveto: 4, # Type 1: 4, CFF: 4
31
+ rlineto: 5, # Type 1: 5, CFF: 5
32
+ hlineto: 6, # Type 1: 6, CFF: 6
33
+ vlineto: 7, # Type 1: 7, CFF: 7
34
+ rrcurveto: 8, # Type 1: 8, CFF: 8
35
+ hhcurveto: 27, # Type 1: 27, CFF: 27
36
+ hvcurveto: 31, # Type 1: 31, CFF: 31
37
+ vhcurveto: 30, # Type 1: 30, CFF: 30
38
+ rcurveline: 24, # Type 1: 24, CFF: 24
39
+ rlinecurve: 25, # Type 1: 25, CFF: 25
40
+
41
+ # Hint operators
42
+ hstem: 1, # Type 1: 1, CFF: 1
43
+ vstem: 3, # Type 1: 3, CFF: 3
44
+ hstemhm: 18, # Type 1: 18, CFF: 18
45
+ vstemhm: 23, # Type 1: 23, CFF: 23
46
+
47
+ # Hint substitution (not in Type 1, but we preserve for compatibility)
48
+ hintmask: 19, # Type 1: N/A, CFF: 19
49
+ cntrmask: 20, # Type 1: N/A, CFF: 20
50
+
51
+ # End char
52
+ endchar: 14, # Type 1: 14 (or 11 in some specs), CFF: 14
53
+
54
+ # Miscellaneous
55
+ callsubr: 10, # Type 1: 10, CFF: 10
56
+ return: 11, # Type 1: 11, CFF: 11
57
+
58
+ # Deprecated operators (preserve for compatibility)
59
+ hstem3: 12, # Type 1: 12 (escape 0), CFF: 12 (escape 0)
60
+ vstem3: 13, # Type 1: 13 (escape 1), CFF: 13 (escape 1)
61
+ seac: 12, # Type 1: 12 (escape 6), CFF: Not supported
62
+ }.freeze
63
+
64
+ # Escape code for two-byte operators
65
+ ESCAPE_BYTE = 12
66
+
67
+ # seac operator escape code (second byte)
68
+ SEAC_ESCAPE_CODE = 6
69
+
70
+ # Initialize a new CharStringConverter
71
+ #
72
+ # @param charstrings [CharStrings, nil] CharStrings dictionary for seac expansion
73
+ def initialize(charstrings = nil)
74
+ @charstrings = charstrings
75
+ end
76
+
77
+ # Convert Type 1 CharString to CFF CharString
78
+ #
79
+ # @param type1_charstring [String] Type 1 CharString bytecode
80
+ # @return [String] CFF CharString bytecode
81
+ #
82
+ # @example Convert a CharString
83
+ # converter = Fontisan::Type1::CharStringConverter.new
84
+ # cff_bytes = converter.convert(type1_bytes)
85
+ def convert(type1_charstring)
86
+ # Parse Type 1 CharString into commands
87
+ parser = Type1::CharStrings::CharStringParser.new
88
+ commands = parser.parse(type1_charstring)
89
+
90
+ # Check for seac operator and expand if needed
91
+ if parser.seac_components
92
+ return expand_seac(parser.seac_components)
93
+ end
94
+
95
+ # Convert commands to CFF format
96
+ convert_commands(commands)
97
+ end
98
+
99
+ # Convert parsed commands to CFF CharString
100
+ #
101
+ # @param commands [Array<Array>] Parsed Type 1 commands
102
+ # @return [String] CFF CharString bytecode
103
+ def convert_commands(commands)
104
+ result = String.new(encoding: Encoding::ASCII_8BIT)
105
+
106
+ commands.each do |command|
107
+ case command[0]
108
+ when :number
109
+ # Encode number in CFF format
110
+ result << encode_cff_number(command[1])
111
+ when :seac
112
+ # seac should be expanded before this point
113
+ raise Fontisan::Error,
114
+ "seac operator not supported in CFF, must be expanded first"
115
+ else
116
+ # Convert operator
117
+ op_code = TYPE1_TO_CFF[command[0]]
118
+ if op_code.nil?
119
+ # Unknown operator, skip or raise error
120
+ next
121
+ end
122
+
123
+ result << encode_cff_operator(op_code)
124
+ end
125
+ end
126
+
127
+ result
128
+ end
129
+
130
+ # Expand seac composite glyph
131
+ #
132
+ # The seac operator in Type 1 creates composite glyphs (like À = A + `).
133
+ # CFF doesn't support seac, so we need to expand it into the base glyphs
134
+ # with appropriate positioning.
135
+ #
136
+ # @param seac_data [Hash] seac component data
137
+ # @return [String] CFF CharString bytecode with expanded seac
138
+ def expand_seac(seac_data)
139
+ # seac format: adx ady bchar achar seac
140
+ # adx, ady: accent offset
141
+ # bchar: base character code
142
+ # achar: accent character code
143
+ # The accent is positioned at (adx, ady) relative to the base
144
+
145
+ seac_data[:base]
146
+ seac_data[:accent]
147
+ seac_data[:adx]
148
+ seac_data[:ady]
149
+
150
+ # For now, we'll create a simple placeholder that indicates seac expansion
151
+ # In a full implementation, we would:
152
+ # 1. Parse the base glyph's CharString
153
+ # 2. Parse the accent glyph's CharString
154
+ # 3. Merge them with the appropriate offset
155
+ # 4. Convert to CFF format
156
+
157
+ # This is a simplified implementation that creates a composite reference
158
+ # CFF doesn't have native seac, so we need to actually merge the outlines
159
+
160
+ # For now, return endchar as placeholder
161
+ # TODO: Implement full seac expansion by merging glyph outlines
162
+ encode_cff_operator(TYPE1_TO_CFF[:endchar])
163
+ end
164
+
165
+ # Check if CharString contains seac operator
166
+ #
167
+ # @param type1_charstring [String] Type 1 CharString bytecode
168
+ # @return [Boolean] True if CharString contains seac
169
+ def seac?(type1_charstring)
170
+ parser = Type1::CharStrings::CharStringParser.new
171
+ parser.parse(type1_charstring)
172
+ !parser.seac_components.nil?
173
+ end
174
+
175
+ private
176
+
177
+ # Encode number in CFF format
178
+ #
179
+ # CFF uses a variable-length encoding for numbers:
180
+ # - 32-246: 1 byte (value - 139)
181
+ # - 247-250: 2 bytes (first byte indicates format)
182
+ # - 251-254: 3 bytes (first byte indicates format)
183
+ # - 255: 5 bytes (signed 16-bit integer)
184
+ # - 28: 2 bytes (signed 16.16 fixed point, not used for CharStrings)
185
+ #
186
+ # @param value [Integer] Number to encode
187
+ # @return [String] Encoded number bytes
188
+ def encode_cff_number(value)
189
+ result = String.new(encoding: Encoding::ASCII_8BIT)
190
+
191
+ if value >= -107 && value <= 107
192
+ # 1-byte number: value + 139
193
+ result << (value + 139).chr
194
+ elsif value >= 108 && value <= 1131
195
+ # 2-byte positive number
196
+ value -= 108
197
+ result << ((value >> 8) + 247).chr
198
+ result << (value & 0xFF).chr
199
+ elsif value >= -1131 && value <= -108
200
+ # 2-byte negative number
201
+ value = -value - 108
202
+ result << ((value >> 8) + 251).chr
203
+ result << (value & 0xFF).chr
204
+ elsif value >= -32768 && value <= 32767
205
+ # 5-byte number (16-bit integer)
206
+ result << 255.chr
207
+ result << [(value >> 8) & 0xFF, value & 0xFF].pack("CC")
208
+ result << [0, 0].pack("CC") # Pad to 5 bytes
209
+ else
210
+ raise Fontisan::Error,
211
+ "Number out of range for CFF encoding: #{value}"
212
+ end
213
+
214
+ result
215
+ end
216
+
217
+ # Encode operator in CFF format
218
+ #
219
+ # Most operators are single-byte. Some use escape byte (12) followed
220
+ # by a second byte.
221
+ #
222
+ # @param op_code [Integer] Operator code
223
+ # @return [String] Encoded operator bytes
224
+ def encode_cff_operator(op_code)
225
+ result = String.new(encoding: Encoding::ASCII_8BIT)
226
+
227
+ if op_code > 31 && op_code != ESCAPE_BYTE
228
+ # Two-byte operator (escape + code)
229
+ result << ESCAPE_BYTE.chr
230
+ result << (op_code - ESCAPE_BYTE).chr
231
+ else
232
+ # Single-byte operator
233
+ result << op_code.chr
234
+ end
235
+
236
+ result
237
+ end
238
+ end
239
+ end
240
+ end