Skip to content

Commit

Permalink
Fix maxp table
Browse files Browse the repository at this point in the history
Original approach was right but there was an error in field sizes. This
lead to shift of the following field. One of the fields is
maxStackElements which defines stack size for font program. FreeFont
(also used in Ghostscript) is very strict about stack size. Incorrect
maxStackElements value lead to stack overflow in FreeFont. Consequently,
Ghostscript couldn't parse the font and completely discarded it.
  • Loading branch information
camertron authored and pointlessone committed Dec 14, 2023
1 parent 54bcc79 commit 3e4b108
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 11 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).

Alexander Mankuta

* `maxp` table

The table is now correctly parsed and encoded for both TrueType and CFF-based
OpenType fonts.

Cameron Dutro, Alexander Mankuta

## 1.7.0

### Changes
Expand Down
151 changes: 141 additions & 10 deletions lib/ttfunk/table/maxp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
module TTFunk
class Table
class Maxp < Table
DEFAULT_MAX_COMPONENT_DEPTH = 1
MAX_V1_TABLE_LENGTH = 32

attr_reader :version
attr_reader :num_glyphs
attr_reader :max_points
Expand All @@ -21,21 +24,149 @@ class Maxp < Table
attr_reader :max_component_elements
attr_reader :max_component_depth

def self.encode(maxp, mapping)
num_glyphs = mapping.length
raw = maxp.raw
raw[4, 2] = [num_glyphs].pack('n')
raw
class << self
def encode(maxp, new2old_glyph)
''.b.tap do |table|
num_glyphs = new2old_glyph.length
table << [maxp.version, num_glyphs].pack('Nn')

if maxp.version == 0x10000
stats = stats_for(
maxp, glyphs_from_ids(maxp, new2old_glyph.values)
)

table << [
stats[:max_points],
stats[:max_contours],
stats[:max_component_points],
stats[:max_component_contours],
# these all come from the fpgm and cvt tables, which
# we don't support at the moment
maxp.max_zones,
maxp.max_twilight_points,
maxp.max_storage,
maxp.max_function_defs,
maxp.max_instruction_defs,
maxp.max_stack_elements,
stats[:max_size_of_instructions],
stats[:max_component_elements],
stats[:max_component_depth]
].pack('n*')
end
end
end

private

def glyphs_from_ids(maxp, glyph_ids)
glyph_ids.each_with_object([]) do |glyph_id, ret|
if (glyph = maxp.file.glyph_outlines.for(glyph_id))
ret << glyph
end
end
end

def stats_for(maxp, glyphs)
stats_for_simple(maxp, glyphs)
.merge(stats_for_compound(maxp, glyphs))
.transform_values { |agg| agg.value_or(0) }
end

def stats_for_simple(_maxp, glyphs)
max_component_elements = Max.new
max_points = Max.new
max_contours = Max.new
max_size_of_instructions = Max.new

glyphs.each do |glyph|
if glyph.compound?
max_component_elements << glyph.glyph_ids.size
else
max_points << glyph.end_point_of_last_contour
max_contours << glyph.number_of_contours
max_size_of_instructions << glyph.instruction_length
end
end

{
max_component_elements: max_component_elements,
max_points: max_points,
max_contours: max_contours,
max_size_of_instructions: max_size_of_instructions
}
end

def stats_for_compound(maxp, glyphs)
max_component_points = Max.new
max_component_depth = Max.new
max_component_contours = Max.new

glyphs.each do |glyph|
next unless glyph.compound?

stats = totals_for_compound(maxp, [glyph], 0)
max_component_points << stats[:total_points]
max_component_depth << stats[:max_depth]
max_component_contours << stats[:total_contours]
end

{
max_component_points: max_component_points,
max_component_depth: max_component_depth,
max_component_contours: max_component_contours
}
end

def totals_for_compound(maxp, glyphs, depth)
total_points = Sum.new
total_contours = Sum.new
max_depth = Max.new(depth)

glyphs.each do |glyph|
if glyph.compound?
stats = totals_for_compound(
maxp, glyphs_from_ids(maxp, glyph.glyph_ids), depth + 1
)

total_points << stats[:total_points]
total_contours << stats[:total_contours]
max_depth << stats[:max_depth]
else
stats = stats_for_simple(maxp, [glyph])
total_points << stats[:max_points]
total_contours << stats[:max_contours]
end
end

{
total_points: total_points,
total_contours: total_contours,
max_depth: max_depth
}
end
end

private

def parse!
@version, @num_glyphs, @max_points, @max_contours,
@max_component_points, @max_component_contours, @max_zones,
@max_twilight_points, @max_storage, @max_function_defs,
@max_instruction_defs, @max_stack_elements, @max_size_of_instructions,
@max_component_elements, @max_component_depth = read(length, 'Nn*')
@version, @num_glyphs = read(6, 'Nn')

if @version == 0x10000
@max_points, @max_contours, @max_component_points,
@max_component_contours, @max_zones, @max_twilight_points,
@max_storage, @max_function_defs, @max_instruction_defs,
@max_stack_elements, @max_size_of_instructions,
@max_component_elements = read(24, 'n*')

# a number of fonts omit these last two bytes for some reason,
# so we have to supply a default here to prevent nils
@max_component_depth =
if length == MAX_V1_TABLE_LENGTH
read(2, 'n').first
else
DEFAULT_MAX_COMPONENT_DEPTH
end
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/ttfunk/ttf_encoder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@

# verified via the Font-Validator tool at:
# https://github.com/HinTak/Font-Validator
expect(checksum).to eq(0xEE3A9625)
expect(checksum).to eq(0xEEAE9DCF)
end
end
end

0 comments on commit 3e4b108

Please sign in to comment.