# ttfsubset # Copyright (C) 2006 Mathieu Blondel # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA $KCODE = "u" $LOAD_PATH.unshift("../lib/") require 'ttf' =begin We are going to reduce the number of glyphs. Required tables concerned are: - cmap - glyf - hmtx - loca - maxp - post Optional tables concerned are: - vmtx Non-supported tables should be removed because they may become incorrect if number of @glyphs changed. =end class UnsupportedFont < Exception end class Subsetter def add_glyph(glyph, glyph_id, char_code) unless @old_to_new_glyph_id.include? glyph_id @glyphs << glyph new_glyph_id = @glyphs.length - 1 @glyph_ids << new_glyph_id @old_to_new_glyph_id[glyph_id] = new_glyph_id @charmaps[char_code] = new_glyph_id #$stderr.puts "Old Glyph #{glyph_id} => New Glyph #{new_glyph_id}" #$stderr.puts "Char Code #{char_code} " + \ # "(#{[char_code].pack("U")}) " + \ # "=> New Glyph #{new_glyph_id}\n\n" @hmetrics << @hmtx_tbl.hmetrics[glyph_id] @postscript_names << @post_tbl.names[glyph_id] \ if @post_tbl.names.include? glyph_id end end def add_composite_glyph(glyph, glyph_id, char_code) glyph_offsets = @font.get_table(:loca).glyph_offsets glyph.components.each do |gc| offs = glyph_offsets[gc.index] gc_glyph = @font.get_table(:glyf).get_glyph_at_offset(offs) gc_cc = @enc_tbl.get_unicode_for_glyph_id(gc.index) if gc_glyph.composite? add_composite_glyph(gc_glyph, gc.index, gc_cc) else add_glyph(gc_glyph, gc.index, gc_cc) end gc.index = @old_to_new_glyph_id[gc.index] end add_glyph(glyph, glyph_id, char_code) end def initialize(filename, input_chars) @font = Font::TTF::File.new(filename) @subset = Font::TTF::File.new @enc_tbl = @font.get_table(:cmap).encoding_tables.find do |t| t.class == Font::TTF::Table::Cmap::EncodingTable4 end raise UnsupportedFont, "Needs an encoding table 4 (unicode)" \ if @enc_tbl.nil? or not @enc_tbl.unicode? @charmaps = {} @glyphs = [] @glyph_ids = [] @hmetrics = [] @postscript_names = [] @hmtx_tbl = @font.get_table(:hmtx) @post_tbl = @font.get_table(:post) # Glyph 0 (unsupported chars match to this glyph) @glyph_ids << 0 @glyphs << @font.get_table(:glyf).get_glyph_at_offset(0) @hmetrics << @hmtx_tbl.hmetrics[0] @postscript_names << @post_tbl.names[0] if @post_tbl.names.include? 0 @charmaps[0] = 0 ascii_range = (0..255).to_a @old_to_new_glyph_id = {} @input_chars = input_chars @input_chars.each do |char_code| glyph_id = @enc_tbl.get_glyph_id_for_unicode(char_code) if glyph_id != 0 # glyph_id == 0 means character is not supported by @font glyph = @enc_tbl.get_glyph_for_unicode(char_code) if glyph.simple? add_glyph(glyph, glyph_id, char_code) else add_composite_glyph(glyph, glyph_id, char_code) end end end # Cmap subset_cmap_tbl = @subset.get_new_table(:cmap) subset_enc_tbl = subset_cmap_tbl.get_new_encoding_table4 subset_enc_tbl.platform_id = \ Font::TTF::Encodings::Platform::UNICODE subset_enc_tbl.encoding_id = \ Font::TTF::Encodings::UnicodeEncoding::UNICODE subset_enc_tbl.charmaps = @charmaps subset_cmap_tbl.encoding_tables << subset_enc_tbl #subset_enc_tbl.charmaps.each do |char_code, glyph_id| #$stderr.puts "Char #{char_code} => Glyph #{glyph_id}" #end glyph_id_array = [0] * 255 @charmaps.each do |char_code, glyph_id| if char_code <= 255 glyph_id_array[char_code] = glyph_id end end subset_enc_tbl0 = subset_cmap_tbl.get_new_encoding_table0 subset_enc_tbl0.glyph_id_array = glyph_id_array subset_cmap_tbl.encoding_tables << subset_enc_tbl0 subset_enc_tbl = subset_cmap_tbl.get_new_encoding_table4 subset_enc_tbl.platform_id = \ Font::TTF::Encodings::Platform::MICROSOFT subset_enc_tbl.encoding_id = \ Font::TTF::Encodings::MicrosoftEncoding::UNICODE subset_enc_tbl.charmaps = @charmaps subset_cmap_tbl.encoding_tables << subset_enc_tbl @subset.set_table(subset_cmap_tbl) # Glyf needs Maxp and Post that's why we put them before # Maxp @subset.set_table(@font.get_table(:maxp)) # Post subset_post_tbl = @font.get_table(:post) subset_post_tbl.names = @postscript_names @subset.set_table(subset_post_tbl) # Glyf subset_glyf_tbl = @subset.get_new_table(:glyf) subset_glyf_tbl.glyphs = @glyphs @subset.set_table(subset_glyf_tbl) # Head @subset.set_table(@font.get_table(:head)) # Hhea @subset.set_table(@font.get_table(:hhea)) # Hmtx subset_hmtx_tbl = @subset.get_new_table(:hmtx) subset_hmtx_tbl.hmetrics = @hmetrics @subset.set_table(subset_hmtx_tbl) # Loca @subset.set_table(@subset.get_new_table(:loca)) # Name name_tbl = @font.get_table(:name) name_tbl.name_records.find_all do |nr| [Font::TTF::Table::Name::NameRecord::UNIQUE_FONT_IDENTIFIER, Font::TTF::Table::Name::NameRecord::FONT_FAMILY_NAME, Font::TTF::Table::Name::NameRecord::FULL_FONT_NAME, Font::TTF::Table::Name::NameRecord::POSTSCRIPT_NAME].include? \ nr.name_id and (nr.unicode? or nr.roman?) end.each do |nr| nr.utf8_str += "_subset" end @subset.set_table(name_tbl) # OS/2 os2_tbl = @font.get_table(:"OS/2") os2_tbl.us_first_char_index = @input_chars.first os2_tbl.us_last_char_index = @input_chars.last @subset.set_table(os2_tbl) # Kerning if @font.table_list.include? :kern kern_tbl = @font.get_table(:kern) subtbl = kern_tbl.subtables.find { |stbl| stbl.format == 0 } kerning_pairs = [] unless subtbl.nil? subtbl.kerning_pairs.each do |kp| if @old_to_new_glyph_id.include? kp.left and \ @old_to_new_glyph_id.include? kp.right kp.left = @old_to_new_glyph_id[kp.left] kp.right = @old_to_new_glyph_id[kp.right] kerning_pairs << kp end end end unless kerning_pairs.empty? subset_kern_tbl = @subset.get_new_table(:kern) subset_kern_subtbl = subset_kern_tbl.get_new_kerning_subtable0 subset_kern_subtbl.kerning_pairs = kerning_pairs subset_kern_tbl.subtables << subset_kern_subtbl @subset.set_table(subset_kern_tbl) end end # Optional tables which are important for font hinting (grid-fitting) [:cvt, :fpgm, :gasp, :prep].each do |tbl| @subset.set_table(@font.get_table(tbl)) if @font.table_list.include? tbl end end def dump @subset.dump end end require 'pathname' input_chars = $stdin.readlines.join.unpack("U*").uniq.sort ARGV.each_with_index do |filename, i| begin subset_path = "subset_" + Pathname.new(filename).basename puts "Creating " + subset_path + "..." File.new(subset_path, File::CREAT|File::RDWR).write(Subsetter.new(filename, input_chars).dump) rescue UnsupportedFont, Font::TTF::MalformedFont, Font::TTF::TableMissing => e puts e end end