glyphic 0.1.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 738496f374c2a619ecfe954d756b61e8c9acd315f58fe987bf9ea0a8ea74780d
4
- data.tar.gz: e6930b83fabcc8ece85a292d3730f1b41aa32506facd362afbd9b8bc97679e9c
3
+ metadata.gz: e0e68eb3afceb9ab4a9fd3f4d3ca5d4e358481b0ec4e9929366bbddb015aa74c
4
+ data.tar.gz: cd24a61fb5ff6a44b92a24c02bc68ff3512b4850efee64c4ab402d138e036062
5
5
  SHA512:
6
- metadata.gz: 141454631eac879ceeb67733f6a690d4dbdcd1381412121bb9337d003d3a475745f7c3198781be499de3e3734291655509627d1db9d9f0b9748e0ea6d17d2f97
7
- data.tar.gz: fd9853fa5bfdf46d2b5beb67758ee0eebaa90e109c7c4907cb505bcd7b1786ca35c8d43f88992207be6d2f1af27c2a54d903fee0145cbe1d3e3b78965edcd291
6
+ metadata.gz: 772f4464e4ecf3dd68e2a510dbec1349f8cc8c9a55c2f0e3123240540baff074182d34e61fd421987b671f8617619b108dcbe5d53aab59043dd8755dcfe12211
7
+ data.tar.gz: 66345c6bbf892723eefde799a1f327c75daf575c88a8ef51f10de9975040df110a6149233f1be20c60e5965c0701e1df9d438a9090e9420dce351b3faaeb4ce0
data/.codespellignore ADDED
File without changes
data/.yamllint ADDED
@@ -0,0 +1,16 @@
1
+ extends: default
2
+ ignore: |
3
+ build/
4
+ node_modules/
5
+ runtime/build/
6
+ tmp/
7
+ **/*.dSYM/**
8
+
9
+ rules:
10
+ comments:
11
+ min-spaces-from-content: 1
12
+ document-start: disable
13
+ line-length:
14
+ max: 200
15
+ truthy:
16
+ check-keys: false
data/CHANGELOG.md CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.1.2] - 2026-09-26
6
+
7
+ - Reject truncated or malformed BDF and TrueType fonts with clear errors.
8
+
5
9
  ## [0.1.1] - 2026-09-25
6
10
 
7
- - Correct the gem author and license attribution.
8
- - Bound the Tessel dependency to compatible major versions.
11
+ - Prevent installation with unsupported major versions of Tessel.
9
12
 
10
13
  ## [0.1.0] - 2026-09-23
11
14
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Glyphic
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/glyphic.rb CHANGED
@@ -123,12 +123,17 @@ module Glyphic
123
123
  block << lines[index]
124
124
  index += 1
125
125
  end
126
+ raise UnsupportedError, "invalid BDF glyph terminator" unless lines[index] == "ENDCHAR"
127
+
126
128
  encoded = block.find { |line| line.start_with?("ENCODING") }&.split&.last
127
129
  code = encoded&.to_i
128
130
  if code && code >= 0
129
131
  code = jis_to_unicode(code) if registry.start_with?("JISX0208")
130
132
  if code
131
- glyph = parse_glyph(block, advance: block.find { |line| line.start_with?("DWIDTH") }&.split&.last.to_i)
133
+ advance = block.find { |line| line.start_with?("DWIDTH ") }&.split&.[](1)
134
+ raise UnsupportedError, "invalid BDF glyph advance" unless advance&.match?(/\A-?\d+\z/)
135
+
136
+ glyph = parse_glyph(block, advance: advance.to_i)
132
137
  glyphs[code] = glyph
133
138
  else
134
139
  skipped += 1
@@ -142,10 +147,16 @@ module Glyphic
142
147
  end
143
148
 
144
149
  def parse_glyph(block, advance:)
145
- bbx = block.find { |line| line.start_with?("BBX") }.split.drop(1).map(&:to_i)
150
+ bbx = block.find { |line| line.start_with?("BBX ") }&.split&.drop(1)&.map { |value| Integer(value, exception: false) }
151
+ raise UnsupportedError, "invalid BDF glyph bounds" unless bbx&.length == 4 && bbx.all? && bbx[0] >= 0 && bbx[1] >= 0
152
+
146
153
  width, height, offset_x, offset_y = bbx
147
154
  bitmap_start = block.index("BITMAP")
155
+ raise UnsupportedError, "invalid BDF glyph bitmap" unless bitmap_start
156
+
148
157
  rows = block[(bitmap_start + 1), height].to_a
158
+ raise UnsupportedError, "invalid BDF glyph bitmap" unless rows.length == height && rows.all? { |row| row.match?(/\A[0-9A-Fa-f]*\z/) && row.length * 4 >= width }
159
+
149
160
  alpha = rows.map do |line|
150
161
  value = line.to_i(16)
151
162
  (0...width).map { |x| (value & (1 << ([line.length * 4, width].max - x - 1))).positive? ? 255 : 0 }
@@ -172,16 +183,29 @@ module Glyphic
172
183
  def initialize(bytes)
173
184
  @bytes = bytes.b
174
185
  @tables = {}
186
+ raise UnsupportedError, "invalid TrueType table directory" unless @bytes.bytesize >= 12
187
+
175
188
  count = @bytes.byteslice(4, 2).unpack1("n")
189
+ raise UnsupportedError, "invalid TrueType table directory" if 12 + count * 16 > @bytes.bytesize
190
+
176
191
  count.times do |index|
177
192
  tag, _checksum, offset, length = @bytes.byteslice(12 + index * 16, 16).unpack("a4N3")
193
+ raise UnsupportedError, "invalid TrueType table data" if offset > @bytes.bytesize || length > @bytes.bytesize - offset
194
+
178
195
  @tables[tag] = @bytes.byteslice(offset, length)
179
196
  end
180
197
  end
181
198
 
182
- def u16(data, offset) = data.byteslice(offset, 2).unpack1("n")
183
- def i16(data, offset) = data.byteslice(offset, 2).unpack1("s>")
184
- def u32(data, offset) = data.byteslice(offset, 4).unpack1("N")
199
+ def slice(data, offset, length)
200
+ part = data&.byteslice(offset, length) if offset >= 0 && length >= 0
201
+ raise UnsupportedError, "invalid TrueType table data" unless part && part.bytesize == length
202
+
203
+ part
204
+ end
205
+
206
+ def u16(data, offset) = slice(data, offset, 2).unpack1("n")
207
+ def i16(data, offset) = slice(data, offset, 2).unpack1("s>")
208
+ def u32(data, offset) = slice(data, offset, 4).unpack1("N")
185
209
  end
186
210
 
187
211
  class Font < Glyphic::Font
@@ -238,13 +262,14 @@ module Glyphic
238
262
 
239
263
  def build_cmap(data)
240
264
  records = @reader.u16(data, 2).times.map do |index|
241
- platform, encoding, offset = data.byteslice(4 + index * 8, 8).unpack("n2N")
265
+ platform, encoding, offset = @reader.slice(data, 4 + index * 8, 8).unpack("n2N")
242
266
  [platform, encoding, offset]
243
267
  end
244
268
  selected = records.sort_by { |platform, encoding, _| platform == 3 && encoding == 10 ? 0 : platform == 3 && encoding == 1 ? 1 : platform.zero? ? 2 : 3 }.find { |_, _, offset| [4, 12].include?(@reader.u16(data, offset)) }
245
269
  return {} unless selected
246
270
  format = @reader.u16(data, selected[2])
247
- format == 12 ? cmap12(data.byteslice(selected[2]..)) : cmap4(data.byteslice(selected[2]..))
271
+ subtable = @reader.slice(data, selected[2], data.bytesize - selected[2])
272
+ format == 12 ? cmap12(subtable) : cmap4(subtable)
248
273
  end
249
274
 
250
275
  def build_kern(data)
@@ -258,7 +283,7 @@ module Glyphic
258
283
  if coverage & 0xff == 0
259
284
  base = offset + 6
260
285
  @reader.u16(data, base).times do |index|
261
- left, right, value = data.byteslice(base + 8 + index * 6, 6).unpack("n2s>")
286
+ left, right, value = @reader.slice(data, base + 8 + index * 6, 6).unpack("n2s>")
262
287
  pairs[[left, right]] = value
263
288
  end
264
289
  end
@@ -270,7 +295,7 @@ module Glyphic
270
295
  def cmap12(data)
271
296
  result = {}
272
297
  @reader.u32(data, 12).times do |index|
273
- start_code, end_code, start_glyph = data.byteslice(16 + index * 12, 12).unpack("N3")
298
+ start_code, end_code, start_glyph = @reader.slice(data, 16 + index * 12, 12).unpack("N3")
274
299
  (start_code..end_code).each { |code| result[code] = start_glyph + code - start_code }
275
300
  end
276
301
  result
@@ -278,13 +303,13 @@ module Glyphic
278
303
 
279
304
  def cmap4(data)
280
305
  segments = @reader.u16(data, 6) / 2
281
- end_codes = data.byteslice(14, segments * 2).unpack("n*")
306
+ end_codes = @reader.slice(data, 14, segments * 2).unpack("n*")
282
307
  start_offset = 16 + segments * 2
283
- start_codes = data.byteslice(start_offset, segments * 2).unpack("n*")
308
+ start_codes = @reader.slice(data, start_offset, segments * 2).unpack("n*")
284
309
  delta_offset = start_offset + segments * 2
285
- deltas = data.byteslice(delta_offset, segments * 2).unpack("s>*")
310
+ deltas = @reader.slice(data, delta_offset, segments * 2).unpack("s>*")
286
311
  range_offset = delta_offset + segments * 2
287
- offsets = data.byteslice(range_offset, segments * 2).unpack("n*")
312
+ offsets = @reader.slice(data, range_offset, segments * 2).unpack("n*")
288
313
  result = {}
289
314
  end_codes.each_index do |segment|
290
315
  (start_codes[segment]..end_codes[segment]).each do |code|
@@ -324,26 +349,26 @@ module Glyphic
324
349
  finish = glyph_offset(index + 1)
325
350
  return [[], [], 0, 0, 0, 0, []] if finish <= offset
326
351
 
327
- glyph = @glyf.byteslice(offset, finish - offset)
352
+ glyph = @reader.slice(@glyf, offset, finish - offset)
328
353
  contours = @reader.i16(glyph, 0)
329
354
  return composite_outline(index, glyph, stack) if contours.negative?
330
355
 
331
- end_points = glyph.byteslice(10, contours * 2).unpack("n*")
356
+ end_points = @reader.slice(glyph, 10, contours * 2).unpack("n*")
332
357
  point_count = end_points.last.to_i + 1
333
358
  cursor = 10 + contours * 2
334
359
  instruction_length = @reader.u16(glyph, cursor)
335
360
  cursor += 2 + instruction_length
336
361
  flags = []
337
362
  while flags.length < point_count
338
- flag = glyph.getbyte(cursor)
363
+ flag = @reader.slice(glyph, cursor, 1).getbyte(0)
339
364
  cursor += 1
340
- repeat = flag.anybits?(8) ? glyph.getbyte(cursor).tap { cursor += 1 } : 0
365
+ repeat = flag.anybits?(8) ? @reader.slice(glyph, cursor, 1).getbyte(0).tap { cursor += 1 } : 0
341
366
  flags.concat([flag] * (repeat + 1))
342
367
  end
343
368
  xs = read_coordinates(glyph, flags, cursor, point_count, horizontal: true)
344
369
  ys = read_coordinates(glyph, flags, xs[1], point_count, horizontal: false)
345
370
  raw_points = xs[0].zip(ys[0]).map { |x, y| [x, y] }
346
- x_min, y_min, x_max, y_max = glyph.byteslice(2, 8).unpack("s>4")
371
+ x_min, y_min, x_max, y_max = @reader.slice(glyph, 2, 8).unpack("s>4")
347
372
  points, flattened_end_points = flatten_contours(raw_points, flags, end_points)
348
373
  [points, flattened_end_points, x_min, y_min, x_max, y_max, raw_points]
349
374
  end
@@ -368,11 +393,11 @@ module Glyphic
368
393
  cursor += 4
369
394
  word_args = flags.anybits?(1)
370
395
  arg1, arg2 = if word_args
371
- values = glyph.byteslice(cursor, 4).unpack("s>2")
396
+ values = @reader.slice(glyph, cursor, 4).unpack("s>2")
372
397
  cursor += 4
373
398
  values
374
399
  else
375
- values = glyph.byteslice(cursor, 2).unpack("c2")
400
+ values = @reader.slice(glyph, cursor, 2).unpack("c2")
376
401
  cursor += 2
377
402
  values
378
403
  end
@@ -476,9 +501,9 @@ module Glyphic
476
501
 
477
502
  def component_transform(glyph, flags, cursor)
478
503
  if flags.anybits?(128)
479
- glyph.byteslice(cursor, 8).unpack("s>4").map { |value| value / 16_384.0 }
504
+ @reader.slice(glyph, cursor, 8).unpack("s>4").map { |value| value / 16_384.0 }
480
505
  elsif flags.anybits?(64)
481
- x, y = glyph.byteslice(cursor, 4).unpack("s>2").map { |value| value / 16_384.0 }
506
+ x, y = @reader.slice(glyph, cursor, 4).unpack("s>2").map { |value| value / 16_384.0 }
482
507
  [x, 0, 0, y]
483
508
  elsif flags.anybits?(8)
484
509
  scale = @reader.i16(glyph, cursor) / 16_384.0
@@ -502,13 +527,13 @@ module Glyphic
502
527
  short = horizontal ? flag.anybits?(2) : flag.anybits?(4)
503
528
  same = horizontal ? flag.anybits?(16) : flag.anybits?(32)
504
529
  if short
505
- delta = glyph.getbyte(cursor)
530
+ delta = @reader.slice(glyph, cursor, 1).getbyte(0)
506
531
  cursor += 1
507
532
  delta = -delta if !same
508
533
  elsif same
509
534
  delta = 0
510
535
  else
511
- delta = glyph.byteslice(cursor, 2).unpack1("s>")
536
+ delta = @reader.slice(glyph, cursor, 2).unpack1("s>")
512
537
  cursor += 2
513
538
  end
514
539
  previous += delta
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glyphic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-25 00:00:00.000000000 Z
11
+ date: 2026-09-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tessel
@@ -37,6 +37,8 @@ executables: []
37
37
  extensions: []
38
38
  extra_rdoc_files: []
39
39
  files:
40
+ - ".codespellignore"
41
+ - ".yamllint"
40
42
  - CHANGELOG.md
41
43
  - LICENSE.txt
42
44
  - README.md