fontisan 0.2.13 → 0.2.14
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 +8 -223
- data/Gemfile +1 -1
- data/lib/fontisan/commands/info_command.rb +5 -5
- data/lib/fontisan/converters/format_converter.rb +2 -1
- data/lib/fontisan/converters/type1_converter.rb +65 -60
- data/lib/fontisan/hints/hint_converter.rb +2 -1
- data/lib/fontisan/open_type_font.rb +0 -40
- data/lib/fontisan/sfnt_font.rb +37 -19
- data/lib/fontisan/true_type_collection.rb +8 -8
- data/lib/fontisan/true_type_font.rb +1 -59
- data/lib/fontisan/type1/afm_parser.rb +2 -1
- data/lib/fontisan/type1/cff_to_type1_converter.rb +24 -19
- data/lib/fontisan/type1/private_dict.rb +28 -7
- data/lib/fontisan/type1/seac_expander.rb +22 -17
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2_font.rb +2 -2
- data/lib/fontisan/woff_font.rb +3 -3
- metadata +2 -2
|
@@ -327,7 +327,10 @@ module Fontisan
|
|
|
327
327
|
|
|
328
328
|
# Get CharStrings INDEX from CFF
|
|
329
329
|
charstrings_index = cff_table.charstrings_index(0)
|
|
330
|
-
|
|
330
|
+
unless charstrings_index
|
|
331
|
+
raise Fontisan::Error,
|
|
332
|
+
"CharStrings INDEX not found"
|
|
333
|
+
end
|
|
331
334
|
|
|
332
335
|
# Get Private DICT for context
|
|
333
336
|
private_dict = cff_table.private_dict(0)
|
|
@@ -335,7 +338,7 @@ module Fontisan
|
|
|
335
338
|
# Create CFF to Type 1 converter
|
|
336
339
|
converter = Type1::CffToType1Converter.new(
|
|
337
340
|
nominal_width: private_dict&.nominal_width || 0,
|
|
338
|
-
default_width: private_dict&.default_width || 0
|
|
341
|
+
default_width: private_dict&.default_width || 0,
|
|
339
342
|
)
|
|
340
343
|
|
|
341
344
|
# Convert each CFF CharString to Type 1 format
|
|
@@ -354,7 +357,7 @@ module Fontisan
|
|
|
354
357
|
private_dict_hash = build_private_dict_hash(private_dict)
|
|
355
358
|
type1_charstrings[glyph_name] = converter.convert(
|
|
356
359
|
cff_charstring,
|
|
357
|
-
private_dict: private_dict_hash
|
|
360
|
+
private_dict: private_dict_hash,
|
|
358
361
|
)
|
|
359
362
|
end
|
|
360
363
|
|
|
@@ -551,7 +554,7 @@ module Fontisan
|
|
|
551
554
|
# Parse version (e.g., "001.000" => 1.0)
|
|
552
555
|
version_parts = version_str.split(".")
|
|
553
556
|
major = version_parts[0].to_i
|
|
554
|
-
minor = version_parts[1]
|
|
557
|
+
minor = version_parts[1].to_i
|
|
555
558
|
version = major + (minor / 1000.0)
|
|
556
559
|
|
|
557
560
|
# Version (Fixed 16.16) - stored as int32
|
|
@@ -626,24 +629,24 @@ module Fontisan
|
|
|
626
629
|
|
|
627
630
|
# Ascent (int16) - Distance from baseline to highest ascender
|
|
628
631
|
# Use BlueValues[2] or [3] if available, otherwise font_bbox[3]
|
|
629
|
-
if blue_values.length >= 4
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
632
|
+
ascent = if blue_values.length >= 4
|
|
633
|
+
blue_values[3] # Top zone top
|
|
634
|
+
elsif blue_values.length >= 3
|
|
635
|
+
blue_values[2] # Top zone bottom
|
|
636
|
+
else
|
|
637
|
+
font_bbox[3] # y_max
|
|
638
|
+
end
|
|
636
639
|
data << [ascent].pack("s>")
|
|
637
640
|
|
|
638
641
|
# Descent (int16) - Distance from baseline to lowest descender (negative)
|
|
639
642
|
# Use BlueValues[0] or [1] if available, otherwise font_bbox[1]
|
|
640
|
-
if blue_values.length >= 2
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
643
|
+
descent = if blue_values.length >= 2
|
|
644
|
+
blue_values[0] # Bottom zone bottom (negative)
|
|
645
|
+
elsif blue_values.length >= 1
|
|
646
|
+
blue_values[0]
|
|
647
|
+
else
|
|
648
|
+
font_bbox[1] # y_min (should be negative)
|
|
649
|
+
end
|
|
647
650
|
data << [descent].pack("s>")
|
|
648
651
|
|
|
649
652
|
# Line Gap (int16) - Additional space between lines
|
|
@@ -728,33 +731,33 @@ module Fontisan
|
|
|
728
731
|
|
|
729
732
|
# Extract font names with fallbacks
|
|
730
733
|
font_name = font.font_name || font_dict&.font_name || "Unnamed"
|
|
731
|
-
family_name = if font_info
|
|
734
|
+
family_name = if font_info.respond_to?(:family_name)
|
|
732
735
|
font_info.family_name || font_dict&.family_name || font_name
|
|
733
736
|
else
|
|
734
737
|
font_dict&.family_name || font_name
|
|
735
738
|
end
|
|
736
|
-
full_name = if font_info
|
|
739
|
+
full_name = if font_info.respond_to?(:full_name)
|
|
737
740
|
font_info.full_name || font_dict&.full_name || family_name
|
|
738
741
|
else
|
|
739
742
|
font_dict&.full_name || family_name
|
|
740
743
|
end
|
|
741
|
-
version = if font_info
|
|
744
|
+
version = if font_info.respond_to?(:version)
|
|
742
745
|
font_info.version || font.version || "001.000"
|
|
743
746
|
else
|
|
744
747
|
font.version || "001.000"
|
|
745
748
|
end
|
|
746
|
-
copyright = if font_info
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
749
|
+
copyright = if font_info.respond_to?(:copyright)
|
|
750
|
+
font_info.copyright || font_dict&.raw_data&.dig(:copyright) || ""
|
|
751
|
+
else
|
|
752
|
+
font_dict&.raw_data&.dig(:copyright) || ""
|
|
753
|
+
end
|
|
751
754
|
postscript_name = font_name
|
|
752
|
-
weight = if font_info
|
|
755
|
+
weight = if font_info.respond_to?(:weight)
|
|
753
756
|
font_info.weight
|
|
754
757
|
else
|
|
755
758
|
"Regular"
|
|
756
759
|
end
|
|
757
|
-
notice = if font_info
|
|
760
|
+
notice = if font_info.respond_to?(:notice)
|
|
758
761
|
font_info.notice
|
|
759
762
|
else
|
|
760
763
|
""
|
|
@@ -782,7 +785,9 @@ module Fontisan
|
|
|
782
785
|
]
|
|
783
786
|
|
|
784
787
|
# Filter out empty strings and build string storage
|
|
785
|
-
name_records = name_records.select
|
|
788
|
+
name_records = name_records.select do |r|
|
|
789
|
+
!r[:string].nil? && !r[:string].empty?
|
|
790
|
+
end
|
|
786
791
|
|
|
787
792
|
# Build string storage (UTF-16BE encoded for Windows platform)
|
|
788
793
|
string_storage = (+"").b
|
|
@@ -810,15 +815,15 @@ module Fontisan
|
|
|
810
815
|
# Write name records
|
|
811
816
|
platform_id = 3 # Windows
|
|
812
817
|
encoding_id = 1 # Unicode BMP
|
|
813
|
-
language_id = 0x0409
|
|
818
|
+
language_id = 0x0409 # US English
|
|
814
819
|
|
|
815
820
|
name_records.each do |record|
|
|
816
821
|
data << [platform_id].pack("n") # platform ID
|
|
817
822
|
data << [encoding_id].pack("n") # encoding ID
|
|
818
823
|
data << [language_id].pack("n") # language ID
|
|
819
824
|
data << [record[:name_id]].pack("n") # name ID
|
|
820
|
-
data << [record[:encoded].bytesize].pack("n")
|
|
821
|
-
data << [record[:offset]].pack("n")
|
|
825
|
+
data << [record[:encoded].bytesize].pack("n") # string length
|
|
826
|
+
data << [record[:offset]].pack("n") # string offset
|
|
822
827
|
end
|
|
823
828
|
|
|
824
829
|
# Write string storage
|
|
@@ -926,13 +931,13 @@ module Fontisan
|
|
|
926
931
|
|
|
927
932
|
# Unicode ranges (4 x uint32) - Basic Latin + Latin-1
|
|
928
933
|
# Bits 0-31: Basic Latin, Latin-1, Latin Extended-A/B, etc.
|
|
929
|
-
data << [0x00000001].pack("N")
|
|
934
|
+
data << [0x00000001].pack("N") # Basic Latin (0-7F)
|
|
930
935
|
data << [0x00000000].pack("N")
|
|
931
936
|
data << [0x00000000].pack("N")
|
|
932
937
|
data << [0x00000000].pack("N")
|
|
933
938
|
|
|
934
939
|
# achVendID (4 bytes) - Vendor ID
|
|
935
|
-
data << "UKWN"
|
|
940
|
+
data << "UKWN" # Unknown
|
|
936
941
|
|
|
937
942
|
# fsSelection (uint16) - Font selection flags
|
|
938
943
|
# Bit 6 (0x40) = Regular weight if 400-500
|
|
@@ -946,25 +951,25 @@ module Fontisan
|
|
|
946
951
|
data << [fs_selection].pack("n")
|
|
947
952
|
|
|
948
953
|
# usFirstCharIndex (uint16) - First Unicode character
|
|
949
|
-
data << [32].pack("n")
|
|
954
|
+
data << [32].pack("n") # Space
|
|
950
955
|
|
|
951
956
|
# usLastCharIndex (uint16) - Last Unicode character
|
|
952
|
-
data << [0xFFFD].pack("n")
|
|
957
|
+
data << [0xFFFD].pack("n") # Replacement character
|
|
953
958
|
|
|
954
959
|
# sTypoAscender (int16) - Use BlueValues or font bbox
|
|
955
|
-
if blue_values.length >= 4
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
+
typo_ascender = if blue_values.length >= 4
|
|
961
|
+
blue_values[3]
|
|
962
|
+
else
|
|
963
|
+
font_bbox[3]
|
|
964
|
+
end
|
|
960
965
|
data << [typo_ascender].pack("s>")
|
|
961
966
|
|
|
962
967
|
# sTypoDescender (int16) - Use BlueValues or font bbox (negative)
|
|
963
|
-
if blue_values.length >= 2
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
+
typo_descender = if blue_values.length >= 2
|
|
969
|
+
blue_values[0]
|
|
970
|
+
else
|
|
971
|
+
font_bbox[1]
|
|
972
|
+
end
|
|
968
973
|
data << [typo_descender].pack("s>")
|
|
969
974
|
|
|
970
975
|
# sTypoLineGap (int16)
|
|
@@ -1013,7 +1018,7 @@ module Fontisan
|
|
|
1013
1018
|
# Version (Fixed 16.16) - Use version 3.0 for CFF fonts (no glyph names)
|
|
1014
1019
|
# Version 2.0 would include glyph names, but for OTF output version 3.0 is fine
|
|
1015
1020
|
# since CFF table contains the glyph names
|
|
1016
|
-
data << [0x00030000].pack("N")
|
|
1021
|
+
data << [0x00030000].pack("N") # Version 3.0
|
|
1017
1022
|
|
|
1018
1023
|
# Italic Angle (Fixed 16.16)
|
|
1019
1024
|
# Get from FontInfo if available, otherwise default to 0
|
|
@@ -1030,7 +1035,7 @@ module Fontisan
|
|
|
1030
1035
|
data << [underline_thickness].pack("s>")
|
|
1031
1036
|
|
|
1032
1037
|
# Fixed Pitch (uint32) - Boolean for monospace
|
|
1033
|
-
is_fixed_pitch =
|
|
1038
|
+
is_fixed_pitch = font_info.is_fixed_pitch || false ? 1 : 0
|
|
1034
1039
|
data << [is_fixed_pitch].pack("N")
|
|
1035
1040
|
|
|
1036
1041
|
# Min/Max Memory for Type 42 (uint32 each) - Not used for CFF, set to 0
|
|
@@ -1066,10 +1071,10 @@ module Fontisan
|
|
|
1066
1071
|
unicode = Type1::AGL.unicode_for_glyph_name(glyph_name)
|
|
1067
1072
|
|
|
1068
1073
|
# If no Unicode mapping, try to derive from encoding position
|
|
1069
|
-
if unicode.nil?
|
|
1074
|
+
if unicode.nil? && (glyph_index < 128)
|
|
1070
1075
|
# For standard encoding, try to map from position
|
|
1071
1076
|
# This is a simplified approach - real implementation would be more robust
|
|
1072
|
-
unicode = glyph_index
|
|
1077
|
+
unicode = glyph_index
|
|
1073
1078
|
end
|
|
1074
1079
|
|
|
1075
1080
|
# Map Unicode to glyph index
|
|
@@ -1088,21 +1093,21 @@ module Fontisan
|
|
|
1088
1093
|
subtable_data = build_cmap_format_4(unicode_to_glyph)
|
|
1089
1094
|
|
|
1090
1095
|
# Calculate offsets
|
|
1091
|
-
encoding_records_offset = 4
|
|
1092
|
-
subtable_offset = encoding_records_offset + 8
|
|
1096
|
+
encoding_records_offset = 4 # After version (2) + num_tables (2)
|
|
1097
|
+
subtable_offset = encoding_records_offset + 8 # After one encoding record (8 bytes)
|
|
1093
1098
|
|
|
1094
1099
|
# Build cmap table header
|
|
1095
1100
|
# Version (uint16)
|
|
1096
1101
|
data << [0].pack("n")
|
|
1097
1102
|
|
|
1098
1103
|
# Number of encoding records (uint16)
|
|
1099
|
-
data << [1].pack("n")
|
|
1104
|
+
data << [1].pack("n") # One encoding record
|
|
1100
1105
|
|
|
1101
1106
|
# Encoding record: Platform ID (uint16), Encoding ID (uint16), Subtable offset (uint32)
|
|
1102
1107
|
# Platform 3 (Windows), Encoding 1 (Unicode BMP)
|
|
1103
1108
|
data << [3].pack("n") # Platform ID: Windows
|
|
1104
1109
|
data << [1].pack("n") # Encoding ID: Unicode BMP
|
|
1105
|
-
data << [subtable_offset].pack("N")
|
|
1110
|
+
data << [subtable_offset].pack("N") # Subtable offset
|
|
1106
1111
|
|
|
1107
1112
|
# Append subtable data
|
|
1108
1113
|
data << subtable_data
|
|
@@ -1160,13 +1165,13 @@ module Fontisan
|
|
|
1160
1165
|
# Calculate segment count and related values
|
|
1161
1166
|
seg_count = segments.length
|
|
1162
1167
|
seg_count_x2 = seg_count * 2
|
|
1163
|
-
search_range = 2
|
|
1168
|
+
search_range = 2**Math.log2(seg_count).to_i * 2
|
|
1164
1169
|
entry_selector = Math.log2(search_range / 2).to_i
|
|
1165
1170
|
range_shift = (seg_count - search_range / 2) * 2
|
|
1166
1171
|
|
|
1167
1172
|
# Build format 4 subtable header (14 bytes)
|
|
1168
|
-
data << [4].pack("n")
|
|
1169
|
-
data << [calculate_cmap4_length(segments)].pack("n")
|
|
1173
|
+
data << [4].pack("n") # Format
|
|
1174
|
+
data << [calculate_cmap4_length(segments)].pack("n") # Length (placeholder)
|
|
1170
1175
|
data << [0].pack("n") # Language (0 = independent)
|
|
1171
1176
|
data << [seg_count_x2].pack("n") # segCountX2
|
|
1172
1177
|
data << [search_range].pack("n") # searchRange
|
|
@@ -1203,9 +1208,9 @@ module Fontisan
|
|
|
1203
1208
|
|
|
1204
1209
|
# Write arrays (padded to even length)
|
|
1205
1210
|
end_codes.each { |code| data << [code].pack("n") }
|
|
1206
|
-
data << [0].pack("n")
|
|
1211
|
+
data << [0].pack("n") # Reserved padding
|
|
1207
1212
|
start_codes.each { |code| data << [code].pack("n") }
|
|
1208
|
-
id_deltas.each { |delta| data << [delta].pack("s>") }
|
|
1213
|
+
id_deltas.each { |delta| data << [delta].pack("s>") } # Signed
|
|
1209
1214
|
id_range_offsets.each { |offset| data << [offset].pack("n") }
|
|
1210
1215
|
glyph_id_array.each { |gid| data << [gid].pack("n") }
|
|
1211
1216
|
|
|
@@ -1227,7 +1232,7 @@ module Fontisan
|
|
|
1227
1232
|
seg_count = segments.length
|
|
1228
1233
|
|
|
1229
1234
|
# Rough estimate (actual calculation done during construction)
|
|
1230
|
-
14 + (seg_count * 8) + (seg_count * 2) + 100
|
|
1235
|
+
14 + (seg_count * 8) + (seg_count * 2) + 100 # 100 for glyph ID array estimate
|
|
1231
1236
|
end
|
|
1232
1237
|
end
|
|
1233
1238
|
end
|
|
@@ -276,7 +276,8 @@ module Fontisan
|
|
|
276
276
|
# Merge all extracted hints (prep_hints and fpgm_hints override stem widths if present)
|
|
277
277
|
# Note: fpgm_hints contains metadata (fpgm_size, has_functions, complexity)
|
|
278
278
|
# which we must filter out before merging into PostScript dict hints
|
|
279
|
-
fpgm_dict_hints = fpgm_hints.
|
|
279
|
+
fpgm_dict_hints = fpgm_hints.except(:fpgm_size, :has_functions,
|
|
280
|
+
:complexity)
|
|
280
281
|
hints.merge!(prep_hints).merge!(fpgm_dict_hints).merge!(blue_zones)
|
|
281
282
|
|
|
282
283
|
# Provide default blue_values if none were detected
|
|
@@ -32,46 +32,6 @@ module Fontisan
|
|
|
32
32
|
# Page size for lazy loading alignment (typical filesystem page size)
|
|
33
33
|
PAGE_SIZE = 4096
|
|
34
34
|
|
|
35
|
-
# Read OpenType Font from a file
|
|
36
|
-
#
|
|
37
|
-
# @param path [String] Path to the OTF file
|
|
38
|
-
# @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
|
|
39
|
-
# @param lazy [Boolean] If true, load tables on demand (default: false)
|
|
40
|
-
# @return [OpenTypeFont] A new instance
|
|
41
|
-
# @raise [ArgumentError] if path is nil or empty, or if mode is invalid
|
|
42
|
-
# @raise [Errno::ENOENT] if file does not exist
|
|
43
|
-
# @raise [RuntimeError] if file format is invalid
|
|
44
|
-
def self.from_file(path, mode: LoadingModes::FULL, lazy: false)
|
|
45
|
-
if path.nil? || path.to_s.empty?
|
|
46
|
-
raise ArgumentError,
|
|
47
|
-
"path cannot be nil or empty"
|
|
48
|
-
end
|
|
49
|
-
raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
|
|
50
|
-
|
|
51
|
-
# Validate mode
|
|
52
|
-
LoadingModes.validate_mode!(mode)
|
|
53
|
-
|
|
54
|
-
File.open(path, "rb") do |io|
|
|
55
|
-
font = read(io)
|
|
56
|
-
font.initialize_storage
|
|
57
|
-
font.loading_mode = mode
|
|
58
|
-
font.lazy_load_enabled = lazy
|
|
59
|
-
|
|
60
|
-
if lazy
|
|
61
|
-
# Keep file handle open for lazy loading
|
|
62
|
-
font.io_source = File.open(path, "rb")
|
|
63
|
-
font.setup_finalizer
|
|
64
|
-
else
|
|
65
|
-
# Read tables upfront
|
|
66
|
-
font.read_table_data(io)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
font
|
|
70
|
-
end
|
|
71
|
-
rescue BinData::ValidityError, EOFError => e
|
|
72
|
-
raise "Invalid OTF file: #{e.message}"
|
|
73
|
-
end
|
|
74
|
-
|
|
75
35
|
# Initialize storage hashes
|
|
76
36
|
#
|
|
77
37
|
# Extends base class to add page_cache for lazy loading.
|
data/lib/fontisan/sfnt_font.rb
CHANGED
|
@@ -149,34 +149,52 @@ module Fontisan
|
|
|
149
149
|
# @raise [RuntimeError] if file format is invalid
|
|
150
150
|
def self.from_file(path, mode: LoadingModes::FULL, lazy: false)
|
|
151
151
|
if path.nil? || path.to_s.empty?
|
|
152
|
-
raise ArgumentError,
|
|
153
|
-
"path cannot be nil or empty"
|
|
152
|
+
raise ArgumentError, "path cannot be nil or empty"
|
|
154
153
|
end
|
|
155
154
|
raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
|
|
156
155
|
|
|
157
|
-
# Validate mode
|
|
158
156
|
LoadingModes.validate_mode!(mode)
|
|
159
157
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
font.lazy_load_enabled = lazy
|
|
165
|
-
|
|
166
|
-
if lazy
|
|
167
|
-
# Keep file handle open for lazy loading
|
|
168
|
-
font.io_source = File.open(path, "rb")
|
|
169
|
-
font.setup_finalizer
|
|
170
|
-
else
|
|
171
|
-
# Read tables upfront
|
|
172
|
-
font.read_table_data(io)
|
|
173
|
-
end
|
|
158
|
+
font = new
|
|
159
|
+
font.initialize_storage
|
|
160
|
+
font.loading_mode = mode
|
|
161
|
+
font.lazy_load_enabled = lazy
|
|
174
162
|
|
|
175
|
-
|
|
163
|
+
lazy ? load_lazy(path, font) : load_eager(path, font)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Load font with lazy loading (keeps file handle open)
|
|
167
|
+
#
|
|
168
|
+
# @param path [String] Path to the font file
|
|
169
|
+
# @param font [SfntFont] Font instance to populate
|
|
170
|
+
# @return [SfntFont] The populated font instance
|
|
171
|
+
def self.load_lazy(path, font)
|
|
172
|
+
font.io_source = File.open(path, "rb")
|
|
173
|
+
font.setup_finalizer
|
|
174
|
+
font.io_source.rewind
|
|
175
|
+
font.read(font.io_source)
|
|
176
|
+
font
|
|
177
|
+
rescue BinData::ValidityError, EOFError => e
|
|
178
|
+
font_type = name.split("::").last
|
|
179
|
+
raise "Invalid #{font_type} file: #{e.message}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Load font with eager loading (reads all data, closes file)
|
|
183
|
+
#
|
|
184
|
+
# @param path [String] Path to the font file
|
|
185
|
+
# @param font [SfntFont] Font instance to populate
|
|
186
|
+
# @return [SfntFont] The populated font instance
|
|
187
|
+
def self.load_eager(path, font)
|
|
188
|
+
File.open(path, "rb") do |io|
|
|
189
|
+
font.read(io)
|
|
190
|
+
font.read_table_data(io)
|
|
176
191
|
end
|
|
192
|
+
font
|
|
177
193
|
rescue BinData::ValidityError, EOFError => e
|
|
178
|
-
|
|
194
|
+
font_type = name.split("::").last
|
|
195
|
+
raise "Invalid #{font_type} file: #{e.message}"
|
|
179
196
|
end
|
|
197
|
+
private_class_method :load_lazy, :load_eager
|
|
180
198
|
|
|
181
199
|
# Read SFNT Font from collection at specific offset
|
|
182
200
|
#
|
|
@@ -32,7 +32,7 @@ module Fontisan
|
|
|
32
32
|
|
|
33
33
|
# Get a single font from the collection
|
|
34
34
|
#
|
|
35
|
-
#
|
|
35
|
+
# Uses the base class from_collection method.
|
|
36
36
|
#
|
|
37
37
|
# @param index [Integer] Index of the font (0-based)
|
|
38
38
|
# @param io [IO] Open file handle
|
|
@@ -42,13 +42,13 @@ module Fontisan
|
|
|
42
42
|
return nil if index >= num_fonts
|
|
43
43
|
|
|
44
44
|
require_relative "true_type_font"
|
|
45
|
-
TrueTypeFont.
|
|
45
|
+
TrueTypeFont.from_collection(io, font_offsets[index], mode: mode)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# Extract fonts as TrueTypeFont objects
|
|
49
49
|
#
|
|
50
50
|
# Reads each font from the TTC file and returns them as TrueTypeFont objects.
|
|
51
|
-
#
|
|
51
|
+
# Uses the base class from_collection method.
|
|
52
52
|
#
|
|
53
53
|
# @param io [IO] Open file handle to read fonts from
|
|
54
54
|
# @return [Array<TrueTypeFont>] Array of font objects
|
|
@@ -56,13 +56,13 @@ module Fontisan
|
|
|
56
56
|
require_relative "true_type_font"
|
|
57
57
|
|
|
58
58
|
font_offsets.map do |offset|
|
|
59
|
-
TrueTypeFont.
|
|
59
|
+
TrueTypeFont.from_collection(io, offset)
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# List all fonts in the collection with basic metadata
|
|
64
64
|
#
|
|
65
|
-
#
|
|
65
|
+
# Uses the base class from_collection method.
|
|
66
66
|
#
|
|
67
67
|
# @param io [IO] Open file handle to read fonts from
|
|
68
68
|
# @return [CollectionListInfo] List of fonts with metadata
|
|
@@ -73,7 +73,7 @@ module Fontisan
|
|
|
73
73
|
require_relative "tables/name"
|
|
74
74
|
|
|
75
75
|
fonts = font_offsets.map.with_index do |offset, index|
|
|
76
|
-
font = TrueTypeFont.
|
|
76
|
+
font = TrueTypeFont.from_collection(io, offset)
|
|
77
77
|
|
|
78
78
|
# Extract basic font info
|
|
79
79
|
name_table = font.table("name")
|
|
@@ -119,7 +119,7 @@ module Fontisan
|
|
|
119
119
|
|
|
120
120
|
# Calculate table sharing statistics
|
|
121
121
|
#
|
|
122
|
-
#
|
|
122
|
+
# Uses the base class from_collection method.
|
|
123
123
|
#
|
|
124
124
|
# @param io [IO] Open file handle
|
|
125
125
|
# @return [TableSharingInfo] Sharing statistics
|
|
@@ -129,7 +129,7 @@ module Fontisan
|
|
|
129
129
|
|
|
130
130
|
# Extract all fonts
|
|
131
131
|
fonts = font_offsets.map do |offset|
|
|
132
|
-
TrueTypeFont.
|
|
132
|
+
TrueTypeFont.from_collection(io, offset)
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
# Build table hash map (checksum -> size)
|
|
@@ -23,66 +23,8 @@ module Fontisan
|
|
|
23
23
|
# ttf.to_file("output.ttf")
|
|
24
24
|
#
|
|
25
25
|
# @example Reading from TTC collection
|
|
26
|
-
# ttf = TrueTypeFont.
|
|
26
|
+
# ttf = TrueTypeFont.from_collection(io, offset)
|
|
27
27
|
class TrueTypeFont < SfntFont
|
|
28
|
-
# Read TrueType Font from a file
|
|
29
|
-
#
|
|
30
|
-
# @param path [String] Path to the TTF file
|
|
31
|
-
# @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
|
|
32
|
-
# @param lazy [Boolean] If true, load tables on demand (default: false)
|
|
33
|
-
# @return [TrueTypeFont] A new instance
|
|
34
|
-
# @raise [ArgumentError] if path is nil or empty, or if mode is invalid
|
|
35
|
-
# @raise [Errno::ENOENT] if file does not exist
|
|
36
|
-
# @raise [RuntimeError] if file format is invalid
|
|
37
|
-
def self.from_file(path, mode: LoadingModes::FULL, lazy: false)
|
|
38
|
-
if path.nil? || path.to_s.empty?
|
|
39
|
-
raise ArgumentError,
|
|
40
|
-
"path cannot be nil or empty"
|
|
41
|
-
end
|
|
42
|
-
raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
|
|
43
|
-
|
|
44
|
-
# Validate mode
|
|
45
|
-
LoadingModes.validate_mode!(mode)
|
|
46
|
-
|
|
47
|
-
File.open(path, "rb") do |io|
|
|
48
|
-
font = read(io)
|
|
49
|
-
font.initialize_storage
|
|
50
|
-
font.loading_mode = mode
|
|
51
|
-
font.lazy_load_enabled = lazy
|
|
52
|
-
|
|
53
|
-
if lazy
|
|
54
|
-
# Reuse existing IO handle by duplicating it to prevent double file open
|
|
55
|
-
# The dup ensures the handle stays open after this block closes
|
|
56
|
-
font.io_source = io.dup
|
|
57
|
-
font.setup_finalizer
|
|
58
|
-
else
|
|
59
|
-
# Read tables upfront
|
|
60
|
-
font.read_table_data(io)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
font
|
|
64
|
-
end
|
|
65
|
-
rescue BinData::ValidityError, EOFError => e
|
|
66
|
-
raise "Invalid TTF file: #{e.message}"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Read TrueType Font from TTC at specific offset
|
|
70
|
-
#
|
|
71
|
-
# @param io [IO] Open file handle
|
|
72
|
-
# @param offset [Integer] Byte offset to the font
|
|
73
|
-
# @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
|
|
74
|
-
# @return [TrueTypeFont] A new instance
|
|
75
|
-
def self.from_ttc(io, offset, mode: LoadingModes::FULL)
|
|
76
|
-
LoadingModes.validate_mode!(mode)
|
|
77
|
-
|
|
78
|
-
io.seek(offset)
|
|
79
|
-
font = read(io)
|
|
80
|
-
font.initialize_storage
|
|
81
|
-
font.loading_mode = mode
|
|
82
|
-
font.read_table_data(io)
|
|
83
|
-
font
|
|
84
|
-
end
|
|
85
|
-
|
|
86
28
|
# Check if font is TrueType flavored
|
|
87
29
|
#
|
|
88
30
|
# @return [Boolean] true for TrueType fonts
|
|
@@ -153,7 +153,8 @@ module Fontisan
|
|
|
153
153
|
elsif @copyright.nil? && (match = line.match(/^Notice\s+(\S.*)/i))
|
|
154
154
|
@copyright = match[1].strip
|
|
155
155
|
elsif @font_bbox.nil? && (match = line.match(/^FontBBox\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$/i))
|
|
156
|
-
@font_bbox = [match[1].to_i, match[2].to_i, match[3].to_i,
|
|
156
|
+
@font_bbox = [match[1].to_i, match[2].to_i, match[3].to_i,
|
|
157
|
+
match[4].to_i]
|
|
157
158
|
end
|
|
158
159
|
|
|
159
160
|
# Break early if all metrics found
|