fontisan 0.2.11 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +214 -51
- data/README.adoc +160 -0
- data/lib/fontisan/cli.rb +177 -6
- data/lib/fontisan/commands/convert_command.rb +32 -1
- data/lib/fontisan/config/conversion_matrix.yml +132 -4
- data/lib/fontisan/constants.rb +12 -0
- data/lib/fontisan/conversion_options.rb +378 -0
- data/lib/fontisan/converters/collection_converter.rb +45 -10
- data/lib/fontisan/converters/format_converter.rb +2 -0
- data/lib/fontisan/converters/outline_converter.rb +78 -4
- data/lib/fontisan/converters/type1_converter.rb +559 -0
- data/lib/fontisan/font_loader.rb +46 -3
- data/lib/fontisan/type1/afm_generator.rb +436 -0
- data/lib/fontisan/type1/afm_parser.rb +298 -0
- data/lib/fontisan/type1/agl.rb +456 -0
- data/lib/fontisan/type1/charstring_converter.rb +240 -0
- data/lib/fontisan/type1/charstrings.rb +408 -0
- data/lib/fontisan/type1/conversion_options.rb +243 -0
- data/lib/fontisan/type1/decryptor.rb +183 -0
- data/lib/fontisan/type1/encodings.rb +697 -0
- data/lib/fontisan/type1/font_dictionary.rb +514 -0
- data/lib/fontisan/type1/generator.rb +220 -0
- data/lib/fontisan/type1/inf_generator.rb +332 -0
- data/lib/fontisan/type1/pfa_generator.rb +343 -0
- data/lib/fontisan/type1/pfa_parser.rb +158 -0
- data/lib/fontisan/type1/pfb_generator.rb +291 -0
- data/lib/fontisan/type1/pfb_parser.rb +166 -0
- data/lib/fontisan/type1/pfm_generator.rb +610 -0
- data/lib/fontisan/type1/pfm_parser.rb +433 -0
- data/lib/fontisan/type1/private_dict.rb +285 -0
- data/lib/fontisan/type1/ttf_to_type1_converter.rb +327 -0
- data/lib/fontisan/type1/upm_scaler.rb +118 -0
- data/lib/fontisan/type1.rb +73 -0
- data/lib/fontisan/type1_font.rb +331 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +2 -0
- metadata +26 -2
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "upm_scaler"
|
|
4
|
+
require_relative "ttf_to_type1_converter"
|
|
5
|
+
require_relative "../tables/name"
|
|
6
|
+
|
|
7
|
+
module Fontisan
|
|
8
|
+
module Type1
|
|
9
|
+
# PFB (Printer Font Binary) Generator
|
|
10
|
+
#
|
|
11
|
+
# [`PFBGenerator`](lib/fontisan/type1/pfb_generator.rb) generates Type 1 PFB files
|
|
12
|
+
# from TrueType fonts.
|
|
13
|
+
#
|
|
14
|
+
# PFB files are segmented binary files used by Windows for Type 1 fonts.
|
|
15
|
+
# They contain:
|
|
16
|
+
# - ASCII segment: Font dictionary
|
|
17
|
+
# - Binary segment: CharString data
|
|
18
|
+
# - ASCII segment: Trailer
|
|
19
|
+
#
|
|
20
|
+
# @example Generate PFB from TTF
|
|
21
|
+
# font = Fontisan::FontLoader.load("font.ttf")
|
|
22
|
+
# pfb_data = Fontisan::Type1::PFBGenerator.generate(font)
|
|
23
|
+
# File.binwrite("font.pfb", pfb_data)
|
|
24
|
+
#
|
|
25
|
+
# @example Generate PFB with custom options
|
|
26
|
+
# options = { upm_scale: 1000, format: :pfb }
|
|
27
|
+
# pfb_data = Fontisan::Type1::PFBGenerator.generate(font, options)
|
|
28
|
+
#
|
|
29
|
+
# @see https://www.adobe.com/devnet/font/pdfs/5178.Type1.pdf
|
|
30
|
+
class PFBGenerator
|
|
31
|
+
# PFB segment markers
|
|
32
|
+
ASCII_SEGMENT = 0x01
|
|
33
|
+
BINARY_SEGMENT = 0x02
|
|
34
|
+
END_SEGMENT = 0x03
|
|
35
|
+
|
|
36
|
+
# Header format string
|
|
37
|
+
PFB_HEADER = "%%!PS-AdobeFont-1.0: %s 1.0\n"
|
|
38
|
+
|
|
39
|
+
# Generate PFB from TTF font
|
|
40
|
+
#
|
|
41
|
+
# @param font [Fontisan::Font] Source TTF font
|
|
42
|
+
# @param options [Hash] Generation options
|
|
43
|
+
# @option options [Integer, :native] :upm_scale Target UPM (default: 1000)
|
|
44
|
+
# @option options [Class] :encoding Encoding class (default: Encodings::AdobeStandard)
|
|
45
|
+
# @option options [Boolean] :convert_curves Convert quadratic to cubic (default: true)
|
|
46
|
+
# @return [String] PFB file content (binary)
|
|
47
|
+
def self.generate(font, options = {})
|
|
48
|
+
new(font, options).generate
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize(font, options = {})
|
|
52
|
+
@font = font
|
|
53
|
+
@options = options
|
|
54
|
+
@metrics = MetricsCalculator.new(font)
|
|
55
|
+
|
|
56
|
+
# Set up scaler
|
|
57
|
+
upm_scale = options[:upm_scale] || 1000
|
|
58
|
+
@scaler = if upm_scale == :native
|
|
59
|
+
UPMScaler.native(font)
|
|
60
|
+
else
|
|
61
|
+
UPMScaler.new(font, target_upm: upm_scale)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Set up encoding
|
|
65
|
+
@encoding = options[:encoding] || Encodings::AdobeStandard
|
|
66
|
+
|
|
67
|
+
# Set up converter options
|
|
68
|
+
@convert_curves = options.fetch(:convert_curves, true)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Generate PFB file content
|
|
72
|
+
#
|
|
73
|
+
# @return [String] PFB binary content
|
|
74
|
+
def generate
|
|
75
|
+
# Build PFB segments
|
|
76
|
+
ascii_segment1 = build_ascii_segment_1
|
|
77
|
+
binary_segment = build_binary_segment
|
|
78
|
+
ascii_segment2 = build_ascii_segment_2
|
|
79
|
+
|
|
80
|
+
# Combine with segment headers
|
|
81
|
+
[
|
|
82
|
+
segment_header(ASCII_SEGMENT, ascii_segment1.bytesize),
|
|
83
|
+
ascii_segment1,
|
|
84
|
+
segment_header(BINARY_SEGMENT, binary_segment.bytesize),
|
|
85
|
+
binary_segment,
|
|
86
|
+
segment_header(ASCII_SEGMENT, ascii_segment2.bytesize),
|
|
87
|
+
ascii_segment2,
|
|
88
|
+
[END_SEGMENT, 0, 0, 0, 0, 0].pack("CV"),
|
|
89
|
+
].join
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# Build first ASCII segment (font dictionary)
|
|
95
|
+
#
|
|
96
|
+
# @return [String] ASCII font dictionary
|
|
97
|
+
def build_ascii_segment_1
|
|
98
|
+
lines = []
|
|
99
|
+
lines << format(PFB_HEADER, @font.post_script_name)
|
|
100
|
+
lines << build_font_dict
|
|
101
|
+
lines << build_private_dict
|
|
102
|
+
lines << build_charstrings_dict
|
|
103
|
+
lines.join("\n")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Build font dictionary
|
|
107
|
+
#
|
|
108
|
+
# @return [String] Font dictionary in PostScript
|
|
109
|
+
def build_font_dict
|
|
110
|
+
dict = []
|
|
111
|
+
dict << "10 dict begin"
|
|
112
|
+
dict << "/FontType 1 def"
|
|
113
|
+
dict << "/FontMatrix [0.001 0 0 0.001 0 0] def"
|
|
114
|
+
|
|
115
|
+
# Font info
|
|
116
|
+
name_table = @font.table(Constants::NAME_TAG)
|
|
117
|
+
if name_table
|
|
118
|
+
font_name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME) || @font.post_script_name
|
|
119
|
+
dict << "/FontName /#{font_name} def"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Bounding box
|
|
123
|
+
head = @font.table(Constants::HEAD_TAG)
|
|
124
|
+
if head
|
|
125
|
+
bbox = [
|
|
126
|
+
@scaler.scale(head.x_min || 0),
|
|
127
|
+
@scaler.scale(head.y_min || 0),
|
|
128
|
+
@scaler.scale(head.x_max || 1000),
|
|
129
|
+
@scaler.scale(head.y_max || 1000),
|
|
130
|
+
]
|
|
131
|
+
dict << "/FontBBox {#{bbox.join(' ')}} def"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Paint type
|
|
135
|
+
dict << "/PaintType 0 def"
|
|
136
|
+
|
|
137
|
+
# Encoding
|
|
138
|
+
if @encoding == Encodings::AdobeStandard
|
|
139
|
+
dict << "/Encoding StandardEncoding def"
|
|
140
|
+
elsif @encoding == Encodings::ISOLatin1
|
|
141
|
+
dict << "/Encoding ISOLatin1Encoding def"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
dict << "currentdict end"
|
|
145
|
+
dict << "dup /FontName get exch definefont pop"
|
|
146
|
+
|
|
147
|
+
dict.join("\n")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Build Private dictionary
|
|
151
|
+
#
|
|
152
|
+
# @return [String] Private dictionary in PostScript
|
|
153
|
+
def build_private_dict
|
|
154
|
+
dict = []
|
|
155
|
+
dict << "/Private 15 dict begin"
|
|
156
|
+
|
|
157
|
+
# Blue values (for hinting)
|
|
158
|
+
# These are typically derived from the font's alignment zones
|
|
159
|
+
dict << "/BlueValues [-20 0 500 510] def"
|
|
160
|
+
dict << "/BlueScale 0.039625 def"
|
|
161
|
+
dict << "/BlueShift 7 def"
|
|
162
|
+
dict << "/BlueFuzz 1 def"
|
|
163
|
+
|
|
164
|
+
# Stem snap hints
|
|
165
|
+
os2 = @font.table(Constants::OS2_TAG)
|
|
166
|
+
if os2.respond_to?(:weight_class)
|
|
167
|
+
stem_width = @scaler.scale([100, 80,
|
|
168
|
+
90][os2.weight_class / 100] || 80)
|
|
169
|
+
dict << "/StemSnapH [#{stem_width}] def"
|
|
170
|
+
dict << "/StemSnapV [#{stem_width}] def"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Force bold flag
|
|
174
|
+
dict << if os2.respond_to?(:weight_class) && os2.weight_class && os2.weight_class >= 700
|
|
175
|
+
"/ForceBold true def"
|
|
176
|
+
else
|
|
177
|
+
"/ForceBold false def"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Language group
|
|
181
|
+
dict << "/LanguageGroup 0 def"
|
|
182
|
+
|
|
183
|
+
# Unique ID (random)
|
|
184
|
+
dict << "/UniqueID #{rand(1000000..9999999)} def"
|
|
185
|
+
|
|
186
|
+
dict << "currentdict end"
|
|
187
|
+
dict << "dup /Private get"
|
|
188
|
+
|
|
189
|
+
dict.join("\n")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Build CharStrings dictionary
|
|
193
|
+
#
|
|
194
|
+
# @return [String] CharStrings dictionary reference
|
|
195
|
+
def build_charstrings_dict
|
|
196
|
+
# This is a placeholder - actual CharStrings are in the binary segment
|
|
197
|
+
"/CharStrings #{@charstrings&.size || 0} dict dup begin\nend"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Build binary segment (CharStrings)
|
|
201
|
+
#
|
|
202
|
+
# @return [String] Binary CharString data
|
|
203
|
+
def build_binary_segment
|
|
204
|
+
# Convert glyphs to Type 1 CharStrings
|
|
205
|
+
charstrings = if @convert_curves
|
|
206
|
+
TTFToType1Converter.convert(@font, @scaler, @encoding)
|
|
207
|
+
else
|
|
208
|
+
# For simple curve conversion skip, generate minimal charstrings
|
|
209
|
+
generate_simple_charstrings
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Encode charstrings to eexec format (encrypted)
|
|
213
|
+
# For now, we'll use plain format (not encrypted)
|
|
214
|
+
# TODO: Implement eexec encryption
|
|
215
|
+
|
|
216
|
+
charstrings.values.join
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Generate simple CharStrings (without curve conversion)
|
|
220
|
+
#
|
|
221
|
+
# @return [Hash<Integer, String>] Glyph ID to CharString mapping
|
|
222
|
+
def generate_simple_charstrings
|
|
223
|
+
glyf_table = @font.table(Constants::GLYF_TAG)
|
|
224
|
+
return {} unless glyf_table
|
|
225
|
+
|
|
226
|
+
maxp = @font.table(Constants::MAXP_TAG)
|
|
227
|
+
num_glyphs = maxp&.num_glyphs || 0
|
|
228
|
+
|
|
229
|
+
charstrings = {}
|
|
230
|
+
num_glyphs.times do |gid|
|
|
231
|
+
charstrings[gid] = simple_charstring(glyf_table, gid)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
charstrings
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Generate a simple CharString for a glyph
|
|
238
|
+
#
|
|
239
|
+
# @param glyf_table [Object] TTF glyf table
|
|
240
|
+
# @param gid [Integer] Glyph ID
|
|
241
|
+
# @return [String] Type 1 CharString data
|
|
242
|
+
def simple_charstring(glyf_table, gid)
|
|
243
|
+
glyph = glyf_table.glyph(gid)
|
|
244
|
+
|
|
245
|
+
# Empty or compound glyph
|
|
246
|
+
if glyph.nil? || glyph.contour_count.zero? || glyph.compound?
|
|
247
|
+
# Return empty charstring (hsbw + endchar)
|
|
248
|
+
return [0, 500, 14].pack("C*")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# For simple glyphs without curve conversion, generate line-based charstring
|
|
252
|
+
# This is a simplified implementation
|
|
253
|
+
lsb = @scaler.scale(glyph.left_side_bearing || 0)
|
|
254
|
+
width = @scaler.scale(glyph.advance_width || 500)
|
|
255
|
+
bytes = [0, lsb, width] # hsbw
|
|
256
|
+
|
|
257
|
+
# Add lines between points (simplified)
|
|
258
|
+
if glyph.respond_to?(:points) && glyph.points && !glyph.points.empty?
|
|
259
|
+
glyph.points.each do |point|
|
|
260
|
+
next unless point.on_curve?
|
|
261
|
+
|
|
262
|
+
# This is very simplified - proper implementation would handle curves
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
bytes << 14 # endchar
|
|
267
|
+
bytes.pack("C*")
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Build second ASCII segment (trailer)
|
|
271
|
+
#
|
|
272
|
+
# @return [String] ASCII trailer
|
|
273
|
+
def build_ascii_segment_2
|
|
274
|
+
lines = []
|
|
275
|
+
lines << "put" # Put the Private dictionary
|
|
276
|
+
lines << "dup /FontName get exch definefont pop"
|
|
277
|
+
lines << "% cleartomark"
|
|
278
|
+
lines.join("\n")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Create PFB segment header
|
|
282
|
+
#
|
|
283
|
+
# @param marker [Integer] Segment type marker
|
|
284
|
+
# @param size [Integer] Segment data size
|
|
285
|
+
# @return [String] 6-byte segment header
|
|
286
|
+
def segment_header(marker, size)
|
|
287
|
+
[marker, size].pack("CV")
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Type1
|
|
5
|
+
# Parser for PFB (Printer Font Binary) format
|
|
6
|
+
#
|
|
7
|
+
# [`PFBParser`](lib/fontisan/type1/pfb_parser.rb) parses the binary PFB format
|
|
8
|
+
# used for storing Adobe Type 1 fonts, primarily on Windows systems.
|
|
9
|
+
#
|
|
10
|
+
# The PFB format consists of binary chunks marked with special codes:
|
|
11
|
+
# - 0x8001: ASCII text chunk
|
|
12
|
+
# - 0x8002: Binary data chunk (usually encrypted)
|
|
13
|
+
# - 0x8003: End of file marker
|
|
14
|
+
#
|
|
15
|
+
# Each chunk (except EOF) has a 4-byte little-endian length prefix.
|
|
16
|
+
#
|
|
17
|
+
# @example Parse a PFB file
|
|
18
|
+
# parser = Fontisan::Type1::PFBParser.new
|
|
19
|
+
# result = parser.parse(File.binread('font.pfb'))
|
|
20
|
+
# puts result.ascii_parts # => ["%!PS-AdobeFont-1.0...", ...]
|
|
21
|
+
# puts result.binary_parts # => [encrypted_binary_data, ...]
|
|
22
|
+
#
|
|
23
|
+
# @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
|
|
24
|
+
class PFBParser
|
|
25
|
+
# PFB chunk markers
|
|
26
|
+
ASCII_CHUNK = 0x8001
|
|
27
|
+
BINARY_CHUNK = 0x8002
|
|
28
|
+
EOF_CHUNK = 0x8003
|
|
29
|
+
|
|
30
|
+
# @return [Array<String>] ASCII text parts
|
|
31
|
+
attr_reader :ascii_parts
|
|
32
|
+
|
|
33
|
+
# @return [Array<String>] Binary data parts
|
|
34
|
+
attr_reader :binary_parts
|
|
35
|
+
|
|
36
|
+
# Parse PFB format data
|
|
37
|
+
#
|
|
38
|
+
# @param data [String] Binary PFB data
|
|
39
|
+
# @return [PFBParser] Self for method chaining
|
|
40
|
+
# @raise [ArgumentError] If data is nil or empty
|
|
41
|
+
# @raise [Fontisan::Error] If PFB format is invalid
|
|
42
|
+
def parse(data)
|
|
43
|
+
raise ArgumentError, "Data cannot be nil" if data.nil?
|
|
44
|
+
raise ArgumentError, "Data cannot be empty" if data.empty?
|
|
45
|
+
|
|
46
|
+
@ascii_parts = []
|
|
47
|
+
@binary_parts = []
|
|
48
|
+
|
|
49
|
+
offset = 0
|
|
50
|
+
chunk_index = 0
|
|
51
|
+
|
|
52
|
+
while offset < data.length
|
|
53
|
+
# Check for chunk marker (must have at least 2 bytes)
|
|
54
|
+
if offset + 2 > data.length
|
|
55
|
+
raise Fontisan::Error,
|
|
56
|
+
"Invalid PFB: incomplete chunk header at offset #{offset}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Read chunk marker (big-endian)
|
|
60
|
+
marker = (data.getbyte(offset) << 8) |
|
|
61
|
+
data.getbyte(offset + 1)
|
|
62
|
+
offset += 2
|
|
63
|
+
|
|
64
|
+
case marker
|
|
65
|
+
when ASCII_CHUNK
|
|
66
|
+
chunk = read_chunk(data, offset, chunk_index, "ASCII")
|
|
67
|
+
@ascii_parts << chunk[:data]
|
|
68
|
+
offset = chunk[:next_offset]
|
|
69
|
+
chunk_index += 1
|
|
70
|
+
|
|
71
|
+
when BINARY_CHUNK
|
|
72
|
+
chunk = read_chunk(data, offset, chunk_index, "binary")
|
|
73
|
+
@binary_parts << chunk[:data]
|
|
74
|
+
offset = chunk[:next_offset]
|
|
75
|
+
chunk_index += 1
|
|
76
|
+
|
|
77
|
+
when EOF_CHUNK
|
|
78
|
+
# End of file - no more chunks
|
|
79
|
+
break
|
|
80
|
+
|
|
81
|
+
else
|
|
82
|
+
raise Fontisan::Error,
|
|
83
|
+
"Invalid PFB: unknown chunk marker 0x#{marker.to_s(16).upcase} at offset #{offset - 2}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get all ASCII parts concatenated
|
|
91
|
+
#
|
|
92
|
+
# @return [String] All ASCII parts joined together
|
|
93
|
+
def ascii_text
|
|
94
|
+
@ascii_parts.join
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get all binary parts concatenated
|
|
98
|
+
#
|
|
99
|
+
# @return [String] All binary parts joined together
|
|
100
|
+
def binary_data
|
|
101
|
+
@binary_parts.join
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Check if parser has parsed data
|
|
105
|
+
#
|
|
106
|
+
# @return [Boolean] True if data has been parsed
|
|
107
|
+
def parsed?
|
|
108
|
+
!@ascii_parts.nil? && !@binary_parts.nil?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Check if this appears to be a PFB file
|
|
112
|
+
#
|
|
113
|
+
# @param data [String] Binary data to check
|
|
114
|
+
# @return [Boolean] True if data starts with PFB marker
|
|
115
|
+
#
|
|
116
|
+
# @example Check if file is PFB format
|
|
117
|
+
# if Fontisan::Type1::PFBParser.pfb_file?(data)
|
|
118
|
+
# # Handle PFB format
|
|
119
|
+
# end
|
|
120
|
+
def self.pfb_file?(data)
|
|
121
|
+
return false if data.nil? || data.length < 2
|
|
122
|
+
|
|
123
|
+
# PFB marker is big-endian (first byte is high byte)
|
|
124
|
+
marker = (data.getbyte(0) << 8) | data.getbyte(1)
|
|
125
|
+
[ASCII_CHUNK, BINARY_CHUNK].include?(marker)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
# Read a chunk from PFB data
|
|
131
|
+
#
|
|
132
|
+
# @param data [String] PFB binary data
|
|
133
|
+
# @param offset [Integer] Current offset in data
|
|
134
|
+
# @param chunk_index [Integer] Index of current chunk (for error messages)
|
|
135
|
+
# @param type [String] Type of chunk ("ASCII" or "binary")
|
|
136
|
+
# @return [Hash] Chunk data with :data and :next_offset
|
|
137
|
+
def read_chunk(data, offset, chunk_index, type)
|
|
138
|
+
# Read 4-byte length (little-endian)
|
|
139
|
+
if offset + 4 > data.length
|
|
140
|
+
raise Fontisan::Error,
|
|
141
|
+
"Invalid PFB: incomplete length for #{type} chunk #{chunk_index}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
length = data.getbyte(offset) |
|
|
145
|
+
(data.getbyte(offset + 1) << 8) |
|
|
146
|
+
(data.getbyte(offset + 2) << 16) |
|
|
147
|
+
(data.getbyte(offset + 3) << 24)
|
|
148
|
+
offset += 4
|
|
149
|
+
|
|
150
|
+
# Read chunk data
|
|
151
|
+
if offset + length > data.length
|
|
152
|
+
raise Fontisan::Error,
|
|
153
|
+
"Invalid PFB: #{type} chunk #{chunk_index} length #{length} exceeds remaining data"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
chunk_data = data.byteslice(offset, length)
|
|
157
|
+
next_offset = offset + length
|
|
158
|
+
|
|
159
|
+
{
|
|
160
|
+
data: chunk_data,
|
|
161
|
+
next_offset: next_offset,
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|