fontisan 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +529 -65
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1301 -275
- data/Rakefile +27 -2
- data/benchmark/variation_quick_bench.rb +47 -0
- data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
- data/fontisan.gemspec +4 -1
- data/lib/fontisan/binary/base_record.rb +22 -1
- data/lib/fontisan/cli.rb +309 -0
- data/lib/fontisan/collection/builder.rb +260 -0
- data/lib/fontisan/collection/offset_calculator.rb +227 -0
- data/lib/fontisan/collection/table_analyzer.rb +204 -0
- data/lib/fontisan/collection/table_deduplicator.rb +241 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +8 -1
- data/lib/fontisan/commands/convert_command.rb +291 -0
- data/lib/fontisan/commands/export_command.rb +161 -0
- data/lib/fontisan/commands/info_command.rb +40 -6
- data/lib/fontisan/commands/instance_command.rb +295 -0
- data/lib/fontisan/commands/ls_command.rb +113 -0
- data/lib/fontisan/commands/pack_command.rb +241 -0
- data/lib/fontisan/commands/subset_command.rb +245 -0
- data/lib/fontisan/commands/unpack_command.rb +338 -0
- data/lib/fontisan/commands/validate_command.rb +178 -0
- data/lib/fontisan/commands/variable_command.rb +30 -1
- data/lib/fontisan/config/collection_settings.yml +56 -0
- data/lib/fontisan/config/conversion_matrix.yml +212 -0
- data/lib/fontisan/config/export_settings.yml +66 -0
- data/lib/fontisan/config/subset_profiles.yml +100 -0
- data/lib/fontisan/config/svg_settings.yml +60 -0
- data/lib/fontisan/config/validation_rules.yml +149 -0
- data/lib/fontisan/config/variable_settings.yml +99 -0
- data/lib/fontisan/config/woff2_settings.yml +77 -0
- data/lib/fontisan/constants.rb +69 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +259 -0
- data/lib/fontisan/converters/outline_converter.rb +936 -0
- data/lib/fontisan/converters/svg_generator.rb +244 -0
- data/lib/fontisan/converters/table_copier.rb +117 -0
- data/lib/fontisan/converters/woff2_encoder.rb +416 -0
- data/lib/fontisan/converters/woff_writer.rb +391 -0
- data/lib/fontisan/error.rb +203 -0
- data/lib/fontisan/export/exporter.rb +262 -0
- data/lib/fontisan/export/table_serializer.rb +255 -0
- data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
- data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
- data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
- data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
- data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
- data/lib/fontisan/export/ttx_generator.rb +527 -0
- data/lib/fontisan/export/ttx_parser.rb +300 -0
- data/lib/fontisan/font_loader.rb +121 -12
- data/lib/fontisan/font_writer.rb +301 -0
- data/lib/fontisan/formatters/text_formatter.rb +102 -0
- data/lib/fontisan/glyph_accessor.rb +503 -0
- data/lib/fontisan/hints/hint_converter.rb +177 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
- data/lib/fontisan/loading_modes.rb +113 -0
- data/lib/fontisan/metrics_calculator.rb +277 -0
- data/lib/fontisan/models/collection_font_summary.rb +52 -0
- data/lib/fontisan/models/collection_info.rb +76 -0
- data/lib/fontisan/models/collection_list_info.rb +37 -0
- data/lib/fontisan/models/font_export.rb +158 -0
- data/lib/fontisan/models/font_summary.rb +48 -0
- data/lib/fontisan/models/glyph_outline.rb +343 -0
- data/lib/fontisan/models/hint.rb +233 -0
- data/lib/fontisan/models/outline.rb +664 -0
- data/lib/fontisan/models/table_sharing_info.rb +40 -0
- data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
- data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
- data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
- data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
- data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
- data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
- data/lib/fontisan/models/ttx/ttfont.rb +49 -0
- data/lib/fontisan/models/validation_report.rb +203 -0
- data/lib/fontisan/open_type_collection.rb +156 -2
- data/lib/fontisan/open_type_font.rb +296 -10
- data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
- data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
- data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
- data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
- data/lib/fontisan/outline_extractor.rb +423 -0
- data/lib/fontisan/subset/builder.rb +268 -0
- data/lib/fontisan/subset/glyph_mapping.rb +215 -0
- data/lib/fontisan/subset/options.rb +142 -0
- data/lib/fontisan/subset/profile.rb +152 -0
- data/lib/fontisan/subset/table_subsetter.rb +461 -0
- data/lib/fontisan/svg/font_face_generator.rb +278 -0
- data/lib/fontisan/svg/font_generator.rb +264 -0
- data/lib/fontisan/svg/glyph_generator.rb +168 -0
- data/lib/fontisan/svg/view_box_calculator.rb +137 -0
- data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
- data/lib/fontisan/tables/cff/charset.rb +282 -0
- data/lib/fontisan/tables/cff/charstring.rb +905 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
- data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
- data/lib/fontisan/tables/cff/dict.rb +351 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/index.rb +237 -0
- data/lib/fontisan/tables/cff/index_builder.rb +170 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +487 -0
- data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
- data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
- data/lib/fontisan/tables/cff2.rb +341 -0
- data/lib/fontisan/tables/cvar.rb +242 -0
- data/lib/fontisan/tables/fvar.rb +2 -2
- data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
- data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
- data/lib/fontisan/tables/glyf.rb +235 -0
- data/lib/fontisan/tables/gvar.rb +270 -0
- data/lib/fontisan/tables/hhea.rb +124 -0
- data/lib/fontisan/tables/hmtx.rb +287 -0
- data/lib/fontisan/tables/hvar.rb +191 -0
- data/lib/fontisan/tables/loca.rb +322 -0
- data/lib/fontisan/tables/maxp.rb +192 -0
- data/lib/fontisan/tables/mvar.rb +185 -0
- data/lib/fontisan/tables/name.rb +99 -30
- data/lib/fontisan/tables/variation_common.rb +346 -0
- data/lib/fontisan/tables/vvar.rb +234 -0
- data/lib/fontisan/true_type_collection.rb +156 -2
- data/lib/fontisan/true_type_font.rb +297 -11
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
- data/lib/fontisan/utils/thread_pool.rb +134 -0
- data/lib/fontisan/validation/checksum_validator.rb +170 -0
- data/lib/fontisan/validation/consistency_validator.rb +197 -0
- data/lib/fontisan/validation/structure_validator.rb +198 -0
- data/lib/fontisan/validation/table_validator.rb +158 -0
- data/lib/fontisan/validation/validator.rb +152 -0
- data/lib/fontisan/variable/axis_normalizer.rb +215 -0
- data/lib/fontisan/variable/delta_applicator.rb +313 -0
- data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
- data/lib/fontisan/variable/instancer.rb +344 -0
- data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
- data/lib/fontisan/variable/region_matcher.rb +208 -0
- data/lib/fontisan/variable/static_font_builder.rb +213 -0
- data/lib/fontisan/variable/table_updater.rb +219 -0
- data/lib/fontisan/variation/blend_applier.rb +199 -0
- data/lib/fontisan/variation/cache.rb +298 -0
- data/lib/fontisan/variation/cache_key_builder.rb +162 -0
- data/lib/fontisan/variation/converter.rb +268 -0
- data/lib/fontisan/variation/data_extractor.rb +86 -0
- data/lib/fontisan/variation/delta_applier.rb +266 -0
- data/lib/fontisan/variation/delta_parser.rb +228 -0
- data/lib/fontisan/variation/inspector.rb +275 -0
- data/lib/fontisan/variation/instance_generator.rb +273 -0
- data/lib/fontisan/variation/interpolator.rb +231 -0
- data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
- data/lib/fontisan/variation/optimizer.rb +418 -0
- data/lib/fontisan/variation/parallel_generator.rb +150 -0
- data/lib/fontisan/variation/region_matcher.rb +221 -0
- data/lib/fontisan/variation/subsetter.rb +463 -0
- data/lib/fontisan/variation/table_accessor.rb +105 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +712 -0
- data/lib/fontisan/woff_font.rb +483 -0
- data/lib/fontisan.rb +120 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +205 -4
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff
|
|
8
|
+
# Type 2 CharString builder/encoder
|
|
9
|
+
#
|
|
10
|
+
# [`CharStringBuilder`](lib/fontisan/tables/cff/charstring_builder.rb)
|
|
11
|
+
# encodes glyph outlines into Type 2 CharString binary format. It takes
|
|
12
|
+
# high-level outline commands and produces the stack-based CharString
|
|
13
|
+
# operators used in CFF fonts.
|
|
14
|
+
#
|
|
15
|
+
# Type 2 CharString encoding:
|
|
16
|
+
# - Numbers are encoded in various compact formats
|
|
17
|
+
# - Operators are single or two-byte commands
|
|
18
|
+
# - All coordinates are relative (dx, dy format)
|
|
19
|
+
# - Current point tracking for relative calculations
|
|
20
|
+
#
|
|
21
|
+
# Operator optimization:
|
|
22
|
+
# - Use specialized operators (hlineto, vlineto) when possible
|
|
23
|
+
# - Merge sequential operators of same type
|
|
24
|
+
# - Minimize operator bytes
|
|
25
|
+
#
|
|
26
|
+
# Reference: Adobe Type 2 CharString Format
|
|
27
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf
|
|
28
|
+
#
|
|
29
|
+
# @example Building a CharString from outline
|
|
30
|
+
# builder = Fontisan::Tables::Cff::CharStringBuilder.new
|
|
31
|
+
# charstring_data = builder.build(outline, width: 500)
|
|
32
|
+
class CharStringBuilder
|
|
33
|
+
# Type 2 CharString operators (opposite of parser)
|
|
34
|
+
OPERATORS = {
|
|
35
|
+
hstem: 1,
|
|
36
|
+
vstem: 3,
|
|
37
|
+
vmoveto: 4,
|
|
38
|
+
rlineto: 5,
|
|
39
|
+
hlineto: 6,
|
|
40
|
+
vlineto: 7,
|
|
41
|
+
rrcurveto: 8,
|
|
42
|
+
callsubr: 10,
|
|
43
|
+
return: 11,
|
|
44
|
+
endchar: 14,
|
|
45
|
+
hstemhm: 18,
|
|
46
|
+
hintmask: 19,
|
|
47
|
+
cntrmask: 20,
|
|
48
|
+
rmoveto: 21,
|
|
49
|
+
hmoveto: 22,
|
|
50
|
+
vstemhm: 23,
|
|
51
|
+
rcurveline: 24,
|
|
52
|
+
rlinecurve: 25,
|
|
53
|
+
vvcurveto: 26,
|
|
54
|
+
hhcurveto: 27,
|
|
55
|
+
shortint: 28,
|
|
56
|
+
callgsubr: 29,
|
|
57
|
+
vhcurveto: 30,
|
|
58
|
+
hvcurveto: 31,
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
# Two-byte operators (12 prefix)
|
|
62
|
+
TWO_BYTE_OPERATORS = {
|
|
63
|
+
and: [12, 3],
|
|
64
|
+
or: [12, 4],
|
|
65
|
+
not: [12, 5],
|
|
66
|
+
abs: [12, 9],
|
|
67
|
+
add: [12, 10],
|
|
68
|
+
sub: [12, 11],
|
|
69
|
+
div: [12, 12],
|
|
70
|
+
neg: [12, 14],
|
|
71
|
+
eq: [12, 15],
|
|
72
|
+
drop: [12, 18],
|
|
73
|
+
put: [12, 20],
|
|
74
|
+
get: [12, 21],
|
|
75
|
+
ifelse: [12, 22],
|
|
76
|
+
random: [12, 23],
|
|
77
|
+
mul: [12, 24],
|
|
78
|
+
sqrt: [12, 26],
|
|
79
|
+
dup: [12, 27],
|
|
80
|
+
exch: [12, 28],
|
|
81
|
+
index: [12, 29],
|
|
82
|
+
roll: [12, 30],
|
|
83
|
+
hflex: [12, 34],
|
|
84
|
+
flex: [12, 35],
|
|
85
|
+
hflex1: [12, 36],
|
|
86
|
+
flex1: [12, 37],
|
|
87
|
+
}.freeze
|
|
88
|
+
|
|
89
|
+
# Build a CharString from an outline
|
|
90
|
+
#
|
|
91
|
+
# @param outline [Models::Outline] Universal outline object
|
|
92
|
+
# @param width [Integer, nil] Glyph width (optional)
|
|
93
|
+
# @return [String] Binary CharString data
|
|
94
|
+
def build(outline, width: nil)
|
|
95
|
+
@output = StringIO.new("".b)
|
|
96
|
+
@current_x = 0.0
|
|
97
|
+
@current_y = 0.0
|
|
98
|
+
@first_move = true
|
|
99
|
+
|
|
100
|
+
# Convert outline to CFF commands
|
|
101
|
+
commands = outline.to_cff_commands
|
|
102
|
+
|
|
103
|
+
# Encode width if provided (before first move)
|
|
104
|
+
if width && !commands.empty?
|
|
105
|
+
# Width is encoded as first operator before first move
|
|
106
|
+
# For now, we'll add it before the first moveto
|
|
107
|
+
encode_width(width)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Encode each command
|
|
111
|
+
commands.each do |cmd|
|
|
112
|
+
encode_command(cmd)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# End character
|
|
116
|
+
write_operator(:endchar)
|
|
117
|
+
|
|
118
|
+
@output.string
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Build an empty CharString (for .notdef or empty glyphs)
|
|
122
|
+
#
|
|
123
|
+
# @param width [Integer, nil] Glyph width
|
|
124
|
+
# @return [String] Binary CharString data
|
|
125
|
+
def build_empty(width: nil)
|
|
126
|
+
@output = StringIO.new("".b)
|
|
127
|
+
|
|
128
|
+
# Encode width if provided
|
|
129
|
+
encode_width(width) if width
|
|
130
|
+
|
|
131
|
+
# Just endchar for empty glyph
|
|
132
|
+
write_operator(:endchar)
|
|
133
|
+
|
|
134
|
+
@output.string
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
# Encode a width value
|
|
140
|
+
#
|
|
141
|
+
# Width is encoded as a delta from nominal width
|
|
142
|
+
# For simplicity, we encode as-is (assuming nominal width is 0)
|
|
143
|
+
#
|
|
144
|
+
# @param width [Integer] Width value
|
|
145
|
+
def encode_width(width)
|
|
146
|
+
write_number(width)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Encode a single command
|
|
150
|
+
#
|
|
151
|
+
# @param cmd [Hash] Command hash with :type and coordinates
|
|
152
|
+
def encode_command(cmd)
|
|
153
|
+
case cmd[:type]
|
|
154
|
+
when :move_to
|
|
155
|
+
encode_moveto(cmd)
|
|
156
|
+
when :line_to
|
|
157
|
+
encode_lineto(cmd)
|
|
158
|
+
when :curve_to
|
|
159
|
+
encode_curveto(cmd)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Encode a moveto command
|
|
164
|
+
#
|
|
165
|
+
# Uses rmoveto (relative move) with dx, dy
|
|
166
|
+
# For first move, can optimize to hmoveto/vmoveto if one delta is 0
|
|
167
|
+
#
|
|
168
|
+
# @param cmd [Hash] Command with :x, :y
|
|
169
|
+
def encode_moveto(cmd)
|
|
170
|
+
dx = cmd[:x] - @current_x
|
|
171
|
+
dy = cmd[:y] - @current_y
|
|
172
|
+
|
|
173
|
+
if @first_move
|
|
174
|
+
# First move - can optimize
|
|
175
|
+
if dx.zero?
|
|
176
|
+
write_number(dy.round)
|
|
177
|
+
write_operator(:vmoveto)
|
|
178
|
+
elsif dy.zero?
|
|
179
|
+
write_number(dx.round)
|
|
180
|
+
write_operator(:hmoveto)
|
|
181
|
+
else
|
|
182
|
+
write_number(dx.round)
|
|
183
|
+
write_number(dy.round)
|
|
184
|
+
write_operator(:rmoveto)
|
|
185
|
+
end
|
|
186
|
+
@first_move = false
|
|
187
|
+
else
|
|
188
|
+
# Subsequent moves
|
|
189
|
+
write_number(dx.round)
|
|
190
|
+
write_number(dy.round)
|
|
191
|
+
write_operator(:rmoveto)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
@current_x = cmd[:x]
|
|
195
|
+
@current_y = cmd[:y]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Encode a lineto command
|
|
199
|
+
#
|
|
200
|
+
# Uses rlineto with dx, dy
|
|
201
|
+
# Could optimize with hlineto/vlineto for horizontal/vertical lines
|
|
202
|
+
#
|
|
203
|
+
# @param cmd [Hash] Command with :x, :y
|
|
204
|
+
def encode_lineto(cmd)
|
|
205
|
+
dx = cmd[:x] - @current_x
|
|
206
|
+
dy = cmd[:y] - @current_y
|
|
207
|
+
|
|
208
|
+
# Simple encoding - could optimize for h/v lines
|
|
209
|
+
write_number(dx.round)
|
|
210
|
+
write_number(dy.round)
|
|
211
|
+
write_operator(:rlineto)
|
|
212
|
+
|
|
213
|
+
@current_x = cmd[:x]
|
|
214
|
+
@current_y = cmd[:y]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Encode a curveto command (cubic Bézier)
|
|
218
|
+
#
|
|
219
|
+
# Uses rrcurveto with 6 relative coordinates:
|
|
220
|
+
# dx1 dy1 dx2 dy2 dx3 dy3
|
|
221
|
+
#
|
|
222
|
+
# @param cmd [Hash] Command with :x1, :y1, :x2, :y2, :x, :y
|
|
223
|
+
def encode_curveto(cmd)
|
|
224
|
+
# Calculate relative coordinates for each control point
|
|
225
|
+
dx1 = cmd[:x1] - @current_x
|
|
226
|
+
dy1 = cmd[:y1] - @current_y
|
|
227
|
+
|
|
228
|
+
dx2 = cmd[:x2] - cmd[:x1]
|
|
229
|
+
dy2 = cmd[:y2] - cmd[:y1]
|
|
230
|
+
|
|
231
|
+
dx3 = cmd[:x] - cmd[:x2]
|
|
232
|
+
dy3 = cmd[:y] - cmd[:y2]
|
|
233
|
+
|
|
234
|
+
# Write operands
|
|
235
|
+
write_number(dx1.round)
|
|
236
|
+
write_number(dy1.round)
|
|
237
|
+
write_number(dx2.round)
|
|
238
|
+
write_number(dy2.round)
|
|
239
|
+
write_number(dx3.round)
|
|
240
|
+
write_number(dy3.round)
|
|
241
|
+
|
|
242
|
+
# Write operator
|
|
243
|
+
write_operator(:rrcurveto)
|
|
244
|
+
|
|
245
|
+
@current_x = cmd[:x]
|
|
246
|
+
@current_y = cmd[:y]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Write a number to the CharString
|
|
250
|
+
#
|
|
251
|
+
# Numbers are encoded in various formats based on their range:
|
|
252
|
+
# - -107 to +107: Single byte (32-246)
|
|
253
|
+
# - -1131 to +1131: Two bytes (247-254 + byte)
|
|
254
|
+
# - -32768 to +32767: Three bytes (28 + 2 bytes)
|
|
255
|
+
# - Otherwise: Five bytes (255 + 4 bytes as 16.16 fixed)
|
|
256
|
+
#
|
|
257
|
+
# @param value [Integer, Float] Number to encode
|
|
258
|
+
def write_number(value)
|
|
259
|
+
# Convert float to integer if it's effectively an integer
|
|
260
|
+
value = value.round if value.is_a?(Float) && value == value.round
|
|
261
|
+
|
|
262
|
+
if value.is_a?(Float)
|
|
263
|
+
# Real number - use 5-byte format (16.16 fixed point)
|
|
264
|
+
write_real(value)
|
|
265
|
+
elsif value >= -107 && value <= 107
|
|
266
|
+
# Single byte format: 32-246 represents -107 to +107
|
|
267
|
+
@output.putc(value + 139)
|
|
268
|
+
elsif value >= 108 && value <= 1131
|
|
269
|
+
# Positive two-byte format: 247-250
|
|
270
|
+
adjusted = value - 108
|
|
271
|
+
b0 = 247 + (adjusted / 256)
|
|
272
|
+
b1 = adjusted % 256
|
|
273
|
+
@output.putc(b0)
|
|
274
|
+
@output.putc(b1)
|
|
275
|
+
elsif value >= -1131 && value <= -108
|
|
276
|
+
# Negative two-byte format: 251-254
|
|
277
|
+
adjusted = -value - 108
|
|
278
|
+
b0 = 251 + (adjusted / 256)
|
|
279
|
+
b1 = adjusted % 256
|
|
280
|
+
@output.putc(b0)
|
|
281
|
+
@output.putc(b1)
|
|
282
|
+
elsif value >= -32768 && value <= 32767
|
|
283
|
+
# Three-byte signed integer
|
|
284
|
+
@output.putc(28)
|
|
285
|
+
@output.write([value].pack("s>")) # Signed 16-bit big-endian
|
|
286
|
+
else
|
|
287
|
+
# Five-byte signed integer (stored as 16.16 fixed point)
|
|
288
|
+
write_real(value.to_f)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Write a real number (5-byte format)
|
|
293
|
+
#
|
|
294
|
+
# @param value [Float] Real number
|
|
295
|
+
def write_real(value)
|
|
296
|
+
# Convert to 16.16 fixed point
|
|
297
|
+
fixed = (value * 65536.0).round
|
|
298
|
+
|
|
299
|
+
@output.putc(255)
|
|
300
|
+
@output.write([fixed].pack("l>")) # Signed 32-bit big-endian
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Write an operator to the CharString
|
|
304
|
+
#
|
|
305
|
+
# @param operator [Symbol] Operator name
|
|
306
|
+
def write_operator(operator)
|
|
307
|
+
if OPERATORS.key?(operator)
|
|
308
|
+
# Single-byte operator
|
|
309
|
+
@output.putc(OPERATORS[operator])
|
|
310
|
+
elsif TWO_BYTE_OPERATORS.key?(operator)
|
|
311
|
+
# Two-byte operator
|
|
312
|
+
bytes = TWO_BYTE_OPERATORS[operator]
|
|
313
|
+
@output.putc(bytes[0])
|
|
314
|
+
@output.putc(bytes[1])
|
|
315
|
+
else
|
|
316
|
+
raise ArgumentError, "Unknown operator: #{operator}"
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "index"
|
|
4
|
+
require_relative "charstring"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
class Cff
|
|
9
|
+
# CharStrings INDEX wrapper
|
|
10
|
+
#
|
|
11
|
+
# This class wraps the CharStrings INDEX to provide convenient access
|
|
12
|
+
# to individual CharString objects. The CharStrings INDEX contains the
|
|
13
|
+
# glyph outline programs (Type 2 CharStrings) for each glyph in the font.
|
|
14
|
+
#
|
|
15
|
+
# CharStrings Format:
|
|
16
|
+
# - INDEX structure containing binary CharString data
|
|
17
|
+
# - Each entry is a Type 2 CharString program
|
|
18
|
+
# - Number of entries typically matches the number of glyphs
|
|
19
|
+
# - Index 0 is typically .notdef glyph
|
|
20
|
+
#
|
|
21
|
+
# Usage:
|
|
22
|
+
# 1. Create from raw CharStrings INDEX data
|
|
23
|
+
# 2. Provide Private DICT and subroutine INDEXes for interpretation
|
|
24
|
+
# 3. Access individual CharStrings by glyph index
|
|
25
|
+
#
|
|
26
|
+
# Reference: CFF specification section 16 "Local/Global Subrs INDEXes"
|
|
27
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
|
|
28
|
+
#
|
|
29
|
+
# @example Using CharStringsIndex
|
|
30
|
+
# # Get CharStrings INDEX from CFF table
|
|
31
|
+
# charstrings_offset = top_dict.charstrings
|
|
32
|
+
# io = StringIO.new(cff.raw_data)
|
|
33
|
+
# io.seek(charstrings_offset)
|
|
34
|
+
# charstrings_index = CharstringsIndex.new(io, start_offset:
|
|
35
|
+
# charstrings_offset)
|
|
36
|
+
#
|
|
37
|
+
# # Get a specific CharString
|
|
38
|
+
# charstring = charstrings_index.charstring_at(
|
|
39
|
+
# glyph_index,
|
|
40
|
+
# private_dict,
|
|
41
|
+
# global_subrs,
|
|
42
|
+
# local_subrs
|
|
43
|
+
# )
|
|
44
|
+
#
|
|
45
|
+
# # Access CharString properties
|
|
46
|
+
# puts charstring.width
|
|
47
|
+
# puts charstring.bounding_box
|
|
48
|
+
# charstring.to_commands.each { |cmd| puts cmd.inspect }
|
|
49
|
+
class CharstringsIndex < Index
|
|
50
|
+
# Get a CharString object at the specified glyph index
|
|
51
|
+
#
|
|
52
|
+
# This method retrieves the binary CharString data at the given index
|
|
53
|
+
# and interprets it as a Type 2 CharString program.
|
|
54
|
+
#
|
|
55
|
+
# @param index [Integer] Glyph index (0-based, 0 is typically .notdef)
|
|
56
|
+
# @param private_dict [PrivateDict] Private DICT for width defaults
|
|
57
|
+
# @param global_subrs [Index] Global subroutines INDEX
|
|
58
|
+
# @param local_subrs [Index, nil] Local subroutines INDEX (optional)
|
|
59
|
+
# @return [CharString, nil] Interpreted CharString object, or nil if
|
|
60
|
+
# index is out of bounds
|
|
61
|
+
#
|
|
62
|
+
# @example Getting a CharString
|
|
63
|
+
# charstring = charstrings_index.charstring_at(
|
|
64
|
+
# 42,
|
|
65
|
+
# private_dict,
|
|
66
|
+
# global_subrs,
|
|
67
|
+
# local_subrs
|
|
68
|
+
# )
|
|
69
|
+
# puts "Width: #{charstring.width}"
|
|
70
|
+
# puts "Bounding box: #{charstring.bounding_box.inspect}"
|
|
71
|
+
def charstring_at(index, private_dict, global_subrs, local_subrs = nil)
|
|
72
|
+
data = self[index]
|
|
73
|
+
return nil unless data
|
|
74
|
+
|
|
75
|
+
CharString.new(data, private_dict, global_subrs, local_subrs)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get all CharStrings as an array of CharString objects
|
|
79
|
+
#
|
|
80
|
+
# This method interprets all CharStrings in the INDEX. Use with
|
|
81
|
+
# caution for fonts with many glyphs as this can be memory-intensive.
|
|
82
|
+
#
|
|
83
|
+
# @param private_dict [PrivateDict] Private DICT for width defaults
|
|
84
|
+
# @param global_subrs [Index] Global subroutines INDEX
|
|
85
|
+
# @param local_subrs [Index, nil] Local subroutines INDEX (optional)
|
|
86
|
+
# @return [Array<CharString>] Array of interpreted CharString objects
|
|
87
|
+
#
|
|
88
|
+
# @example Getting all CharStrings
|
|
89
|
+
# charstrings = charstrings_index.all_charstrings(
|
|
90
|
+
# private_dict,
|
|
91
|
+
# global_subrs,
|
|
92
|
+
# local_subrs
|
|
93
|
+
# )
|
|
94
|
+
# charstrings.each_with_index do |cs, i|
|
|
95
|
+
# puts "Glyph #{i}: width=#{cs.width}, bbox=#{cs.bounding_box}"
|
|
96
|
+
# end
|
|
97
|
+
def all_charstrings(private_dict, global_subrs, local_subrs = nil)
|
|
98
|
+
Array.new(count) do |i|
|
|
99
|
+
charstring_at(i, private_dict, global_subrs, local_subrs)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Iterate over each CharString in the INDEX
|
|
104
|
+
#
|
|
105
|
+
# This method yields each CharString as it is interpreted, which is
|
|
106
|
+
# more memory-efficient than loading all at once.
|
|
107
|
+
#
|
|
108
|
+
# @param private_dict [PrivateDict] Private DICT for width defaults
|
|
109
|
+
# @param global_subrs [Index] Global subroutines INDEX
|
|
110
|
+
# @param local_subrs [Index, nil] Local subroutines INDEX (optional)
|
|
111
|
+
# @yield [CharString, Integer] Interpreted CharString and its index
|
|
112
|
+
# @return [Enumerator] If no block given
|
|
113
|
+
#
|
|
114
|
+
# @example Iterating over CharStrings
|
|
115
|
+
# charstrings_index.each_charstring(private_dict, global_subrs,
|
|
116
|
+
# local_subrs) do |cs, index|
|
|
117
|
+
# puts "Glyph #{index}: #{cs.bounding_box}"
|
|
118
|
+
# end
|
|
119
|
+
def each_charstring(private_dict, global_subrs, local_subrs = nil)
|
|
120
|
+
unless block_given?
|
|
121
|
+
return enum_for(:each_charstring, private_dict, global_subrs,
|
|
122
|
+
local_subrs)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
count.times do |i|
|
|
126
|
+
charstring = charstring_at(i, private_dict, global_subrs,
|
|
127
|
+
local_subrs)
|
|
128
|
+
yield charstring, i if charstring
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Get the number of glyphs (CharStrings) in this INDEX
|
|
133
|
+
#
|
|
134
|
+
# This is typically the same as the number of glyphs in the font.
|
|
135
|
+
#
|
|
136
|
+
# @return [Integer] Number of glyphs
|
|
137
|
+
def glyph_count
|
|
138
|
+
count
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Check if a glyph index is valid
|
|
142
|
+
#
|
|
143
|
+
# @param index [Integer] Glyph index to check
|
|
144
|
+
# @return [Boolean] True if index is valid
|
|
145
|
+
def valid_glyph_index?(index)
|
|
146
|
+
index >= 0 && index < count
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Get the size of a CharString in bytes
|
|
150
|
+
#
|
|
151
|
+
# This returns the size of the binary CharString data without
|
|
152
|
+
# interpreting it.
|
|
153
|
+
#
|
|
154
|
+
# @param index [Integer] Glyph index
|
|
155
|
+
# @return [Integer, nil] Size in bytes, or nil if index is invalid
|
|
156
|
+
def charstring_size(index)
|
|
157
|
+
item_size(index)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|