Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

TTFunk::Subset

This class is a first pass at building a subset of a font.
  • Loading branch information...
commit 776d71472c8f7a4bccd423d3856c3f3c086a71ef 1 parent e80ad0e
@jamis jamis authored
View
2  example.rb
@@ -20,7 +20,7 @@ def character_lookup(file, character)
end
end
-file = TTFunk::File.new("data/fonts/DejaVuSans.ttf")
+file = TTFunk::File.new(ARGV.first || "data/fonts/DejaVuSans.ttf")
puts "-- FONT ------------------------------------"
View
3  lib/ttfunk/directory.rb
@@ -1,9 +1,10 @@
module TTFunk
class Directory
attr_reader :tables
+ attr_reader :scaler_type
def initialize(io)
- scaler_type, table_count, search_range,
+ @scaler_type, table_count, search_range,
entry_selector, range_shift = io.read(12).unpack("Nn*")
@tables = {}
View
113 lib/ttfunk/subset.rb
@@ -0,0 +1,113 @@
+require 'set'
+require 'ttfunk/table/cmap'
+require 'ttfunk/table/glyf'
+require 'ttfunk/table/hmtx'
+require 'ttfunk/table/kern'
+require 'ttfunk/table/loca'
+
+module TTFunk
+ class Subset
+ attr_reader :original
+
+ def initialize(original)
+ @original = original
+ @subset = Set.new([0])
+ end
+
+ def use(characters)
+ @subset.merge(characters)
+ end
+
+ def encode
+ cmap = original.cmap.unicode.first
+
+ charmap = @subset.inject({}) { |map, code| map[code] = cmap[code]; map }
+ cmap_table = TTFunk::Table::Cmap.encode(charmap)
+
+ glyph_ids = @subset.map { |character| cmap[character] }
+ glyphs = collect_glyphs(glyph_ids)
+
+ old2new_glyph = cmap_table[:charmap].inject({}) { |map, (code, ids)| map[ids[:old]] = ids[:new]; map }
+ next_glyph_id = cmap_table[:max_glyph_id]
+
+ glyphs.keys.each do |old_id|
+ unless old2new_glyph.key?(old_id)
+ old2new_glyph[old_id] = next_glyph_id
+ next_glyph_id += 1
+ end
+ end
+
+ new2old_glyph = old2new_glyph.invert
+
+ glyf_table = TTFunk::Table::Glyf.encode(glyphs, new2old_glyph, old2new_glyph)
+ loca_table = TTFunk::Table::Loca.encode(glyf_table[:offsets])
+ kern_table = TTFunk::Table::Kern.encode(original.kerning, old2new_glyph)
+ hmtx_table = TTFunk::Table::Hmtx.encode(original.horizontal_metrics, new2old_glyph)
+ hhea_table = TTFunk::Table::Hhea.encode(original.horizontal_header, hmtx_table)
+ maxp_table = TTFunk::Table::Maxp.encode(original.maximum_profile, old2new_glyph)
+ os2_table = original.os2.raw
+ post_table = TTFunk::Table::Post.encode(original.postscript, new2old_glyph)
+ name_table = TTFunk::Table::Name.encode(original.name)
+ head_table = TTFunk::Table::Head.encode(original.header, loca_table)
+
+ tables = { 'cmap' => cmap_table[:table],
+ 'glyf' => glyf_table[:table],
+ 'loca' => loca_table[:table],
+ 'kern' => kern_table,
+ 'hmtx' => hmtx_table[:table],
+ 'hhea' => hhea_table,
+ 'maxp' => maxp_table,
+ 'OS/2' => os2_table,
+ 'post' => post_table,
+ 'name' => name_table,
+ 'head' => head_table }
+
+ tables.delete_if { |tag, table| table.nil? }
+
+ search_range = (Math.log(tables.length) / Math.log(2)).to_i * 16
+ entry_selector = (Math.log(search_range) / Math.log(2)).to_i
+ range_shift = tables.length * 16 - search_range
+
+ newfont = [original.directory.scaler_type, tables.length, search_range, entry_selector, range_shift].pack("Nn*")
+
+ directory_size = tables.length * 16
+ offset = newfont.length + directory_size
+
+ table_data = ""
+ head_offset = nil
+ tables.each do |tag, data|
+ newfont << [tag, checksum(data), offset, data.length].pack("A4N*")
+ table_data << data
+ head_offset = offset if tag == 'head'
+ offset += data.length
+ while offset % 4 != 0
+ offset += 1
+ table_data << "\0"
+ end
+ end
+
+ newfont << table_data
+ sum = checksum(newfont)
+ adjustment = 0xB1B0AFBA - sum
+ newfont[head_offset+8,4] = [adjustment].pack("N")
+
+ return newfont
+ end
+
+ private
+
+ def checksum(data)
+ data += "\0" * (4 - data.length % 4) unless data.length % 4 == 0
+ data.unpack("N*").inject(0) { |sum, dword| sum + dword } & 0xFFFF_FFFF
+ end
+
+ def collect_glyphs(glyph_ids)
+ glyphs = glyph_ids.inject({}) { |h, id| h[id] = original.glyph_outlines.for(id); h }
+ additional_ids = glyphs.values.select { |g| g && g.compound? }.map { |g| g.glyph_ids }.flatten
+
+ glyphs.update(collect_glyphs(additional_ids)) if additional_ids.any?
+
+ return glyphs
+ end
+ end
+end
View
8 lib/ttfunk/table.rb
@@ -25,6 +25,14 @@ def exists?
!@offset.nil?
end
+ def raw
+ if exists?
+ parse_from(offset) { io.read(length) }
+ else
+ nil
+ end
+ end
+
def tag
self.class.name.split(/::/).last.downcase
end
View
8 lib/ttfunk/table/cmap.rb
@@ -4,6 +4,14 @@ class Cmap < Table
attr_reader :version
attr_reader :tables
+ def self.encode(charmap)
+ result = Cmap::Subtable.encode(charmap)
+
+ # pack 'version' and 'table-count'
+ result[:table] = [0, 1, result.delete(:subtable)].pack("nnA*")
+ return result
+ end
+
def unicode
@unicode ||= @tables.select { |table| table.unicode? }
end
View
69 lib/ttfunk/table/cmap/format04.rb
@@ -6,6 +6,75 @@ module Format04
attr_reader :language
attr_reader :code_map
+ # Expects a hash mapping character codes to glyph ids (where the
+ # glyph ids are from the original font). Returns a hash including
+ # a new map (:charmap) that maps the characters in charmap to a
+ # another hash containing both the old (:old) and new (:new) glyph
+ # ids. The returned hash also includes a :subtable key, which contains
+ # the encoded subtable for the given charmap.
+ def self.encode(charmap)
+ end_codes = []
+ start_codes = []
+ next_id = 0
+ last = nil
+
+ new_map = charmap.keys.sort.inject({}) do |map, code|
+ map[code] = { :old => charmap[code], :new => next_id }
+ next_id += 1
+
+ if last.nil? || code != last+1
+ end_codes << last if last
+ start_codes << code
+ end
+ last = code
+
+ map
+ end
+
+ end_codes << last if last
+ end_codes << 0xFFFF
+ start_codes << 0xFFFF
+ segcount = start_codes.length
+
+ # build the conversion tables
+ deltas = []
+ range_offsets = []
+ glyph_indices = []
+
+ offset = 0
+ start_codes.zip(end_codes).each_with_index do |(a, b), segment|
+ if a == 0xFFFF
+ deltas << 0
+ range_offsets << 0
+ break
+ end
+
+ start_glyph_id = new_map[a][:new]
+ if a - start_glyph_id >= 0x8000
+ deltas << 0
+ range_offsets << 2 * (glyph_indices.length + segcount - segment)
+ a.upto(b) { |code| glyph_indices << new_map[code][:new] }
+ else
+ deltas << -a + start_glyph_id
+ range_offsets << 0
+ end
+ offset += 2
+ end
+
+ # format, length, language
+ subtable = [4, 16 + 8 * segcount + 2 * glyph_indices.length, 0].pack("nnn")
+
+ search_range = 2 * 2 ** (Math.log(segcount) / Math.log(2)).to_i
+ entry_selector = (Math.log(search_range / 2) / Math.log(2)).to_i
+ range_shift = (2 * segcount) - search_range
+ subtable << [segcount * 2, search_range, entry_selector, range_shift].pack("nnnn")
+
+ subtable << end_codes.pack("n*") << "\0\0" << start_codes.pack("n*")
+ subtable << deltas.pack("n*") << range_offsets.pack("n*") << glyph_indices.pack("n*")
+
+ { :charmap => new_map, :subtable => subtable, :max_glyph_id => next_id }
+ end
+
def [](code)
@code_map[code] || 0
end
View
23 lib/ttfunk/table/cmap/subtable.rb
@@ -10,21 +10,28 @@ class Subtable
attr_reader :encoding_id
attr_reader :format
+ def self.encode(charmap)
+ result = Format04.encode(charmap)
+ # platform-id, encoding-id, offset
+ result[:subtable] = [0, 0, 12, result[:subtable]].pack("nnNA*")
+ return result
+ end
+
def initialize(file, table_start)
@file = file
@platform_id, @encoding_id, @offset = read(8, "nnN")
@offset += table_start
- saved, io.pos = io.pos, @offset
- @format = read(2, "n").first
+ parse_from(@offset) do
+ @format = read(2, "n").first
- case @format
- when 0 then extend(TTFunk::Table::Cmap::Format00)
- when 4 then extend(TTFunk::Table::Cmap::Format04)
- end
+ case @format
+ when 0 then extend(TTFunk::Table::Cmap::Format00)
+ when 4 then extend(TTFunk::Table::Cmap::Format04)
+ end
- parse_cmap!
- io.pos = saved
+ parse_cmap!
+ end
end
def unicode?
View
23 lib/ttfunk/table/glyf.rb
@@ -3,6 +3,29 @@
module TTFunk
class Table
class Glyf < Table
+ # Accepts a hash mapping (old) glyph-ids to glyph objects, and a hash
+ # mapping old glyph-ids to new glyph-ids.
+ #
+ # Returns a hash containing:
+ #
+ # * :table - a string representing the encoded 'glyf' table containing
+ # the given glyphs.
+ # * :offsets - an array of offsets for each glyph
+ def self.encode(glyphs, new2old, old2new)
+ result = { :table => "", :offsets => [] }
+
+ new2old.keys.sort.each do |new_id|
+ glyph = glyphs[new2old[new_id]]
+ result[:offsets] << result[:table].length
+ result[:table] << glyph.recode(old2new) if glyph
+ end
+
+ # include an offset at the end of the table, for use in computing the
+ # size of the last glyph
+ result[:offsets] << result[:table].length
+ return result
+ end
+
def for(glyph_id)
return @cache[glyph_id] if @cache.key?(glyph_id)
View
11 lib/ttfunk/table/glyf/compound.rb
@@ -63,6 +63,17 @@ def initialize(raw, x_min, y_min, x_max, y_max)
def compound?
true
end
+
+ def recode(mapping)
+ result = @raw.dup
+ new_ids = glyph_ids.map { |id| mapping[id] }
+
+ new_ids.zip(@glyph_id_offsets).each do |new_id, offset|
+ result[offset, 2] = [new_id].pack("n")
+ end
+
+ return result
+ end
end
end
end
View
4 lib/ttfunk/table/glyf/simple.rb
@@ -26,6 +26,10 @@ def initialize(raw, number_of_contours, x_min, y_min, x_max, y_max)
def compound?
false
end
+
+ def recode(mapping)
+ raw
+ end
end
end
end
View
7 lib/ttfunk/table/head.rb
@@ -21,6 +21,13 @@ class Head < TTFunk::Table
attr_reader :index_to_loc_format
attr_reader :glyph_data_format
+ def self.encode(head, loca)
+ table = head.raw
+ table[8,4] = "\0\0\0\0" # set checksum adjustment to 0 initially
+ table[-4,2] = [loca[:type]].pack("n") # set index_to_loc_format
+ return table
+ end
+
private
def parse!
View
6 lib/ttfunk/table/hhea.rb
@@ -16,6 +16,12 @@ class Hhea < Table
attr_reader :metric_data_format
attr_reader :number_of_metrics
+ def self.encode(hhea, hmtx)
+ raw = hhea.raw
+ raw[-2,2] = [hmtx[:number_of_metrics]].pack("n")
+ return raw
+ end
+
private
def parse!
View
16 lib/ttfunk/table/hmtx.rb
@@ -7,6 +7,16 @@ class Hmtx < Table
attr_reader :left_side_bearings
attr_reader :widths
+ def self.encode(hmtx, mapping)
+ metrics = mapping.keys.sort.map do |new_id|
+ metric = hmtx.for(new_id)
+ [metric.advance_width, metric.left_side_bearing]
+ end
+
+ { :number_of_metrics => metrics.length,
+ :table => metrics.flatten.pack("n*") }
+ end
+
HorizontalMetric = Struct.new(:advance_width, :left_side_bearing)
def for(glyph_id)
@@ -22,15 +32,15 @@ def parse!
file.horizontal_header.number_of_metrics.times do
advance = read(2, "n").first
- lsb = read_sshort(1).first
+ lsb = read_signed(1).first
@metrics.push HorizontalMetric.new(advance, lsb)
end
lsb_count = file.maximum_profile.num_glyphs - file.horizontal_header.number_of_metrics
- @left_side_bearings = read_sshort(lsb_count)
+ @left_side_bearings = read_signed(lsb_count)
@widths = @metrics.map { |metric| metric.advance_width }
- @widths += @left_side_bearings.length * @widths.last
+ @widths += [@widths.last] * @left_side_bearings.length
end
end
end
View
12 lib/ttfunk/table/kern.rb
@@ -6,6 +6,14 @@ class Kern < Table
attr_reader :version
attr_reader :tables
+ def self.encode(kerning, mapping)
+ return nil unless kerning.exists? && kerning.tables.any?
+ tables = kerning.tables.map { |table| table.recode(mapping) }.compact
+ return nil if tables.empty?
+
+ [0, tables.length, tables.join].pack("nnA*")
+ end
+
private
def parse!
@@ -27,7 +35,7 @@ def parse_version_0_tables(num_tables)
format = coverage >> 8
add_table format, :version => version, :length => length,
- :coverage => coverage, :data => handle.read(length-6),
+ :coverage => coverage, :data => io.read(length-6),
:vertical => (coverage & 0x1 == 0),
:minimum => (coverage & 0x2 != 0),
:cross => (coverage & 0x4 != 0),
@@ -41,7 +49,7 @@ def parse_version_1_tables(num_tables)
format = coverage & 0x0FF
add_table format, :length => length, :coverage => coverage,
- :tuple_index => tuple_index, :data => handle.read(length-8),
+ :tuple_index => tuple_index, :data => io.read(length-8),
:vertical => (coverage & 0x8000 != 0),
:cross => (coverage & 0x4000 != 0),
:variation => (coverage & 0x2000 != 0)
View
21 lib/ttfunk/table/kern/format0.rb
@@ -10,7 +10,6 @@ class Format0
attr_reader :pairs
def initialize(attributes={})
- @file = file
@attributes = attributes
num_pairs, search_range, entry_selector, range_shift, *pairs =
@@ -36,6 +35,26 @@ def horizontal?
def cross_stream?
@attributes[:cross]
end
+
+ def recode(mapping)
+ subset = []
+ pairs.each do |(left, right), value|
+ if mapping[left] && mapping[right]
+ subset << [mapping[left], mapping[right], value]
+ end
+ end
+
+ return nil if subset.empty?
+
+ num_pairs = subset.length
+ search_range = 2 * 2 ** (Math.log(num_pairs) / Math.log(2)).to_i
+ entry_selector = (Math.log(search_range / 2) / Math.log(2)).to_i
+ range_shift = (2 * num_pairs) - search_range
+
+ [attributes[:version], num_pairs * 6 + 14, attributes[:coverage],
+ num_pairs, search_range, entry_selector, range_shift, subset].
+ flatten.pack("n*")
+ end
end
end
end
View
15 lib/ttfunk/table/loca.rb
@@ -5,6 +5,21 @@ class Table
class Loca < Table
attr_reader :offsets
+ # Accepts an array of offsets, with each index corresponding to the
+ # glyph id with that index.
+ #
+ # Returns a hash containing:
+ #
+ # * :table - the string representing the table's contents
+ # * :type - the type of offset (to be encoded in the 'head' table)
+ def self.encode(offsets)
+ if offsets.any? { |ofs| ofs > 0xFFFF }
+ { :type => 1, :table => offsets.pack("N*") }
+ else
+ { :type => 0, :table => offsets.map { |o| o/2 }.pack("n*") }
+ end
+ end
+
def index_of(glyph_id)
@offsets[glyph_id]
end
View
7 lib/ttfunk/table/maxp.rb
@@ -19,6 +19,13 @@ 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")
+ return raw
+ end
+
private
def parse!
View
31 lib/ttfunk/table/name.rb
@@ -24,7 +24,6 @@ def initialize(text, platform_id, encoding_id, language_id)
attr_reader :unique_subfamily
attr_reader :font_name
attr_reader :version
- attr_reader :postscript_name
attr_reader :trademark
attr_reader :manufacturer
attr_reader :designer
@@ -38,6 +37,36 @@ def initialize(text, platform_id, encoding_id, language_id)
attr_reader :compatible_full
attr_reader :sample_text
+ @@subset_tag = "AAAAAA"
+
+ def self.encode(names)
+ tag = @@subset_tag.dup
+ @@subset_tag.succ!
+
+ postscript_name = Name::String.new("#{tag}+#{names.postscript_name}", 1, 0, 0)
+
+ strings = names.strings.dup
+ strings[6] = [postscript_name]
+ str_count = strings.inject(0) { |sum, (id, list)| sum + list.length }
+
+ table = [0, str_count, 6 + 12 * str_count].pack("n*")
+ strtable = ""
+
+ strings.each do |id, list|
+ list.each do |string|
+ table << [string.platform_id, string.encoding_id, string.language_id, id, string.length, strtable.length].pack("n*")
+ strtable << string
+ end
+ end
+
+ table << strtable
+ end
+
+ def postscript_name
+ return @postscript_name if @postscript_name
+ font_family.first || "unnamed"
+ end
+
private
def parse!
View
37 lib/ttfunk/table/post.rb
@@ -15,6 +15,11 @@ class Post < Table
attr_reader :subtable
+ def self.encode(post, mapping)
+ return nil unless post.exists?
+ post.recode(mapping)
+ end
+
def fixed_pitch?
@fixed_pitch != 0
end
@@ -23,6 +28,34 @@ def glyph_for(code)
".notdef"
end
+ def recode(mapping)
+ return raw if format == 0x00030000
+
+ table = raw[0,32]
+ table[0,4] = [0x00020000].pack("N")
+
+ index = []
+ strings = []
+
+ mapping.keys.sort.each do |new_id|
+ post_glyph = glyph_for(mapping[new_id])
+ position = Format10::POSTSCRIPT_GLYPHS.index(post_glyph)
+ if position
+ index << position
+ else
+ index << 257 + strings.length
+ strings << post_glyph
+ end
+ end
+
+ table << [mapping.length, *index].pack("n*")
+ strings.each do |string|
+ table << [string.length, string].pack("CA*")
+ end
+
+ return table
+ end
+
private
def parse!
@@ -40,10 +73,10 @@ def parse!
when 0x00040000 then extend(Post::Format40)
end
- parse_table!
+ parse_format!
end
- def parse_table!
+ def parse_format!
warn "postscript table format 0x%08X is not supported" % @format
end
end
Please sign in to comment.
Something went wrong with that request. Please try again.