fontisan 0.2.11 → 0.2.13
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 +294 -52
- data/Gemfile +5 -0
- data/README.adoc +163 -2
- data/docs/CONVERSION_GUIDE.adoc +633 -0
- data/docs/TYPE1_FONTS.adoc +445 -0
- data/lib/fontisan/cli.rb +177 -6
- data/lib/fontisan/commands/convert_command.rb +32 -1
- data/lib/fontisan/commands/info_command.rb +83 -2
- 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 +17 -5
- data/lib/fontisan/converters/outline_converter.rb +78 -4
- data/lib/fontisan/converters/type1_converter.rb +1234 -0
- data/lib/fontisan/font_loader.rb +46 -3
- data/lib/fontisan/hints/hint_converter.rb +4 -1
- 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/cff_to_type1_converter.rb +302 -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 +576 -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 +369 -0
- data/lib/fontisan/type1/pfa_parser.rb +159 -0
- data/lib/fontisan/type1/pfb_generator.rb +314 -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 +342 -0
- data/lib/fontisan/type1/seac_expander.rb +501 -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 +75 -0
- data/lib/fontisan/type1_font.rb +318 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +2 -0
- metadata +30 -3
- data/docs/DOCUMENTATION_SUMMARY.md +0 -141
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Type1
|
|
5
|
+
# PFM (Printer Font Metrics) file parser
|
|
6
|
+
#
|
|
7
|
+
# [`PFMParser`](lib/fontisan/type1/pfm_parser.rb) parses Printer Font Metrics
|
|
8
|
+
# files which contain font metric information for Type 1 fonts on Windows.
|
|
9
|
+
#
|
|
10
|
+
# PFM files are binary files that include:
|
|
11
|
+
# - Character widths
|
|
12
|
+
# - Kerning pairs
|
|
13
|
+
# - Font metadata (name, version, copyright, etc.)
|
|
14
|
+
# - Extended text metrics
|
|
15
|
+
#
|
|
16
|
+
# @example Parse a PFM file
|
|
17
|
+
# pfm = Fontisan::Type1::PFMParser.parse_file("font.pfm")
|
|
18
|
+
# puts pfm.font_name
|
|
19
|
+
# puts pfm.character_widths['A']
|
|
20
|
+
# puts pfm.kerning_pairs[['A', 'V']]
|
|
21
|
+
#
|
|
22
|
+
# @see https://www.adobe.com/devnet/font/pdfs/5005.PFM_Spec.pdf
|
|
23
|
+
class PFMParser
|
|
24
|
+
# PFM Header structure
|
|
25
|
+
PFM_HEADER_SIZE = 256
|
|
26
|
+
PFM_VERSION = 0x0100
|
|
27
|
+
|
|
28
|
+
# @return [String] Font name
|
|
29
|
+
attr_reader :font_name
|
|
30
|
+
|
|
31
|
+
# @return [String] Full name
|
|
32
|
+
attr_reader :full_name
|
|
33
|
+
|
|
34
|
+
# @return [String] Family name
|
|
35
|
+
attr_reader :family_name
|
|
36
|
+
|
|
37
|
+
# @return [String] Copyright notice
|
|
38
|
+
attr_reader :copyright
|
|
39
|
+
|
|
40
|
+
# @return [Hash<String, Integer>] Character widths (glyph index => width)
|
|
41
|
+
attr_reader :character_widths
|
|
42
|
+
|
|
43
|
+
# @return [Hash<Array(Integer), Integer>] Kerning pairs ([left_idx, right_idx] => adjustment)
|
|
44
|
+
attr_reader :kerning_pairs
|
|
45
|
+
|
|
46
|
+
# @return [Hash] Extended text metrics
|
|
47
|
+
attr_reader :extended_metrics
|
|
48
|
+
|
|
49
|
+
# @return [Integer] Font bounding box [llx, lly, urx, ury]
|
|
50
|
+
attr_reader :font_bbox
|
|
51
|
+
|
|
52
|
+
# @return [String] Raw data
|
|
53
|
+
attr_reader :raw_data
|
|
54
|
+
|
|
55
|
+
# Parse PFM file
|
|
56
|
+
#
|
|
57
|
+
# @param path [String] Path to PFM file
|
|
58
|
+
# @return [PFMParser] Parsed PFM data
|
|
59
|
+
# @raise [ArgumentError] If path is nil
|
|
60
|
+
# @raise [Fontisan::Error] If file cannot be read or parsed
|
|
61
|
+
def self.parse_file(path)
|
|
62
|
+
raise ArgumentError, "Path cannot be nil" if path.nil?
|
|
63
|
+
|
|
64
|
+
unless File.exist?(path)
|
|
65
|
+
raise Fontisan::Error, "PFM file not found: #{path}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
content = File.binread(path)
|
|
69
|
+
parse(content)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Parse PFM content
|
|
73
|
+
#
|
|
74
|
+
# @param content [String] PFM file content (binary)
|
|
75
|
+
# @return [PFMParser] Parsed PFM data
|
|
76
|
+
def self.parse(content)
|
|
77
|
+
new.parse(content)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Alias for parse method
|
|
81
|
+
def self.parse_string(content)
|
|
82
|
+
parse(content)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Initialize a new PFMParser
|
|
86
|
+
def initialize
|
|
87
|
+
@character_widths = {}
|
|
88
|
+
@kerning_pairs = {}
|
|
89
|
+
@extended_metrics = {}
|
|
90
|
+
@font_bbox = nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Parse PFM content
|
|
94
|
+
#
|
|
95
|
+
# @param content [String] PFM file content (binary)
|
|
96
|
+
# @return [PFMParser] Self for method chaining
|
|
97
|
+
def parse(content)
|
|
98
|
+
@raw_data = content
|
|
99
|
+
parse_header(content)
|
|
100
|
+
parse_driver_info(content)
|
|
101
|
+
parse_extended_metrics(content)
|
|
102
|
+
parse_character_widths(content)
|
|
103
|
+
parse_kerning_pairs(content)
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Get character width for character index
|
|
108
|
+
#
|
|
109
|
+
# @param char_index [Integer] Character index
|
|
110
|
+
# @return [Integer, nil] Character width or nil if not found
|
|
111
|
+
def width(char_index)
|
|
112
|
+
@character_widths[char_index]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Get kerning adjustment for character pair
|
|
116
|
+
#
|
|
117
|
+
# @param left_idx [Integer] Left character index
|
|
118
|
+
# @param right_idx [Integer] Right character index
|
|
119
|
+
# @return [Integer, nil] Kerning adjustment or nil if not found
|
|
120
|
+
def kerning(left_idx, right_idx)
|
|
121
|
+
@kerning_pairs[[left_idx, right_idx]]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check if character exists
|
|
125
|
+
#
|
|
126
|
+
# @param char_index [Integer] Character index
|
|
127
|
+
# @return [Boolean] True if character exists
|
|
128
|
+
def has_character?(char_index)
|
|
129
|
+
@character_widths.key?(char_index)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Parse PFM header
|
|
135
|
+
#
|
|
136
|
+
# @param content [String] PFM content
|
|
137
|
+
def parse_header(content)
|
|
138
|
+
# Read first 256 bytes as header
|
|
139
|
+
return if content.length < PFM_HEADER_SIZE
|
|
140
|
+
|
|
141
|
+
# Version (2 bytes at offset 0)
|
|
142
|
+
read_uint16(content, 0)
|
|
143
|
+
# dfVersion = version
|
|
144
|
+
|
|
145
|
+
# Size info (4 bytes at offset 2)
|
|
146
|
+
# dfSize = read_uint32(content, 2)
|
|
147
|
+
|
|
148
|
+
# Copyright (60 bytes at offset 6)
|
|
149
|
+
@copyright = read_pascal_string(content[6, 60])
|
|
150
|
+
|
|
151
|
+
# Font type (2 bytes at offset 66)
|
|
152
|
+
# dfType = read_uint16(content, 66)
|
|
153
|
+
|
|
154
|
+
# Points (2 bytes at offset 68)
|
|
155
|
+
# dfPoints = read_uint16(content, 68)
|
|
156
|
+
|
|
157
|
+
# VertRes (2 bytes at offset 70)
|
|
158
|
+
# dfVertRes = read_uint16(content, 70)
|
|
159
|
+
|
|
160
|
+
# HorizRes (2 bytes at offset 72)
|
|
161
|
+
# dfHorizRes = read_uint16(content, 72)
|
|
162
|
+
|
|
163
|
+
# Ascent (2 bytes at offset 74)
|
|
164
|
+
# dfAscent = read_uint16(content, 74)
|
|
165
|
+
|
|
166
|
+
# InternalLeading (2 bytes at offset 76)
|
|
167
|
+
# dfInternalLeading = read_uint16(content, 76)
|
|
168
|
+
|
|
169
|
+
# ExternalLeading (2 bytes at offset 78)
|
|
170
|
+
# dfExternalLeading = read_uint16(content, 78)
|
|
171
|
+
|
|
172
|
+
# Italic (1 byte at offset 80)
|
|
173
|
+
# dfItalic = content.getbyte(80)
|
|
174
|
+
|
|
175
|
+
# Underline (1 byte at offset 81)
|
|
176
|
+
# dfUnderline = content.getbyte(81)
|
|
177
|
+
|
|
178
|
+
# StrikeOut (1 byte at offset 82)
|
|
179
|
+
# dfStrikeOut = content.getbyte(82)
|
|
180
|
+
|
|
181
|
+
# Weight (2 bytes at offset 83)
|
|
182
|
+
# dfWeight = read_uint16(content, 83)
|
|
183
|
+
|
|
184
|
+
# CharSet (1 byte at offset 85)
|
|
185
|
+
# dfCharSet = content.getbyte(85)
|
|
186
|
+
|
|
187
|
+
# PixWidth (2 bytes at offset 86)
|
|
188
|
+
# dfPixWidth = read_uint16(content, 86)
|
|
189
|
+
|
|
190
|
+
# PixHeight (2 bytes at offset 88)
|
|
191
|
+
# dfPixHeight = read_uint16(content, 88)
|
|
192
|
+
|
|
193
|
+
# PitchAndFamily (1 byte at offset 90)
|
|
194
|
+
# dfPitchAndFamily = content.getbyte(90)
|
|
195
|
+
|
|
196
|
+
# AverageWidth (2 bytes at offset 91)
|
|
197
|
+
# dfAverageWidth = read_uint16(content, 91)
|
|
198
|
+
|
|
199
|
+
# MaxWidth (2 bytes at offset 93)
|
|
200
|
+
# dfMaxWidth = read_uint16(content, 93)
|
|
201
|
+
|
|
202
|
+
# FirstChar (1 byte at offset 95)
|
|
203
|
+
# dfFirstChar = content.getbyte(95)
|
|
204
|
+
|
|
205
|
+
# LastChar (1 byte at offset 96)
|
|
206
|
+
# dfLastChar = content.getbyte(96)
|
|
207
|
+
|
|
208
|
+
# DefaultChar (1 byte at offset 97)
|
|
209
|
+
# dfDefaultChar = content.getbyte(97)
|
|
210
|
+
|
|
211
|
+
# BreakChar (1 byte at offset 98)
|
|
212
|
+
# dfBreakChar = content.getbyte(98)
|
|
213
|
+
|
|
214
|
+
# WidthBytes (2 bytes at offset 99)
|
|
215
|
+
# dfWidthBytes = read_uint16(content, 99)
|
|
216
|
+
|
|
217
|
+
# Device (4 bytes at offset 101)
|
|
218
|
+
# dfDevice = read_uint32(content, 101)
|
|
219
|
+
|
|
220
|
+
# Face (4 bytes at offset 105)
|
|
221
|
+
# dfFace = read_uint32(content, 105)
|
|
222
|
+
|
|
223
|
+
# Device name (usually empty in PFM)
|
|
224
|
+
# BitsPointer (4 bytes at offset 109)
|
|
225
|
+
# dfBitsPointer = read_uint32(content, 109)
|
|
226
|
+
|
|
227
|
+
# BitsOffset (4 bytes at offset 113)
|
|
228
|
+
# dfBitsOffset = read_uint32(content, 113)
|
|
229
|
+
|
|
230
|
+
# Font name offset (4 bytes at offset 117)
|
|
231
|
+
@dfFace_offset = read_uint32(content, 105)
|
|
232
|
+
|
|
233
|
+
# Ext metrics offset (4 bytes at offset 117)
|
|
234
|
+
@dfExtMetrics_offset = read_uint32(content, 117)
|
|
235
|
+
|
|
236
|
+
# Ext table offset (4 bytes at offset 121)
|
|
237
|
+
@dfExtentTable_offset = read_uint32(content, 121)
|
|
238
|
+
|
|
239
|
+
# Origin table offset (4 bytes at offset 125)
|
|
240
|
+
# dfOriginTable = read_uint32(content, 125)
|
|
241
|
+
|
|
242
|
+
# PairKernTable offset (4 bytes at offset 129)
|
|
243
|
+
@dfPairKernTable_offset = read_uint32(content, 129)
|
|
244
|
+
|
|
245
|
+
# TrackKernTable offset (4 bytes at offset 133)
|
|
246
|
+
# dfTrackKernTable = read_uint32(content, 133)
|
|
247
|
+
|
|
248
|
+
# DriverInfo offset (4 bytes at offset 137)
|
|
249
|
+
@dfDriverInfo_offset = read_uint32(content, 137)
|
|
250
|
+
|
|
251
|
+
# Reserved (4 bytes at offset 141)
|
|
252
|
+
# dfReserved = read_uint32(content, 141)
|
|
253
|
+
|
|
254
|
+
# Signature (4 bytes at offset 145)
|
|
255
|
+
# dfSignature = read_uint32(content, 145)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Parse driver info to get font name
|
|
259
|
+
#
|
|
260
|
+
# @param content [String] PFM content
|
|
261
|
+
def parse_driver_info(content)
|
|
262
|
+
return unless @dfFace_offset&.positive?
|
|
263
|
+
|
|
264
|
+
# Read font name at dfFace offset (byte 105 in header)
|
|
265
|
+
# Font name is a Pascal-style string
|
|
266
|
+
offset = @dfFace_offset
|
|
267
|
+
return if offset >= content.length
|
|
268
|
+
|
|
269
|
+
@font_name = read_pascal_string(content[offset..])
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Parse extended text metrics
|
|
273
|
+
#
|
|
274
|
+
# @param content [String] PFM content
|
|
275
|
+
def parse_extended_metrics(content)
|
|
276
|
+
return unless @dfExtMetrics_offset&.positive?
|
|
277
|
+
|
|
278
|
+
offset = @dfExtMetrics_offset
|
|
279
|
+
return if offset + 48 > content.length
|
|
280
|
+
|
|
281
|
+
# Extended text metrics are 48 bytes
|
|
282
|
+
# etmSize (4 bytes)
|
|
283
|
+
# etmPointSize (4 bytes)
|
|
284
|
+
# etmOrientation (4 bytes)
|
|
285
|
+
# etmMasterHeight (4 bytes)
|
|
286
|
+
# etmMinScale (4 bytes)
|
|
287
|
+
# etmMaxScale (4 bytes)
|
|
288
|
+
# etmMasterUnits (4 bytes)
|
|
289
|
+
# etmCapHeight (4 bytes)
|
|
290
|
+
# etmXHeight (4 bytes)
|
|
291
|
+
# etmLowerCaseAscent (4 bytes)
|
|
292
|
+
# etmLowerCaseDescent (4 bytes)
|
|
293
|
+
# etmSlant (4 bytes)
|
|
294
|
+
# etmSuperScript (4 bytes)
|
|
295
|
+
# etmSubScript (4 bytes)
|
|
296
|
+
# etmSuperScriptSize (4 bytes)
|
|
297
|
+
# etmSubScriptSize (4 bytes)
|
|
298
|
+
# etmUnderlineOffset (4 bytes)
|
|
299
|
+
# etmUnderlineWidth (4 bytes)
|
|
300
|
+
# etmDoubleUpperUnderlineOffset (4 bytes)
|
|
301
|
+
# etmDoubleLowerUnderlineOffset (4 bytes)
|
|
302
|
+
# etmDoubleUpperUnderlineWidth (4 bytes)
|
|
303
|
+
# etmDoubleLowerUnderlineWidth (4 bytes)
|
|
304
|
+
# etmStrikeOutOffset (4 bytes)
|
|
305
|
+
# etmStrikeOutWidth (4 bytes)
|
|
306
|
+
# etmKernPairs (4 bytes)
|
|
307
|
+
# etmKernTracks (4 bytes)
|
|
308
|
+
|
|
309
|
+
# Just read some key metrics
|
|
310
|
+
@extended_metrics[:cap_height] = read_int32(content, offset + 28)
|
|
311
|
+
@extended_metrics[:x_height] = read_int32(content, offset + 32)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Parse character width table
|
|
315
|
+
#
|
|
316
|
+
# @param content [String] PFM content
|
|
317
|
+
def parse_character_widths(content)
|
|
318
|
+
return unless @dfExtentTable_offset&.positive?
|
|
319
|
+
|
|
320
|
+
offset = @dfExtentTable_offset
|
|
321
|
+
return if offset >= content.length
|
|
322
|
+
|
|
323
|
+
# Read extent table
|
|
324
|
+
# The extent table is an array of 2-byte values
|
|
325
|
+
# First value is the number of extents
|
|
326
|
+
num_extents = read_uint16(content, offset)
|
|
327
|
+
offset += 2
|
|
328
|
+
|
|
329
|
+
num_extents.times do |i|
|
|
330
|
+
break if offset + 2 > content.length
|
|
331
|
+
|
|
332
|
+
width = read_uint16(content, offset)
|
|
333
|
+
@character_widths[i] = width
|
|
334
|
+
offset += 2
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Parse kerning pairs
|
|
339
|
+
#
|
|
340
|
+
# @param content [String] PFM content
|
|
341
|
+
def parse_kerning_pairs(content)
|
|
342
|
+
return unless @dfPairKernTable_offset&.positive?
|
|
343
|
+
|
|
344
|
+
offset = @dfPairKernTable_offset
|
|
345
|
+
return if offset >= content.length
|
|
346
|
+
|
|
347
|
+
# Read number of kern pairs (2 bytes)
|
|
348
|
+
num_pairs = read_uint16(content, offset)
|
|
349
|
+
return if num_pairs.zero?
|
|
350
|
+
|
|
351
|
+
offset += 2
|
|
352
|
+
# Skip size info (2 bytes)
|
|
353
|
+
offset += 2
|
|
354
|
+
|
|
355
|
+
# Each kern pair is 6 bytes:
|
|
356
|
+
# - First character index (2 bytes)
|
|
357
|
+
# - Second character index (2 bytes)
|
|
358
|
+
# - Kerning amount (2 bytes)
|
|
359
|
+
num_pairs.times do
|
|
360
|
+
break if offset + 6 > content.length
|
|
361
|
+
|
|
362
|
+
first = read_uint16(content, offset)
|
|
363
|
+
second = read_uint16(content, offset + 2)
|
|
364
|
+
amount = read_int16(content, offset + 4)
|
|
365
|
+
|
|
366
|
+
@kerning_pairs[[first, second]] = amount
|
|
367
|
+
offset += 6
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Read 16-bit unsigned integer (little-endian)
|
|
372
|
+
#
|
|
373
|
+
# @param data [String] Binary data
|
|
374
|
+
# @param offset [Integer] Offset to read from
|
|
375
|
+
# @return [Integer] 16-bit unsigned integer
|
|
376
|
+
def read_uint16(data, offset)
|
|
377
|
+
return 0 if offset + 2 > data.length
|
|
378
|
+
|
|
379
|
+
data.getbyte(offset) | (data.getbyte(offset + 1) << 8)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Read 32-bit unsigned integer (little-endian)
|
|
383
|
+
#
|
|
384
|
+
# @param data [String] Binary data
|
|
385
|
+
# @param offset [Integer] Offset to read from
|
|
386
|
+
# @return [Integer] 32-bit unsigned integer
|
|
387
|
+
def read_uint32(data, offset)
|
|
388
|
+
return 0 if offset + 4 > data.length
|
|
389
|
+
|
|
390
|
+
data.getbyte(offset) |
|
|
391
|
+
(data.getbyte(offset + 1) << 8) |
|
|
392
|
+
(data.getbyte(offset + 2) << 16) |
|
|
393
|
+
(data.getbyte(offset + 3) << 24)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Read 32-bit signed integer (little-endian)
|
|
397
|
+
#
|
|
398
|
+
# @param data [String] Binary data
|
|
399
|
+
# @param offset [Integer] Offset to read from
|
|
400
|
+
# @return [Integer] 32-bit signed integer
|
|
401
|
+
def read_int32(data, offset)
|
|
402
|
+
value = read_uint32(data, offset)
|
|
403
|
+
# Convert to signed
|
|
404
|
+
value >= 0x80000000 ? value - 0x100000000 : value
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Read 16-bit signed integer (little-endian)
|
|
408
|
+
#
|
|
409
|
+
# @param data [String] Binary data
|
|
410
|
+
# @param offset [Integer] Offset to read from
|
|
411
|
+
# @return [Integer] 16-bit signed integer
|
|
412
|
+
def read_int16(data, offset)
|
|
413
|
+
value = read_uint16(data, offset)
|
|
414
|
+
# Convert to signed
|
|
415
|
+
value >= 0x8000 ? value - 0x10000 : value
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Read Pascal-style string
|
|
419
|
+
#
|
|
420
|
+
# @param data [String] Binary data starting with length byte
|
|
421
|
+
# @return [String] String value
|
|
422
|
+
def read_pascal_string(data)
|
|
423
|
+
return "" if data.nil? || data.empty?
|
|
424
|
+
|
|
425
|
+
length = data.getbyte(0)
|
|
426
|
+
return "" if length.nil? || length.zero? || length > data.length - 1
|
|
427
|
+
|
|
428
|
+
data[1, length].to_s.force_encoding("ASCII-8BIT").encode("UTF-8",
|
|
429
|
+
invalid: :replace, undef: :replace)
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|