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,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff
|
|
8
|
+
# CFF INDEX structure builder
|
|
9
|
+
#
|
|
10
|
+
# [`IndexBuilder`](lib/fontisan/tables/cff/index_builder.rb) constructs
|
|
11
|
+
# binary INDEX structures from arrays of data items. INDEX is a fundamental
|
|
12
|
+
# CFF data structure used for storing arrays of variable-length data.
|
|
13
|
+
#
|
|
14
|
+
# The builder calculates optimal offset sizes, constructs the offset array,
|
|
15
|
+
# and produces compact binary output.
|
|
16
|
+
#
|
|
17
|
+
# Structure produced:
|
|
18
|
+
# - count (Card16): Number of items
|
|
19
|
+
# - offSize (OffSize): Size of offset values (1-4 bytes)
|
|
20
|
+
# - offset[count+1] (Offset): Array of offsets to data
|
|
21
|
+
# - data: Concatenated data bytes
|
|
22
|
+
#
|
|
23
|
+
# Offsets are 1-based (first offset is always 1). The last offset points
|
|
24
|
+
# one byte past the end of the data.
|
|
25
|
+
#
|
|
26
|
+
# Reference: CFF specification section 5 "INDEX Data"
|
|
27
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
|
|
28
|
+
#
|
|
29
|
+
# @example Building an INDEX
|
|
30
|
+
# items = ["data1".b, "data2".b, "data3".b]
|
|
31
|
+
# index_data = Fontisan::Tables::Cff::IndexBuilder.build(items)
|
|
32
|
+
class IndexBuilder
|
|
33
|
+
# Build INDEX structure from array of binary strings
|
|
34
|
+
#
|
|
35
|
+
# @param items [Array<String>] Array of binary data items
|
|
36
|
+
# @return [String] Binary INDEX data
|
|
37
|
+
# @raise [ArgumentError] If items is not an Array
|
|
38
|
+
def self.build(items)
|
|
39
|
+
validate_items!(items)
|
|
40
|
+
|
|
41
|
+
return build_empty_index if items.empty?
|
|
42
|
+
|
|
43
|
+
# Calculate total data size
|
|
44
|
+
data_size = items.sum(&:bytesize)
|
|
45
|
+
|
|
46
|
+
# Calculate optimal offset size (1-4 bytes)
|
|
47
|
+
# Last offset will be data_size + 1 (1-based)
|
|
48
|
+
off_size = calculate_off_size(data_size + 1)
|
|
49
|
+
|
|
50
|
+
# Build offset array (count + 1 offsets)
|
|
51
|
+
offsets = build_offsets(items, off_size)
|
|
52
|
+
|
|
53
|
+
# Concatenate all data
|
|
54
|
+
data = items.join
|
|
55
|
+
|
|
56
|
+
# Assemble INDEX structure
|
|
57
|
+
output = StringIO.new("".b)
|
|
58
|
+
|
|
59
|
+
# Write count (Card16)
|
|
60
|
+
output.write([items.length].pack("n"))
|
|
61
|
+
|
|
62
|
+
# Write offSize (OffSize)
|
|
63
|
+
output.putc(off_size)
|
|
64
|
+
|
|
65
|
+
# Write offset array
|
|
66
|
+
offsets.each do |offset|
|
|
67
|
+
write_offset(output, offset, off_size)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Write data
|
|
71
|
+
output.write(data)
|
|
72
|
+
|
|
73
|
+
output.string
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Build an empty INDEX (count = 0)
|
|
77
|
+
#
|
|
78
|
+
# @return [String] Binary empty INDEX
|
|
79
|
+
def self.build_empty_index
|
|
80
|
+
# Empty INDEX has only count field (0)
|
|
81
|
+
[0].pack("n")
|
|
82
|
+
end
|
|
83
|
+
private_class_method :build_empty_index
|
|
84
|
+
|
|
85
|
+
# Validate items parameter
|
|
86
|
+
#
|
|
87
|
+
# @param items [Object] Items to validate
|
|
88
|
+
# @raise [ArgumentError] If items is invalid
|
|
89
|
+
def self.validate_items!(items)
|
|
90
|
+
raise ArgumentError, "items must be Array" unless items.is_a?(Array)
|
|
91
|
+
|
|
92
|
+
items.each_with_index do |item, i|
|
|
93
|
+
unless item.is_a?(String)
|
|
94
|
+
raise ArgumentError,
|
|
95
|
+
"item #{i} must be String, got: #{item.class}"
|
|
96
|
+
end
|
|
97
|
+
unless item.encoding == ::Encoding::BINARY
|
|
98
|
+
raise ArgumentError,
|
|
99
|
+
"item #{i} must have BINARY encoding, got: #{item.encoding}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
private_class_method :validate_items!
|
|
104
|
+
|
|
105
|
+
# Calculate optimal offset size for given maximum offset
|
|
106
|
+
#
|
|
107
|
+
# @param max_offset [Integer] Maximum offset value
|
|
108
|
+
# @return [Integer] Offset size (1-4 bytes)
|
|
109
|
+
def self.calculate_off_size(max_offset)
|
|
110
|
+
return 1 if max_offset <= 0xFF
|
|
111
|
+
return 2 if max_offset <= 0xFFFF
|
|
112
|
+
return 3 if max_offset <= 0xFFFFFF
|
|
113
|
+
|
|
114
|
+
4
|
|
115
|
+
end
|
|
116
|
+
private_class_method :calculate_off_size
|
|
117
|
+
|
|
118
|
+
# Build offset array from items
|
|
119
|
+
#
|
|
120
|
+
# Offsets are 1-based. First offset is always 1.
|
|
121
|
+
# Each offset points to the start of its item in the data array.
|
|
122
|
+
# Last offset points one byte past the end of data.
|
|
123
|
+
#
|
|
124
|
+
# @param items [Array<String>] Array of data items
|
|
125
|
+
# @param off_size [Integer] Offset size (1-4 bytes)
|
|
126
|
+
# @return [Array<Integer>] Array of offsets (count + 1 elements)
|
|
127
|
+
def self.build_offsets(items, _off_size)
|
|
128
|
+
offsets = []
|
|
129
|
+
current_offset = 1 # 1-based
|
|
130
|
+
|
|
131
|
+
# First offset is always 1
|
|
132
|
+
offsets << current_offset
|
|
133
|
+
|
|
134
|
+
# Calculate offset for each item
|
|
135
|
+
items.each do |item|
|
|
136
|
+
current_offset += item.bytesize
|
|
137
|
+
offsets << current_offset
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
offsets
|
|
141
|
+
end
|
|
142
|
+
private_class_method :build_offsets
|
|
143
|
+
|
|
144
|
+
# Write an offset value of specified size
|
|
145
|
+
#
|
|
146
|
+
# @param io [StringIO] Output stream
|
|
147
|
+
# @param offset [Integer] Offset value to write
|
|
148
|
+
# @param size [Integer] Number of bytes (1-4)
|
|
149
|
+
def self.write_offset(io, offset, size)
|
|
150
|
+
case size
|
|
151
|
+
when 1
|
|
152
|
+
io.putc(offset & 0xFF)
|
|
153
|
+
when 2
|
|
154
|
+
io.write([offset].pack("n")) # Big-endian unsigned 16-bit
|
|
155
|
+
when 3
|
|
156
|
+
# 24-bit big-endian
|
|
157
|
+
io.putc((offset >> 16) & 0xFF)
|
|
158
|
+
io.putc((offset >> 8) & 0xFF)
|
|
159
|
+
io.putc(offset & 0xFF)
|
|
160
|
+
when 4
|
|
161
|
+
io.write([offset].pack("N")) # Big-endian unsigned 32-bit
|
|
162
|
+
else
|
|
163
|
+
raise ArgumentError, "Invalid offset size: #{size}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
private_class_method :write_offset
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dict"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff
|
|
8
|
+
# CFF Private DICT structure
|
|
9
|
+
#
|
|
10
|
+
# The Private DICT contains glyph-specific hinting and width data.
|
|
11
|
+
# Each font has its own Private DICT (or multiple for CIDFonts).
|
|
12
|
+
#
|
|
13
|
+
# Private DICT Operators:
|
|
14
|
+
# - blue_values: Alignment zones for overshoot suppression
|
|
15
|
+
# - other_blues: Additional alignment zones
|
|
16
|
+
# - family_blues: Family-wide alignment zones
|
|
17
|
+
# - family_other_blues: Family-wide additional alignment zones
|
|
18
|
+
# - blue_scale: Point size for overshoot suppression
|
|
19
|
+
# - blue_shift: Pixels to shift alignment zones
|
|
20
|
+
# - blue_fuzz: Tolerance for alignment zones
|
|
21
|
+
# - std_hw: Standard horizontal stem width
|
|
22
|
+
# - std_vw: Standard vertical stem width
|
|
23
|
+
# - stem_snap_h: Horizontal stem snap widths
|
|
24
|
+
# - stem_snap_v: Vertical stem snap widths
|
|
25
|
+
# - force_bold: Force bold flag
|
|
26
|
+
# - language_group: Language group (0=Latin, 1=CJK)
|
|
27
|
+
# - expansion_factor: Expansion factor for counters
|
|
28
|
+
# - initial_random_seed: Random seed for Type 1 hinting
|
|
29
|
+
# - subrs: Offset to Local Subr INDEX (relative to Private DICT)
|
|
30
|
+
# - default_width_x: Default glyph width
|
|
31
|
+
# - nominal_width_x: Nominal glyph width
|
|
32
|
+
#
|
|
33
|
+
# Reference: CFF specification section 10 "Private DICT"
|
|
34
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
|
|
35
|
+
#
|
|
36
|
+
# @example Parsing a Private DICT
|
|
37
|
+
# private_size, private_offset = top_dict.private
|
|
38
|
+
# private_data = cff.raw_data[private_offset, private_size]
|
|
39
|
+
# private_dict = Fontisan::Tables::Cff::PrivateDict.new(private_data)
|
|
40
|
+
# puts private_dict[:blue_values] # => [array of blue values]
|
|
41
|
+
# puts private_dict.default_width_x # => default glyph width
|
|
42
|
+
class PrivateDict < Dict
|
|
43
|
+
# Private DICT specific operators
|
|
44
|
+
#
|
|
45
|
+
# These extend the common operators defined in the base Dict class
|
|
46
|
+
PRIVATE_DICT_OPERATORS = {
|
|
47
|
+
6 => :blue_values,
|
|
48
|
+
7 => :other_blues,
|
|
49
|
+
8 => :family_blues,
|
|
50
|
+
9 => :family_other_blues,
|
|
51
|
+
[12, 9] => :blue_scale,
|
|
52
|
+
[12, 10] => :blue_shift,
|
|
53
|
+
[12, 11] => :blue_fuzz,
|
|
54
|
+
10 => :std_hw,
|
|
55
|
+
11 => :std_vw,
|
|
56
|
+
[12, 12] => :stem_snap_h,
|
|
57
|
+
[12, 13] => :stem_snap_v,
|
|
58
|
+
[12, 14] => :force_bold,
|
|
59
|
+
[12, 17] => :language_group,
|
|
60
|
+
[12, 18] => :expansion_factor,
|
|
61
|
+
[12, 19] => :initial_random_seed,
|
|
62
|
+
19 => :subrs,
|
|
63
|
+
20 => :default_width_x,
|
|
64
|
+
21 => :nominal_width_x,
|
|
65
|
+
}.freeze
|
|
66
|
+
|
|
67
|
+
# Default values for Private DICT operators
|
|
68
|
+
#
|
|
69
|
+
# These are used when an operator is not present in the DICT
|
|
70
|
+
DEFAULTS = {
|
|
71
|
+
blue_scale: 0.039625,
|
|
72
|
+
blue_shift: 7,
|
|
73
|
+
blue_fuzz: 1,
|
|
74
|
+
force_bold: false,
|
|
75
|
+
language_group: 0,
|
|
76
|
+
expansion_factor: 0.06,
|
|
77
|
+
initial_random_seed: 0,
|
|
78
|
+
default_width_x: 0,
|
|
79
|
+
nominal_width_x: 0,
|
|
80
|
+
}.freeze
|
|
81
|
+
|
|
82
|
+
# Get a value with default fallback
|
|
83
|
+
#
|
|
84
|
+
# @param key [Symbol] Operator name
|
|
85
|
+
# @return [Object] Value or default value
|
|
86
|
+
def fetch(key, default = nil)
|
|
87
|
+
@dict.fetch(key, DEFAULTS.fetch(key, default))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get the blue values (alignment zones)
|
|
91
|
+
#
|
|
92
|
+
# Blue values define vertical zones for overshoot suppression
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<Integer>, nil] Array of blue values (pairs of bottom/top)
|
|
95
|
+
def blue_values
|
|
96
|
+
@dict[:blue_values]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get the other blue values
|
|
100
|
+
#
|
|
101
|
+
# Additional alignment zones beyond the baseline and cap height
|
|
102
|
+
#
|
|
103
|
+
# @return [Array<Integer>, nil] Array of other blue values
|
|
104
|
+
def other_blues
|
|
105
|
+
@dict[:other_blues]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Get the family blue values
|
|
109
|
+
#
|
|
110
|
+
# Family-wide alignment zones shared across fonts in a family
|
|
111
|
+
#
|
|
112
|
+
# @return [Array<Integer>, nil] Array of family blue values
|
|
113
|
+
def family_blues
|
|
114
|
+
@dict[:family_blues]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get the family other blue values
|
|
118
|
+
#
|
|
119
|
+
# @return [Array<Integer>, nil] Array of family other blue values
|
|
120
|
+
def family_other_blues
|
|
121
|
+
@dict[:family_other_blues]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Get the blue scale
|
|
125
|
+
#
|
|
126
|
+
# Point size at which overshoot suppression is maximum
|
|
127
|
+
#
|
|
128
|
+
# @return [Float] Blue scale value
|
|
129
|
+
def blue_scale
|
|
130
|
+
fetch(:blue_scale)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Get the blue shift
|
|
134
|
+
#
|
|
135
|
+
# Number of device pixels to shift alignment zones
|
|
136
|
+
#
|
|
137
|
+
# @return [Integer] Blue shift in pixels
|
|
138
|
+
def blue_shift
|
|
139
|
+
fetch(:blue_shift)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get the blue fuzz
|
|
143
|
+
#
|
|
144
|
+
# Tolerance for alignment zone matching
|
|
145
|
+
#
|
|
146
|
+
# @return [Integer] Blue fuzz in font units
|
|
147
|
+
def blue_fuzz
|
|
148
|
+
fetch(:blue_fuzz)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Get the standard horizontal width
|
|
152
|
+
#
|
|
153
|
+
# Dominant horizontal stem width
|
|
154
|
+
#
|
|
155
|
+
# @return [Integer, nil] Standard horizontal width
|
|
156
|
+
def std_hw
|
|
157
|
+
value = @dict[:std_hw]
|
|
158
|
+
# std_hw is stored as an array with one element
|
|
159
|
+
value.is_a?(Array) ? value.first : value
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Get the standard vertical width
|
|
163
|
+
#
|
|
164
|
+
# Dominant vertical stem width
|
|
165
|
+
#
|
|
166
|
+
# @return [Integer, nil] Standard vertical width
|
|
167
|
+
def std_vw
|
|
168
|
+
value = @dict[:std_vw]
|
|
169
|
+
# std_vw is stored as an array with one element
|
|
170
|
+
value.is_a?(Array) ? value.first : value
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Get the horizontal stem snap widths
|
|
174
|
+
#
|
|
175
|
+
# Array of horizontal stem widths for stem snapping
|
|
176
|
+
#
|
|
177
|
+
# @return [Array<Integer>, nil] Horizontal stem snap widths
|
|
178
|
+
def stem_snap_h
|
|
179
|
+
@dict[:stem_snap_h]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Get the vertical stem snap widths
|
|
183
|
+
#
|
|
184
|
+
# Array of vertical stem widths for stem snapping
|
|
185
|
+
#
|
|
186
|
+
# @return [Array<Integer>, nil] Vertical stem snap widths
|
|
187
|
+
def stem_snap_v
|
|
188
|
+
@dict[:stem_snap_v]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Check if force bold is enabled
|
|
192
|
+
#
|
|
193
|
+
# @return [Boolean] True if force bold is enabled
|
|
194
|
+
def force_bold?
|
|
195
|
+
fetch(:force_bold)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Get the language group
|
|
199
|
+
#
|
|
200
|
+
# 0 = Latin/Greek/Cyrillic, 1 = CJK
|
|
201
|
+
#
|
|
202
|
+
# @return [Integer] Language group (0 or 1)
|
|
203
|
+
def language_group
|
|
204
|
+
fetch(:language_group)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Get the expansion factor
|
|
208
|
+
#
|
|
209
|
+
# Controls horizontal counter expansion
|
|
210
|
+
#
|
|
211
|
+
# @return [Float] Expansion factor
|
|
212
|
+
def expansion_factor
|
|
213
|
+
fetch(:expansion_factor)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Get the initial random seed
|
|
217
|
+
#
|
|
218
|
+
# Seed for pseudo-random number generation in Type 1 hinting
|
|
219
|
+
#
|
|
220
|
+
# @return [Integer] Initial random seed
|
|
221
|
+
def initial_random_seed
|
|
222
|
+
fetch(:initial_random_seed)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Get the Local Subr INDEX offset
|
|
226
|
+
#
|
|
227
|
+
# Offset is relative to the beginning of the Private DICT
|
|
228
|
+
#
|
|
229
|
+
# @return [Integer, nil] Offset to Local Subr INDEX
|
|
230
|
+
def subrs
|
|
231
|
+
@dict[:subrs]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Get the default glyph width
|
|
235
|
+
#
|
|
236
|
+
# Used when width is not explicitly specified in CharString
|
|
237
|
+
#
|
|
238
|
+
# @return [Integer] Default width in font units
|
|
239
|
+
def default_width_x
|
|
240
|
+
fetch(:default_width_x)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Get the nominal glyph width
|
|
244
|
+
#
|
|
245
|
+
# Base value for width calculations in CharStrings
|
|
246
|
+
#
|
|
247
|
+
# @return [Integer] Nominal width in font units
|
|
248
|
+
def nominal_width_x
|
|
249
|
+
fetch(:nominal_width_x)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Check if this Private DICT has local subroutines
|
|
253
|
+
#
|
|
254
|
+
# @return [Boolean] True if subrs offset is present
|
|
255
|
+
def has_local_subrs?
|
|
256
|
+
!subrs.nil?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Check if this Private DICT has blue values defined
|
|
260
|
+
#
|
|
261
|
+
# @return [Boolean] True if blue values are present
|
|
262
|
+
def has_blue_values?
|
|
263
|
+
!blue_values.nil? && !blue_values.empty?
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Check if this is for CJK language group
|
|
267
|
+
#
|
|
268
|
+
# @return [Boolean] True if language group is 1 (CJK)
|
|
269
|
+
def cjk?
|
|
270
|
+
language_group == 1
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
private
|
|
274
|
+
|
|
275
|
+
# Get Private DICT specific operators
|
|
276
|
+
#
|
|
277
|
+
# @return [Hash] Private DICT operators merged with base operators
|
|
278
|
+
def derived_operators
|
|
279
|
+
PRIVATE_DICT_OPERATORS
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dict"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff
|
|
8
|
+
# CFF Top DICT structure
|
|
9
|
+
#
|
|
10
|
+
# The Top DICT contains font-level metadata and pointers to other CFF
|
|
11
|
+
# structures like CharStrings, Charset, Encoding, and Private DICT.
|
|
12
|
+
#
|
|
13
|
+
# Top DICT Operators (in addition to common DICT operators):
|
|
14
|
+
# - charset: Offset to Charset data
|
|
15
|
+
# - encoding: Offset to Encoding data
|
|
16
|
+
# - charstrings: Offset to CharStrings INDEX
|
|
17
|
+
# - private: Size and offset to Private DICT
|
|
18
|
+
# - font_bbox: Font bounding box [xMin, yMin, xMax, yMax]
|
|
19
|
+
# - unique_id: Unique ID for this font
|
|
20
|
+
# - xuid: Extended unique ID array
|
|
21
|
+
# - ros: CIDFont registry, ordering, supplement
|
|
22
|
+
# - cidcount: Number of CIDs in CIDFont
|
|
23
|
+
# - fdarray: Offset to Font DICT INDEX (CIDFont)
|
|
24
|
+
# - fdselect: Offset to FDSelect data (CIDFont)
|
|
25
|
+
#
|
|
26
|
+
# Reference: CFF specification section 9 "Top DICT"
|
|
27
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
|
|
28
|
+
#
|
|
29
|
+
# @example Parsing a Top DICT
|
|
30
|
+
# top_dict_data = cff.top_dict_index[0]
|
|
31
|
+
# top_dict = Fontisan::Tables::Cff::TopDict.new(top_dict_data)
|
|
32
|
+
# puts top_dict[:charstrings] # => offset to CharStrings
|
|
33
|
+
# puts top_dict[:charset] # => offset to Charset
|
|
34
|
+
class TopDict < Dict
|
|
35
|
+
# Top DICT specific operators
|
|
36
|
+
#
|
|
37
|
+
# These extend the common operators defined in the base Dict class
|
|
38
|
+
TOP_DICT_OPERATORS = {
|
|
39
|
+
5 => :font_bbox,
|
|
40
|
+
13 => :unique_id,
|
|
41
|
+
14 => :xuid,
|
|
42
|
+
15 => :charset,
|
|
43
|
+
16 => :encoding,
|
|
44
|
+
17 => :charstrings,
|
|
45
|
+
18 => :private,
|
|
46
|
+
[12, 30] => :ros,
|
|
47
|
+
[12, 31] => :cid_font_version,
|
|
48
|
+
[12, 32] => :cid_font_revision,
|
|
49
|
+
[12, 33] => :cid_font_type,
|
|
50
|
+
[12, 34] => :cid_count,
|
|
51
|
+
[12, 35] => :uid_base,
|
|
52
|
+
[12, 36] => :fd_array,
|
|
53
|
+
[12, 37] => :fd_select,
|
|
54
|
+
[12, 38] => :font_name,
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
# Default values for Top DICT operators
|
|
58
|
+
#
|
|
59
|
+
# These are used when an operator is not present in the DICT
|
|
60
|
+
DEFAULTS = {
|
|
61
|
+
is_fixed_pitch: false,
|
|
62
|
+
italic_angle: 0,
|
|
63
|
+
underline_position: -100,
|
|
64
|
+
underline_thickness: 50,
|
|
65
|
+
paint_type: 0,
|
|
66
|
+
charstring_type: 2,
|
|
67
|
+
font_matrix: [0.001, 0, 0, 0.001, 0, 0],
|
|
68
|
+
unique_id: nil,
|
|
69
|
+
font_bbox: [0, 0, 0, 0],
|
|
70
|
+
stroke_width: 0,
|
|
71
|
+
charset: 0, # Offset 0 = ISOAdobe charset
|
|
72
|
+
encoding: 0, # Offset 0 = Standard encoding
|
|
73
|
+
cid_count: 8720,
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
# Get a value with default fallback
|
|
77
|
+
#
|
|
78
|
+
# @param key [Symbol] Operator name
|
|
79
|
+
# @return [Object] Value or default value
|
|
80
|
+
def fetch(key, default = nil)
|
|
81
|
+
@dict.fetch(key, DEFAULTS.fetch(key, default))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get the charset offset
|
|
85
|
+
#
|
|
86
|
+
# Charset determines which glyphs are present and their SIDs
|
|
87
|
+
#
|
|
88
|
+
# Special values:
|
|
89
|
+
# - 0: ISOAdobe charset
|
|
90
|
+
# - 1: Expert charset
|
|
91
|
+
# - 2: Expert Subset charset
|
|
92
|
+
# - Otherwise: Offset to custom charset
|
|
93
|
+
#
|
|
94
|
+
# @return [Integer] Charset offset or predefined charset ID
|
|
95
|
+
def charset
|
|
96
|
+
fetch(:charset)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get the encoding offset
|
|
100
|
+
#
|
|
101
|
+
# Encoding maps character codes to glyph indices
|
|
102
|
+
#
|
|
103
|
+
# Special values:
|
|
104
|
+
# - 0: Standard encoding
|
|
105
|
+
# - 1: Expert encoding
|
|
106
|
+
# - Otherwise: Offset to custom encoding
|
|
107
|
+
#
|
|
108
|
+
# @return [Integer] Encoding offset or predefined encoding ID
|
|
109
|
+
def encoding
|
|
110
|
+
fetch(:encoding)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get the CharStrings offset
|
|
114
|
+
#
|
|
115
|
+
# CharStrings INDEX contains the glyph programs (outline data)
|
|
116
|
+
#
|
|
117
|
+
# @return [Integer, nil] Offset to CharStrings INDEX
|
|
118
|
+
def charstrings
|
|
119
|
+
@dict[:charstrings]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Get the Private DICT size and offset
|
|
123
|
+
#
|
|
124
|
+
# The private operator stores [size, offset] as a two-element array
|
|
125
|
+
#
|
|
126
|
+
# @return [Array<Integer>, nil] [size, offset] or nil if not present
|
|
127
|
+
def private
|
|
128
|
+
@dict[:private]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Get the Private DICT size
|
|
132
|
+
#
|
|
133
|
+
# @return [Integer, nil] Size in bytes, or nil if no Private DICT
|
|
134
|
+
def private_size
|
|
135
|
+
private&.first
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Get the Private DICT offset
|
|
139
|
+
#
|
|
140
|
+
# @return [Integer, nil] Offset in bytes, or nil if no Private DICT
|
|
141
|
+
def private_offset
|
|
142
|
+
private&.last
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Get the font bounding box
|
|
146
|
+
#
|
|
147
|
+
# @return [Array<Integer>] [xMin, yMin, xMax, yMax]
|
|
148
|
+
def font_bbox
|
|
149
|
+
fetch(:font_bbox)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Get the font matrix
|
|
153
|
+
#
|
|
154
|
+
# Transform from glyph space to user space
|
|
155
|
+
#
|
|
156
|
+
# @return [Array<Float>] 6-element affine transformation matrix
|
|
157
|
+
def font_matrix
|
|
158
|
+
fetch(:font_matrix)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Check if this is a CIDFont
|
|
162
|
+
#
|
|
163
|
+
# CIDFonts have the ROS (Registry-Ordering-Supplement) operator
|
|
164
|
+
#
|
|
165
|
+
# @return [Boolean] True if CIDFont
|
|
166
|
+
def cid_font?
|
|
167
|
+
has_key?(:ros)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Get the ROS (Registry, Ordering, Supplement) for CIDFonts
|
|
171
|
+
#
|
|
172
|
+
# @return [Array<Integer>, nil] [registry_sid, ordering_sid, supplement]
|
|
173
|
+
def ros
|
|
174
|
+
@dict[:ros]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Get the CID count for CIDFonts
|
|
178
|
+
#
|
|
179
|
+
# @return [Integer] Number of CIDs
|
|
180
|
+
def cid_count
|
|
181
|
+
fetch(:cid_count)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Get the FDArray offset for CIDFonts
|
|
185
|
+
#
|
|
186
|
+
# FDArray is a Font DICT INDEX for CIDFonts
|
|
187
|
+
#
|
|
188
|
+
# @return [Integer, nil] Offset to FDArray
|
|
189
|
+
def fd_array
|
|
190
|
+
@dict[:fd_array]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Get the FDSelect offset for CIDFonts
|
|
194
|
+
#
|
|
195
|
+
# FDSelect maps CIDs to Font DICTs in FDArray
|
|
196
|
+
#
|
|
197
|
+
# @return [Integer, nil] Offset to FDSelect
|
|
198
|
+
def fd_select
|
|
199
|
+
@dict[:fd_select]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Get the CharString type
|
|
203
|
+
#
|
|
204
|
+
# @return [Integer] CharString type (typically 2 for Type 2 CharStrings)
|
|
205
|
+
def charstring_type
|
|
206
|
+
fetch(:charstring_type)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Check if the font has a custom charset
|
|
210
|
+
#
|
|
211
|
+
# @return [Boolean] True if charset is custom (not 0, 1, or 2)
|
|
212
|
+
def custom_charset?
|
|
213
|
+
charset_val = charset
|
|
214
|
+
charset_val && charset_val > 2
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Check if the font has a custom encoding
|
|
218
|
+
#
|
|
219
|
+
# @return [Boolean] True if encoding is custom (not 0 or 1)
|
|
220
|
+
def custom_encoding?
|
|
221
|
+
encoding_val = encoding
|
|
222
|
+
encoding_val && encoding_val > 1
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
# Get Top DICT specific operators
|
|
228
|
+
#
|
|
229
|
+
# @return [Hash] Top DICT operators merged with base operators
|
|
230
|
+
def derived_operators
|
|
231
|
+
TOP_DICT_OPERATORS
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|