fontisan 0.4.43 → 0.4.44
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/TODO.bug-fixes/12-type1-generators-duck-typing.md +80 -0
- data/TODO.bug-fixes/13-hint-extractor-duck-typing.md +30 -0
- data/TODO.bug-fixes/14-font-interface-type-checks.md +82 -0
- data/TODO.bug-fixes/15-glyph-type-checks-and-simple-glyph-points-bug.md +49 -0
- data/TODO.bug-fixes/16-variation-table-checks.md +31 -0
- data/TODO.bug-fixes/17-ufo-and-stitcher-cleanup.md +32 -0
- data/TODO.bug-fixes/18-generic-value-and-io-checks.md +56 -0
- data/TODO.bug-fixes/19-serialization-migration.md +52 -0
- data/TODO.bug-fixes/20-variation-validator-type-checks.md +29 -0
- data/TODO.bug-fixes/21-xml-builder-send.md +24 -0
- data/TODO.bug-fixes/22-type1-converter-font-info.md +25 -0
- data/TODO.bug-fixes/23-boundary-type-checks.md +31 -0
- data/TODO.bug-fixes/24-validator-dynamic-field-key.md +25 -0
- data/TODO.bug-fixes/25-serialization-migration.md +49 -0
- data/TODO.bug-fixes/26-variation-preserver-spec-migration.md +25 -0
- data/TODO.bug-fixes/27-table-class-registry.md +50 -0
- data/TODO.bug-fixes/28-spec-doubles-cleanup.md +40 -0
- data/TODO.bug-fixes/29-strategy-registries.md +30 -0
- data/TODO.bug-fixes/README.md +24 -0
- data/lib/fontisan/binary/base_record.rb +13 -12
- data/lib/fontisan/collection/builder.rb +3 -5
- data/lib/fontisan/collection/dfont_builder.rb +4 -11
- data/lib/fontisan/collection/writer.rb +1 -1
- data/lib/fontisan/commands/pack_command.rb +4 -4
- data/lib/fontisan/commands/unpack_command.rb +1 -1
- data/lib/fontisan/commands/validate_command.rb +1 -1
- data/lib/fontisan/converters/format_converter.rb +2 -2
- data/lib/fontisan/converters/glyf_table_builder.rb +1 -1
- data/lib/fontisan/converters/outline_converter.rb +2 -6
- data/lib/fontisan/converters/table_copier.rb +4 -12
- data/lib/fontisan/converters/type1_converter.rb +12 -37
- data/lib/fontisan/converters/woff2_encoder.rb +8 -10
- data/lib/fontisan/converters/woff_writer.rb +3 -3
- data/lib/fontisan/export/exporter.rb +8 -9
- data/lib/fontisan/export/table_serializer.rb +71 -186
- data/lib/fontisan/export/transformers/font_to_ttx.rb +18 -18
- data/lib/fontisan/export/transformers/head_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/hhea_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/maxp_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/name_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/os2_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/post_transformer.rb +1 -1
- data/lib/fontisan/export/ttx_generator.rb +36 -37
- data/lib/fontisan/font_writer.rb +1 -1
- data/lib/fontisan/glyph_accessor.rb +7 -9
- data/lib/fontisan/hints/postscript_hint_extractor.rb +24 -64
- data/lib/fontisan/hints/truetype_hint_extractor.rb +1 -1
- data/lib/fontisan/models/ttx/tables/binary_table.rb +1 -1
- data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
- data/lib/fontisan/sfnt_font.rb +2 -0
- data/lib/fontisan/sfnt_source.rb +28 -0
- data/lib/fontisan/stitcher/source.rb +8 -10
- data/lib/fontisan/subset/builder.rb +2 -2
- data/lib/fontisan/subset/table_strategy/cff2.rb +2 -3
- data/lib/fontisan/svg_to_glyf/assembler.rb +59 -20
- data/lib/fontisan/svg_to_glyf/document.rb +28 -9
- data/lib/fontisan/svg_to_glyf/geometry/normalizer.rb +34 -17
- data/lib/fontisan/tables/cff/charstring.rb +3 -0
- data/lib/fontisan/tables/cff/dict.rb +6 -3
- data/lib/fontisan/tables/cff/index.rb +1 -1
- data/lib/fontisan/tables/cff/private_dict_writer.rb +2 -2
- data/lib/fontisan/tables/cff/table_builder.rb +1 -1
- data/lib/fontisan/tables/cff.rb +3 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +2 -4
- data/lib/fontisan/tables/cff2.rb +3 -0
- data/lib/fontisan/tables/cmap.rb +3 -0
- data/lib/fontisan/tables/cvar.rb +3 -0
- data/lib/fontisan/tables/fvar.rb +3 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +9 -0
- data/lib/fontisan/tables/glyf.rb +12 -8
- data/lib/fontisan/tables/glyf_table.rb +1 -1
- data/lib/fontisan/tables/gvar.rb +3 -0
- data/lib/fontisan/tables/head.rb +3 -0
- data/lib/fontisan/tables/hhea.rb +3 -0
- data/lib/fontisan/tables/hmtx.rb +3 -7
- data/lib/fontisan/tables/hvar.rb +4 -0
- data/lib/fontisan/tables/loca.rb +3 -0
- data/lib/fontisan/tables/maxp.rb +3 -0
- data/lib/fontisan/tables/mvar.rb +3 -0
- data/lib/fontisan/tables/name.rb +3 -0
- data/lib/fontisan/tables/os2.rb +3 -0
- data/lib/fontisan/tables/post.rb +3 -0
- data/lib/fontisan/tables/registry.rb +71 -0
- data/lib/fontisan/tables/vvar.rb +3 -0
- data/lib/fontisan/tables.rb +2 -0
- data/lib/fontisan/type1/afm_generator.rb +12 -65
- data/lib/fontisan/type1/charstring_converter.rb +2 -2
- data/lib/fontisan/type1/conversion_options.rb +3 -2
- data/lib/fontisan/type1/decryptor.rb +1 -1
- data/lib/fontisan/type1/generator.rb +14 -9
- data/lib/fontisan/type1/inf_generator.rb +10 -35
- data/lib/fontisan/type1/pfa_generator.rb +36 -26
- data/lib/fontisan/type1/pfb_generator.rb +24 -12
- data/lib/fontisan/type1/pfm_generator.rb +14 -31
- data/lib/fontisan/type1/ttf_to_type1_converter.rb +30 -24
- data/lib/fontisan/ufo/bounds.rb +91 -0
- data/lib/fontisan/ufo/compile/fvar.rb +2 -2
- data/lib/fontisan/ufo/contour.rb +5 -0
- data/lib/fontisan/ufo/convert/from_bin_data.rb +15 -29
- data/lib/fontisan/ufo/info.rb +4 -0
- data/lib/fontisan/ufo/point.rb +0 -7
- data/lib/fontisan/ufo.rb +1 -0
- data/lib/fontisan/utilities/brotli_wrapper.rb +3 -5
- data/lib/fontisan/utilities/padding.rb +9 -8
- data/lib/fontisan/validation/collection_validator.rb +2 -2
- data/lib/fontisan/validators/validator.rb +4 -4
- data/lib/fontisan/variable/delta_applicator.rb +4 -11
- data/lib/fontisan/variable/metric_delta_processor.rb +3 -3
- data/lib/fontisan/variable/static_font_builder.rb +19 -23
- data/lib/fontisan/variation/instance_font_wrapper.rb +41 -0
- data/lib/fontisan/variation/instance_writer.rb +19 -17
- data/lib/fontisan/variation/metrics_adjuster.rb +17 -20
- data/lib/fontisan/variation/validator.rb +26 -38
- data/lib/fontisan/variation/variable_svg_generator.rb +0 -48
- data/lib/fontisan/variation/variation_preserver.rb +2 -3
- data/lib/fontisan/variation.rb +1 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/sfnt_checksum.rb +1 -1
- data/lib/fontisan/woff2/table_transformer.rb +22 -29
- data/lib/fontisan/woff2_font.rb +9 -8
- data/lib/fontisan/woff_font.rb +2 -0
- data/lib/fontisan.rb +1 -0
- metadata +24 -2
|
@@ -56,24 +56,23 @@ module Fontisan
|
|
|
56
56
|
def extract(charstring)
|
|
57
57
|
return [] if charstring.nil?
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
charstring.data
|
|
62
|
-
elsif charstring.respond_to?(:bytes)
|
|
63
|
-
charstring.bytes
|
|
64
|
-
elsif charstring.is_a?(String)
|
|
65
|
-
charstring.bytes
|
|
66
|
-
else
|
|
67
|
-
return []
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
return [] if bytes.empty?
|
|
59
|
+
bytes = charstring_to_bytes(charstring)
|
|
60
|
+
return [] if bytes.nil? || bytes.empty?
|
|
71
61
|
|
|
72
62
|
parse_charstring(bytes)
|
|
73
63
|
end
|
|
74
64
|
|
|
75
65
|
private
|
|
76
66
|
|
|
67
|
+
# Coerce a CharString input to an Array<Integer> of bytes.
|
|
68
|
+
# Accepts a String (raw bytes) or a CFF::CharString.
|
|
69
|
+
def charstring_to_bytes(charstring)
|
|
70
|
+
return charstring.bytes if charstring.is_a?(String)
|
|
71
|
+
return charstring.data if charstring.is_a?(Tables::Cff::CharString)
|
|
72
|
+
|
|
73
|
+
raise TypeError, "Unsupported charstring type: #{charstring.class}"
|
|
74
|
+
end
|
|
75
|
+
|
|
77
76
|
# Parse CharString bytes to extract hints
|
|
78
77
|
#
|
|
79
78
|
# @param bytes [Array<Integer>] CharString bytes
|
|
@@ -289,58 +288,19 @@ module Fontisan
|
|
|
289
288
|
|
|
290
289
|
# Extract hint-related parameters from Private DICT
|
|
291
290
|
# These are the key hinting parameters in CFF
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
hints[:family_other_blues] =
|
|
306
|
-
private_dict.family_other_blues
|
|
307
|
-
end
|
|
308
|
-
if private_dict.respond_to?(:blue_scale)
|
|
309
|
-
hints[:blue_scale] =
|
|
310
|
-
private_dict.blue_scale
|
|
311
|
-
end
|
|
312
|
-
if private_dict.respond_to?(:blue_shift)
|
|
313
|
-
hints[:blue_shift] =
|
|
314
|
-
private_dict.blue_shift
|
|
315
|
-
end
|
|
316
|
-
if private_dict.respond_to?(:blue_fuzz)
|
|
317
|
-
hints[:blue_fuzz] =
|
|
318
|
-
private_dict.blue_fuzz
|
|
319
|
-
end
|
|
320
|
-
if private_dict.respond_to?(:std_hw)
|
|
321
|
-
hints[:std_hw] =
|
|
322
|
-
private_dict.std_hw
|
|
323
|
-
end
|
|
324
|
-
if private_dict.respond_to?(:std_vw)
|
|
325
|
-
hints[:std_vw] =
|
|
326
|
-
private_dict.std_vw
|
|
327
|
-
end
|
|
328
|
-
if private_dict.respond_to?(:stem_snap_h)
|
|
329
|
-
hints[:stem_snap_h] =
|
|
330
|
-
private_dict.stem_snap_h
|
|
331
|
-
end
|
|
332
|
-
if private_dict.respond_to?(:stem_snap_v)
|
|
333
|
-
hints[:stem_snap_v] =
|
|
334
|
-
private_dict.stem_snap_v
|
|
335
|
-
end
|
|
336
|
-
if private_dict.respond_to?(:force_bold)
|
|
337
|
-
hints[:force_bold] =
|
|
338
|
-
private_dict.force_bold
|
|
339
|
-
end
|
|
340
|
-
if private_dict.respond_to?(:language_group)
|
|
341
|
-
hints[:language_group] =
|
|
342
|
-
private_dict.language_group
|
|
343
|
-
end
|
|
291
|
+
hints[:blue_values] = private_dict.blue_values
|
|
292
|
+
hints[:other_blues] = private_dict.other_blues
|
|
293
|
+
hints[:family_blues] = private_dict.family_blues
|
|
294
|
+
hints[:family_other_blues] = private_dict.family_other_blues
|
|
295
|
+
hints[:blue_scale] = private_dict.blue_scale
|
|
296
|
+
hints[:blue_shift] = private_dict.blue_shift
|
|
297
|
+
hints[:blue_fuzz] = private_dict.blue_fuzz
|
|
298
|
+
hints[:std_hw] = private_dict.std_hw
|
|
299
|
+
hints[:std_vw] = private_dict.std_vw
|
|
300
|
+
hints[:stem_snap_h] = private_dict.stem_snap_h
|
|
301
|
+
hints[:stem_snap_v] = private_dict.stem_snap_v
|
|
302
|
+
hints[:force_bold] = private_dict.force_bold?
|
|
303
|
+
hints[:language_group] = private_dict.language_group
|
|
344
304
|
|
|
345
305
|
hints.compact
|
|
346
306
|
rescue StandardError => e
|
|
@@ -63,7 +63,7 @@ module Fontisan
|
|
|
63
63
|
# @return [Array<Hint>] Extracted hints
|
|
64
64
|
def extract(glyph)
|
|
65
65
|
return [] if glyph.nil? || glyph.empty?
|
|
66
|
-
return [] unless glyph.
|
|
66
|
+
return [] unless glyph.is_a?(Fontisan::Tables::SimpleGlyph)
|
|
67
67
|
|
|
68
68
|
instructions = glyph.instructions || []
|
|
69
69
|
return [] if instructions.empty?
|
|
@@ -18,7 +18,7 @@ module Fontisan
|
|
|
18
18
|
# based on table tag
|
|
19
19
|
def to_xml(options = {})
|
|
20
20
|
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
|
21
|
-
xml.
|
|
21
|
+
xml.public_send(tag.to_sym) do
|
|
22
22
|
xml.hexdata do
|
|
23
23
|
xml.text("\n #{format_hexdata(hexdata)}\n ")
|
|
24
24
|
end
|
|
@@ -127,7 +127,7 @@ module Fontisan
|
|
|
127
127
|
replacements = []
|
|
128
128
|
|
|
129
129
|
patterns.each do |pattern|
|
|
130
|
-
if glyph_id && pattern.
|
|
130
|
+
if glyph_id && pattern.is_a?(PatternAnalyzer::Pattern) && pattern.positions.is_a?(Hash)
|
|
131
131
|
# Use exact positions from pattern analysis for this glyph
|
|
132
132
|
glyph_positions = pattern.positions[glyph_id] || []
|
|
133
133
|
|
data/lib/fontisan/sfnt_font.rb
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
# Marks a class as providing the SFNT font source interface.
|
|
5
|
+
#
|
|
6
|
+
# SfntFont, WoffFont, and Woff2Font all expose the same table-access
|
|
7
|
+
# surface (#table, #tables, #table_data, #table_names, #has_table?)
|
|
8
|
+
# but don't share a common base class (WOFF/WOFF2 wrap an SfntFont
|
|
9
|
+
# but inherit from BinData::Record directly).
|
|
10
|
+
#
|
|
11
|
+
# Including this module lets callers type-check with `is_a?(SfntSource)`
|
|
12
|
+
# instead of `respond_to?(:table)` duck-typing.
|
|
13
|
+
module SfntSource
|
|
14
|
+
# @param tag [String] table tag
|
|
15
|
+
# @return [Object, nil] parsed table or nil
|
|
16
|
+
def table(tag); end
|
|
17
|
+
|
|
18
|
+
# @return [Hash<String, String>] raw table bytes by tag
|
|
19
|
+
def table_data(tag = nil); end
|
|
20
|
+
|
|
21
|
+
# @return [Array<String>] table tags present in this font
|
|
22
|
+
def table_names; end
|
|
23
|
+
|
|
24
|
+
# @param tag [String] table tag
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def has_table?(tag); end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -80,7 +80,7 @@ module Fontisan
|
|
|
80
80
|
# @return [Symbol]
|
|
81
81
|
def bitmap_mode
|
|
82
82
|
return :none if @font.is_a?(Fontisan::Ufo::Font)
|
|
83
|
-
return :none unless @font.
|
|
83
|
+
return :none unless @font.is_a?(SfntSource)
|
|
84
84
|
|
|
85
85
|
has_cbdt = @font.has_table?("CBDT") && @font.has_table?("CBLC")
|
|
86
86
|
has_glyf = @font.has_table?("glyf") || @font.has_table?("CFF ")
|
|
@@ -190,7 +190,7 @@ module Fontisan
|
|
|
190
190
|
cache[:loca] = @font.table("loca")
|
|
191
191
|
cache[:glyf] = @font.table("glyf")
|
|
192
192
|
# loca needs head's index_to_loc_format to size its offsets
|
|
193
|
-
if cache[:
|
|
193
|
+
if cache[:head]
|
|
194
194
|
cache[:loca].parse_with_context(
|
|
195
195
|
cache[:head].index_to_loc_format,
|
|
196
196
|
@font.table("maxp")&.num_glyphs || 0,
|
|
@@ -217,15 +217,13 @@ module Fontisan
|
|
|
217
217
|
num_h_metrics = hhea&.number_of_h_metrics || 1
|
|
218
218
|
num_glyphs = maxp&.num_glyphs || 0
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
hmtx.parse_with_context(num_h_metrics, num_glyphs)
|
|
222
|
-
end
|
|
220
|
+
hmtx.parse_with_context(num_h_metrics, num_glyphs)
|
|
223
221
|
|
|
224
222
|
# Fallback advance width when hmtx lookup fails for a GID.
|
|
225
223
|
# Per the OpenType spec, glyphs at GID >= numberOfHMetrics
|
|
226
224
|
# inherit the last LongHorMetric's advanceWidth. If the table
|
|
227
225
|
# is empty or corrupt, fall back to the font's unitsPerEm.
|
|
228
|
-
fallback_width = if hmtx.
|
|
226
|
+
fallback_width = if hmtx.h_metrics&.any?
|
|
229
227
|
hmtx.h_metrics.last[:advance_width]
|
|
230
228
|
else
|
|
231
229
|
head&.units_per_em || 1000
|
|
@@ -284,9 +282,9 @@ module Fontisan
|
|
|
284
282
|
glyph = Fontisan::Ufo::Glyph.new(name: name)
|
|
285
283
|
glyph.width = glyph_width(gid)
|
|
286
284
|
|
|
287
|
-
if raw.
|
|
285
|
+
if raw.is_a?(Fontisan::Tables::SimpleGlyph) && raw.simple?
|
|
288
286
|
copy_simple_contours(raw, glyph)
|
|
289
|
-
elsif raw.
|
|
287
|
+
elsif raw.is_a?(Fontisan::Tables::CompoundGlyph) && raw.compound?
|
|
290
288
|
flatten_compound_into(raw, glyph, cache, Set.new)
|
|
291
289
|
end
|
|
292
290
|
|
|
@@ -372,9 +370,9 @@ module Fontisan
|
|
|
372
370
|
|
|
373
371
|
matrix = component.transformation_matrix
|
|
374
372
|
|
|
375
|
-
if raw.
|
|
373
|
+
if raw.is_a?(Fontisan::Tables::SimpleGlyph) && raw.simple?
|
|
376
374
|
flatten_simple_component(raw, ufo_glyph, matrix)
|
|
377
|
-
elsif raw.
|
|
375
|
+
elsif raw.is_a?(Fontisan::Tables::CompoundGlyph) && raw.compound?
|
|
378
376
|
flatten_compound_into(raw, ufo_glyph, cache, visited, depth + 1)
|
|
379
377
|
end
|
|
380
378
|
end
|
|
@@ -110,8 +110,8 @@ module Fontisan
|
|
|
110
110
|
def validate_input!
|
|
111
111
|
raise ArgumentError, "Font cannot be nil" if font.nil?
|
|
112
112
|
|
|
113
|
-
unless font.
|
|
114
|
-
raise ArgumentError, "Font must
|
|
113
|
+
unless font.is_a?(SfntSource)
|
|
114
|
+
raise ArgumentError, "Font must be an SfntSource instance"
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
# Validate options
|
|
@@ -55,13 +55,12 @@ module Fontisan
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def self.raw_bytes_from_font(font)
|
|
58
|
-
|
|
59
|
-
t&.raw_data
|
|
58
|
+
font.table("CFF2")&.raw_data
|
|
60
59
|
end
|
|
61
60
|
private_class_method :raw_bytes_from_font
|
|
62
61
|
|
|
63
62
|
def self.raw_bytes_from_table(table)
|
|
64
|
-
table&.raw_data
|
|
63
|
+
table&.raw_data
|
|
65
64
|
end
|
|
66
65
|
private_class_method :raw_bytes_from_table
|
|
67
66
|
|
|
@@ -7,7 +7,10 @@ module Fontisan
|
|
|
7
7
|
# path data + transforms
|
|
8
8
|
# → Path::Parser.parse → [Command]
|
|
9
9
|
# → Path::ContourBuilder.build → [Ufo::Contour]
|
|
10
|
-
# →
|
|
10
|
+
# → measure content bounds (Ufo::Bounds)
|
|
11
|
+
# → union content bounds with the SVG viewBox bounds
|
|
12
|
+
# → Normalizer maps the unioned space into the em-square
|
|
13
|
+
# → compose with the accumulated group transform
|
|
11
14
|
# → round to Integer
|
|
12
15
|
# → Ufo::Glyph
|
|
13
16
|
#
|
|
@@ -16,6 +19,8 @@ module Fontisan
|
|
|
16
19
|
# composes the normalizer with the group transform and runs the
|
|
17
20
|
# pipeline once per path.
|
|
18
21
|
class Assembler
|
|
22
|
+
DEFAULT_VIEWBOX = Ufo::Bounds.new(min_x: 0, min_y: 0, max_x: DEFAULT_UPM, max_y: DEFAULT_UPM)
|
|
23
|
+
|
|
19
24
|
attr_reader :upm
|
|
20
25
|
|
|
21
26
|
# @param upm [Integer] font units-per-em
|
|
@@ -28,15 +33,16 @@ module Fontisan
|
|
|
28
33
|
# @param path_data [String] SVG path d= attribute
|
|
29
34
|
# @param codepoint [Integer, nil] Unicode codepoint
|
|
30
35
|
# @param name [String, nil] glyph name
|
|
31
|
-
# @param viewbox [Hash{Symbol=>Float}, nil]
|
|
36
|
+
# @param viewbox [Ufo::Bounds, Hash{Symbol=>Float}, nil] SVG viewbox
|
|
32
37
|
# @param transform [Geometry::AffineTransform, nil] group transform
|
|
33
38
|
# @return [Fontisan::Ufo::Glyph]
|
|
34
39
|
def build_from_path_data(path_data, codepoint: nil, name: nil,
|
|
35
40
|
viewbox: nil, transform: nil)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
assemble(path_data: path_data,
|
|
42
|
+
viewbox: resolve_viewbox(viewbox),
|
|
43
|
+
group_transform: transform || Geometry::AffineTransform.identity,
|
|
44
|
+
codepoint: codepoint,
|
|
45
|
+
name: name)
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
# Build a glyph from an SVG file.
|
|
@@ -49,7 +55,11 @@ module Fontisan
|
|
|
49
55
|
codepoint ||= codepoint_from_filename(File.basename(file_path))
|
|
50
56
|
|
|
51
57
|
doc.each_path.with_object(nil) do |(data, transform), _|
|
|
52
|
-
return
|
|
58
|
+
return assemble(path_data: data,
|
|
59
|
+
viewbox: doc.viewbox || default_viewbox,
|
|
60
|
+
group_transform: transform,
|
|
61
|
+
codepoint: codepoint,
|
|
62
|
+
name: nil)
|
|
53
63
|
end
|
|
54
64
|
|
|
55
65
|
empty_glyph(codepoint)
|
|
@@ -73,18 +83,33 @@ module Fontisan
|
|
|
73
83
|
|
|
74
84
|
private
|
|
75
85
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def build_contours(path_data, final_transform)
|
|
86
|
+
# Shared pipeline used by every entry point. Parses the d-string,
|
|
87
|
+
# builds contours, computes the normalization bounds as the union
|
|
88
|
+
# of the actual content bounds and the declared viewBox bounds,
|
|
89
|
+
# applies the normalizer composed with the group transform, and
|
|
90
|
+
# assembles the result into a Ufo::Glyph.
|
|
91
|
+
def assemble(path_data:, viewbox:, group_transform:, codepoint:, name:)
|
|
85
92
|
commands = Path::Parser.parse(path_data)
|
|
86
93
|
contours = Path::ContourBuilder.new.build(commands)
|
|
87
|
-
|
|
94
|
+
norm_bounds = normalization_bounds(contours, viewbox)
|
|
95
|
+
final = normalizer_for(norm_bounds).final_transform(group_transform)
|
|
96
|
+
transformed = contours.map { |c| transform_contour(c, final) }
|
|
97
|
+
assemble_glyph(name || glyph_name_for(codepoint), transformed, codepoint)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Union the actual content bounds with the SVG viewBox bounds.
|
|
101
|
+
# Content outside the viewBox is preserved (not clipped) by
|
|
102
|
+
# expanding the normalization space to contain it; content inside
|
|
103
|
+
# a larger viewBox keeps its relative scale.
|
|
104
|
+
#
|
|
105
|
+
# @param contours [Array<Ufo::Contour>]
|
|
106
|
+
# @param viewbox [Ufo::Bounds]
|
|
107
|
+
# @return [Ufo::Bounds]
|
|
108
|
+
def normalization_bounds(contours, viewbox)
|
|
109
|
+
content = Ufo::Bounds.measure(contours)
|
|
110
|
+
return viewbox if content.empty?
|
|
111
|
+
|
|
112
|
+
content.union(viewbox)
|
|
88
113
|
end
|
|
89
114
|
|
|
90
115
|
def transform_contour(contour, transform)
|
|
@@ -96,9 +121,23 @@ module Fontisan
|
|
|
96
121
|
Fontisan::Ufo::Contour.new(points)
|
|
97
122
|
end
|
|
98
123
|
|
|
99
|
-
def normalizer_for(
|
|
100
|
-
Geometry::Normalizer.new(
|
|
101
|
-
|
|
124
|
+
def normalizer_for(bounds)
|
|
125
|
+
Geometry::Normalizer.new(bounds: bounds, upm: @upm)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Accept either a Ufo::Bounds (preferred) or a legacy hash with
|
|
129
|
+
# :width, :height (treated as origin 0,0). Returns a Bounds.
|
|
130
|
+
def resolve_viewbox(viewbox)
|
|
131
|
+
return default_viewbox unless viewbox
|
|
132
|
+
return viewbox if viewbox.is_a?(Ufo::Bounds)
|
|
133
|
+
|
|
134
|
+
Ufo::Bounds.new(min_x: 0, min_y: 0,
|
|
135
|
+
max_x: viewbox[:width].to_f,
|
|
136
|
+
max_y: viewbox[:height].to_f)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def default_viewbox
|
|
140
|
+
self.class.const_get(:DEFAULT_VIEWBOX)
|
|
102
141
|
end
|
|
103
142
|
|
|
104
143
|
def assemble_glyph(name, contours, codepoint)
|
|
@@ -9,7 +9,7 @@ module Fontisan
|
|
|
9
9
|
# transforms apply to which paths and what coordinate space the
|
|
10
10
|
# SVG defines (via viewBox).
|
|
11
11
|
class Document
|
|
12
|
-
attr_reader :
|
|
12
|
+
attr_reader :viewbox, :source
|
|
13
13
|
|
|
14
14
|
# @param xml [String] raw SVG XML
|
|
15
15
|
def self.from_xml(xml)
|
|
@@ -25,7 +25,7 @@ module Fontisan
|
|
|
25
25
|
def initialize(doc)
|
|
26
26
|
@doc = doc
|
|
27
27
|
@source = doc
|
|
28
|
-
extract_viewbox
|
|
28
|
+
@viewbox = extract_viewbox
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# Yield each <path> element's d= string along with the accumulated
|
|
@@ -41,17 +41,36 @@ module Fontisan
|
|
|
41
41
|
|
|
42
42
|
private
|
|
43
43
|
|
|
44
|
+
# Parse the SVG viewBox attribute. The SVG spec allows either
|
|
45
|
+
# whitespace- or comma-separated values: "0 0 1000 1000" or
|
|
46
|
+
# "0,0,1000,1000". The four components are min_x, min_y, width,
|
|
47
|
+
# height — all are significant for normalization.
|
|
48
|
+
#
|
|
49
|
+
# Falls back to the root element's width/height attributes (origin
|
|
50
|
+
# assumed to be 0,0). Returns nil if no geometry can be derived.
|
|
51
|
+
#
|
|
52
|
+
# @return [Ufo::Bounds, nil]
|
|
44
53
|
def extract_viewbox
|
|
45
54
|
root = @doc.root
|
|
46
|
-
|
|
55
|
+
return unless root
|
|
56
|
+
|
|
57
|
+
vb = root.attribute("viewBox")&.value
|
|
47
58
|
if vb
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
parts = vb.split(/[\s,]+/).map(&:to_f)
|
|
60
|
+
return if parts.length != 4
|
|
61
|
+
|
|
62
|
+
min_x, min_y, w, h = parts
|
|
63
|
+
return if w.zero? || h.zero?
|
|
64
|
+
|
|
65
|
+
return Ufo::Bounds.new(min_x: min_x, min_y: min_y,
|
|
66
|
+
max_x: min_x + w, max_y: min_y + h)
|
|
54
67
|
end
|
|
68
|
+
|
|
69
|
+
w = root.attribute("width")&.value&.to_f
|
|
70
|
+
h = root.attribute("height")&.value&.to_f
|
|
71
|
+
return if w.nil? || h.nil? || w.zero? || h.zero?
|
|
72
|
+
|
|
73
|
+
Ufo::Bounds.new(min_x: 0, min_y: 0, max_x: w, max_y: h)
|
|
55
74
|
end
|
|
56
75
|
|
|
57
76
|
# Recursively walk the XML tree. When a <g> has a transform=,
|
|
@@ -3,32 +3,49 @@
|
|
|
3
3
|
module Fontisan
|
|
4
4
|
module SvgToGlyf
|
|
5
5
|
module Geometry
|
|
6
|
-
# Computes the affine transform that maps SVG
|
|
7
|
-
# (Y-down, origin
|
|
8
|
-
# at bottom-left, scaled to UPM).
|
|
6
|
+
# Computes the affine transform that maps SVG coordinate space
|
|
7
|
+
# (Y-down, arbitrary origin) into font coordinate space (Y-up,
|
|
8
|
+
# origin at the bottom-left of the em-square, scaled to UPM).
|
|
9
9
|
#
|
|
10
|
-
# The normalization
|
|
11
|
-
#
|
|
12
|
-
#
|
|
10
|
+
# The normalization accepts a Ufo::Bounds describing the SVG
|
|
11
|
+
# coordinate extent to map into the em-square. That bounds may
|
|
12
|
+
# be the SVG viewBox, the actual content extents, or the union
|
|
13
|
+
# of both — Assembler decides which.
|
|
14
|
+
#
|
|
15
|
+
# The transform is:
|
|
16
|
+
#
|
|
17
|
+
# 1. Translate the source origin (min_x, min_y) to (0, 0).
|
|
18
|
+
# 2. Scale uniformly to font units: (upm/w, upm/h).
|
|
19
|
+
# 3. Flip Y across the half-height of the scaled space.
|
|
20
|
+
# 4. Translate up by upm so the top of the source maps to the
|
|
21
|
+
# top of the em-square.
|
|
22
|
+
#
|
|
23
|
+
# The resulting matrix is composed with the SVG document's
|
|
13
24
|
# accumulated group transform to produce the final per-point
|
|
14
25
|
# transform.
|
|
15
26
|
class Normalizer
|
|
16
|
-
attr_reader :
|
|
27
|
+
attr_reader :bounds, :upm
|
|
17
28
|
|
|
18
|
-
# @param
|
|
19
|
-
# @param
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@viewbox_width = viewbox_width.to_f
|
|
23
|
-
@viewbox_height = viewbox_height.to_f
|
|
29
|
+
# @param bounds [Ufo::Bounds] source coordinate extents
|
|
30
|
+
# @param upm [Integer, Float] font units-per-em
|
|
31
|
+
def initialize(bounds:, upm:)
|
|
32
|
+
@bounds = bounds
|
|
24
33
|
@upm = upm.to_f
|
|
25
34
|
end
|
|
26
35
|
|
|
27
|
-
# @return [AffineTransform] the
|
|
36
|
+
# @return [AffineTransform] the source→font normalization
|
|
28
37
|
def matrix
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
AffineTransform.
|
|
38
|
+
w = @bounds.width
|
|
39
|
+
h = @bounds.height
|
|
40
|
+
return AffineTransform.identity if w.zero? || h.zero?
|
|
41
|
+
|
|
42
|
+
sx = @upm / w
|
|
43
|
+
sy = @upm / h
|
|
44
|
+
# x' = sx * (x - min_x)
|
|
45
|
+
# y' = -sy * (y - min_y) + upm
|
|
46
|
+
AffineTransform.new(sx, 0, 0, -sy,
|
|
47
|
+
-sx * @bounds.min_x,
|
|
48
|
+
sy * @bounds.min_y + @upm)
|
|
32
49
|
end
|
|
33
50
|
|
|
34
51
|
# Compose the normalization with an SVG group transform,
|
|
@@ -36,6 +36,9 @@ module Fontisan
|
|
|
36
36
|
# puts charstring.path # => array of path commands
|
|
37
37
|
# bbox = charstring.bounding_box # => [xMin, yMin, xMax, yMax]
|
|
38
38
|
class CharString
|
|
39
|
+
# @return [String, Array<Integer>] Raw charstring data/bytes
|
|
40
|
+
attr_reader :data
|
|
41
|
+
|
|
39
42
|
# @return [Integer, nil] Glyph width (nil if using default width)
|
|
40
43
|
attr_reader :width
|
|
41
44
|
|
|
@@ -106,10 +106,13 @@ module Fontisan
|
|
|
106
106
|
@dict.values
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
#
|
|
109
|
+
# Expose the raw key/value pairs as a Hash. This is an accessor
|
|
110
|
+
# for the DICT's internal hash, NOT wire-format serialization —
|
|
111
|
+
# CFF DICT wire format is the binary operand/operator stream
|
|
112
|
+
# produced by to_binary_s.
|
|
110
113
|
#
|
|
111
|
-
# @return [Hash] Dictionary as hash
|
|
112
|
-
def
|
|
114
|
+
# @return [Hash] Dictionary as hash (dup of internal storage)
|
|
115
|
+
def raw_hash
|
|
113
116
|
@dict.dup
|
|
114
117
|
end
|
|
115
118
|
|
|
@@ -69,10 +69,10 @@ module Fontisan
|
|
|
69
69
|
#
|
|
70
70
|
# @param source_dict [PrivateDict] Source dictionary
|
|
71
71
|
def parse_source(source_dict)
|
|
72
|
-
return unless source_dict.
|
|
72
|
+
return unless source_dict.is_a?(Tables::Cff::Dict)
|
|
73
73
|
|
|
74
74
|
# Extract only non-hint params (subrs, widths)
|
|
75
|
-
@params = source_dict.
|
|
75
|
+
@params = source_dict.raw_hash.select do |k, _|
|
|
76
76
|
%i[subrs default_width_x nominal_width_x].include?(k)
|
|
77
77
|
end
|
|
78
78
|
end
|
data/lib/fontisan/tables/cff.rb
CHANGED
|
@@ -41,6 +41,9 @@ module Fontisan
|
|
|
41
41
|
# puts cff.font_count # => 1
|
|
42
42
|
# puts cff.header.version # => "1.0"
|
|
43
43
|
class Cff < Binary::BaseRecord
|
|
44
|
+
extend Registered
|
|
45
|
+
|
|
46
|
+
register_tag "CFF "
|
|
44
47
|
# Inner namespace autoloads — declared here so Cff::* constants
|
|
45
48
|
# resolve on first reference without require_relative.
|
|
46
49
|
autoload :CFFGlyph, "fontisan/tables/cff/cff_glyph"
|
|
@@ -161,9 +161,7 @@ module Fontisan
|
|
|
161
161
|
# @return [Integer] Total stem count (hstem + vstem)
|
|
162
162
|
def calculate_stem_count
|
|
163
163
|
return 0 unless @hint_set
|
|
164
|
-
|
|
165
|
-
# Get font-level hints (from private_dict_hints JSON)
|
|
166
|
-
return 0 unless @hint_set.respond_to?(:private_dict_hints)
|
|
164
|
+
return 0 unless @hint_set.is_a?(Models::HintSet)
|
|
167
165
|
|
|
168
166
|
begin
|
|
169
167
|
font_hints = JSON.parse(@hint_set.private_dict_hints || "{}")
|
|
@@ -194,7 +192,7 @@ module Fontisan
|
|
|
194
192
|
#
|
|
195
193
|
# @return [Boolean] True if private_dict_hints are present
|
|
196
194
|
def has_font_level_hints?
|
|
197
|
-
return false unless @hint_set.
|
|
195
|
+
return false unless @hint_set.is_a?(Models::HintSet)
|
|
198
196
|
|
|
199
197
|
hints = JSON.parse(@hint_set.private_dict_hints || "{}")
|
|
200
198
|
!hints.empty?
|
data/lib/fontisan/tables/cff2.rb
CHANGED
|
@@ -20,6 +20,9 @@ module Fontisan
|
|
|
20
20
|
# cff2 = Fontisan::Tables::Cff2.read(data)
|
|
21
21
|
# num_glyphs = cff2.glyph_count
|
|
22
22
|
class Cff2 < Binary::BaseRecord
|
|
23
|
+
extend Registered
|
|
24
|
+
|
|
25
|
+
register_tag "CFF2"
|
|
23
26
|
# Inner namespace autoloads — declared here so Cff2::* constants
|
|
24
27
|
# resolve on first reference without require_relative.
|
|
25
28
|
autoload :BlendOperator, "fontisan/tables/cff2/blend_operator"
|
data/lib/fontisan/tables/cmap.rb
CHANGED
data/lib/fontisan/tables/cvar.rb
CHANGED
|
@@ -20,6 +20,9 @@ module Fontisan
|
|
|
20
20
|
# cvar = Fontisan::Tables::Cvar.read(data)
|
|
21
21
|
# cvt_deltas = cvar.cvt_variations
|
|
22
22
|
class Cvar < Binary::BaseRecord
|
|
23
|
+
extend Registered
|
|
24
|
+
|
|
25
|
+
register_tag "cvar"
|
|
23
26
|
uint16 :major_version
|
|
24
27
|
uint16 :minor_version
|
|
25
28
|
uint16 :tuple_variation_count
|