# TTF/Ruby, a library to read and write TrueType fonts in Ruby.
# 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
module Font
module TTF
module Table
# Cmap is the character to glyph index mapping table.
class Cmap < Font::TTF::FontChunk
# Base class for encoding table classes. It provides attributes which are
# common to those classes such as platform_id and encoding_id.
class EncodingTable < Font::TTF::FontChunk
attr_accessor :table, :platform_id, :encoding_id, :offset_from_table
def initialize(table, offset=nil, len=nil, platform_id=nil,
encoding_id=nil)
@table = table
@font = table.font
if not offset.nil?
@offset_from_table = offset
@platform_id = platform_id
@encoding_id = encoding_id
super(@table.font, @table.offset + offset, len)
else
super(@table.font)
end
end
def unicode?
@platform_id == Font::TTF::Encodings::Platform::UNICODE or \
(@platform_id == Font::TTF::Encodings::Platform::MICROSOFT and \
@encoding_id == Font::TTF::Encodings::MicrosoftEncoding::UNICODE)
end
# Returns the format (a Fixnum) of the encoding table.
def format
if self.class.superclass == EncodingTable
self.class.name.split(//).last.to_i
else
nil # Not implemented table format
end
end
end
# Encoding table in format 0, the Apple standard character to glyph
# mapping table.
class EncodingTable0 < EncodingTable
attr_accessor :version
# An array of 256 elements with simple one to one mapping of
# character codes to glyph indices.
#
# Char 0 => Index glyph_id_array[0]
# Char 1 => Index glyph_id_array[1]
# ...
attr_accessor :glyph_id_array
def initialize(*args)
super(*args)
if exists_in_file?
# 2 * IO::SIZEOF_USHORT corresponds to format and len
# that we want to skip
@table.font.at_offset(@offset + 2 * IO::SIZEOF_USHORT) do
@version = @font.read_ushort
@glyph_id_array = @font.read_bytes(256)
end
else
# This is to ensure that it will be an array
@glyph_id_array = []
end
end
# Dumps the table in binary raw format as may be found in a font
# file.
def dump
raw = (format || 0).to_ushort
len = 3 * IO::SIZEOF_USHORT + 256
raw += len.to_ushort
raw += (@version || 0).to_ushort
raw += @glyph_id_array.to_bytes
end
end
# Encoding table in format 2. Not implemented.
class EncodingTable2 < EncodingTable
end
# Encoding table in format 4. This format is well-suited to map
# several contiguous ranges, possibly with holes of characters.
class EncodingTable4 < EncodingTable
attr_accessor :version
attr_reader :search_range, :entry_selector, :range_shift,
:end_count_array, :reserved_pad, :start_count_array,
:id_delta_array, :id_range_offset_array,
:glyph_index_array, :segments
def initialize(*args)
super(*args)
if exists_in_file?
# 2 * IO::SIZEOF_USHORT corresponds to format and len
# that we want to skip
@table.font.at_offset(@offset + 2 * IO::SIZEOF_USHORT) do
@version = @font.read_ushort
@seg_count_x2 = @font.read_ushort
@seg_count = @seg_count_x2 / 2
@search_range = @font.read_ushort
@entry_selector = @font.read_ushort
@range_shift = @font.read_ushort
@end_count_array = @font.read_ushorts(@seg_count)
@reserved_pad = @font.read_ushort
@start_count_array = @font.read_ushorts(@seg_count)
@id_delta_array = @font.read_ushorts(@seg_count)
@id_range_offset_array = @font.read_ushorts(@seg_count)
@nb_glyph_indices = len - 8 * IO::SIZEOF_USHORT \
- 4 * @seg_count * IO::SIZEOF_USHORT
@nb_glyph_indices /= IO::SIZEOF_USHORT;
if @nb_glyph_indices > 0
@glyph_index_array = \
@font.read_ushorts(@nb_glyph_indices)
else
@glyph_index_array = []
end
# Keep them in memory so we don't need to
# recalculate them every time
@segments = get_segments
end # end at_offset
else
@segments = []
@glyph_index_array = []
end
end # end initialize
def get_segments
segments = []
# For each segment...
@start_count_array.each_with_index do |start, curr_seg|
endd = @end_count_array[curr_seg]
delta = @id_delta_array[curr_seg]
range = @id_range_offset_array[curr_seg]
segments[curr_seg] = {}
start.upto(endd) do |curr_char|
if range == 0
index = (curr_char + delta)
else
gindex = range / 2 + (curr_char - start) - \
(@seg_count - curr_seg)
index = @glyph_index_array[gindex]
index = 0 if index.nil?
index += delta if index != 0
end
index = index % 65536
# charcode => glyph index
segments[curr_seg][curr_char] = index
end
end
segments
end
private :get_segments
# Returns a Hash. Its keys are characters codes and associated values
# are glyph indices.
def charmaps
hsh = {}
segments.each do |seg|
seg.each do |char_code, glyph_index|
hsh[char_code] = glyph_index
end
end
hsh
end
# Sets the charmaps. cmps is a Hash in the same fashion as the one
# returned by charmaps.
def charmaps=(cmps)
# TODO: we would need fewer segments if we ensured that
# glyphs with id in ascending order were associated
# with char codes in ascending order
raise "Charmaps is an empty array" if cmps.length == 0
# Order is important since we will rebuild segments
char_codes = cmps.keys.sort
@start_count_array = []
@end_count_array = []
@id_delta_array = []
curr_seg = 0
i = 0
@start_count_array[0] = char_codes.first
@id_delta_array[0] = cmps[char_codes.first] - char_codes.first
last_char = 0
char_codes.each do |char_code|
glyph_id = cmps[char_code]
curr_delta = glyph_id - char_code
if i > 0 and curr_delta != @id_delta_array.last
# Need to create a new segment
@end_count_array[curr_seg] = last_char
curr_seg += 1
@start_count_array[curr_seg] = char_code
@id_delta_array[curr_seg] = curr_delta
end
last_char = char_code
i += 1
end
seg_count = @start_count_array.length
@end_count_array[seg_count - 1] = last_char
@id_range_offset_array = [0] * seg_count # Range offsets not used
@segments = get_segments # Recalculate segments
# Values below are calculated
# to allow faster computation by font rasterizers
res = 1
power = 0
while res <= seg_count
res *= 2
power += 1
end
@search_range = res
@entry_selector = power - 1
@range_shift = 2 * seg_count - @search_range
end
# Returns index/id of glyph associated with unicode.
def get_glyph_id_for_unicode(unicode)
id = 0
@segments.length.times do |i|
if @start_count_array[i] <= unicode and \
unicode <= @end_count_array[i]
@segments[i].each do |char_code, glyph_id|
if char_code == unicode
id = glyph_id
break
end
end
end
end
id
end
# Returns the Font::TTF::Table::Glyf::SimpleGlyph or
# Font::TTF::Table::Glyf::CompositeGlyph associated with unicode.
def get_glyph_for_unicode(unicode)
id = get_glyph_id_for_unicode(unicode)
offs = @font.get_table(:loca).glyph_offsets[id]
@font.get_table(:glyf).get_glyph_at_offset(offs)
end
# Returns the unicode of glyph with index id.
def get_unicode_for_glyph_id(id)
# TODO: this method could be rewritten much more efficiently
# by using @start_count_array and @end_count_array
unicode = 0
charmaps.each do |char_code, glyph_index|
if glyph_index == id
unicode = char_code
break
end
end
unicode
end
# Dumps the table in binary raw format as may be found in a font
# file.
def dump
raw = (format || 0).to_ushort
seg_count = @segments.length || 0
len = 8 * IO::SIZEOF_USHORT + 4 * seg_count * IO::SIZEOF_USHORT + \
@glyph_index_array.length * IO::SIZEOF_USHORT
raw += len.to_ushort
raw += (@version || 0).to_ushort
raw += (seg_count * 2).to_ushort
raw += (@search_range || 0).to_ushort
raw += (@entry_selector || 0).to_ushort
raw += (@range_shift || 0).to_ushort
raw += (@end_count_array || []).to_ushorts
raw += (@reserved_pad || 0).to_ushort
raw += (@start_count_array || []).to_ushorts
raw += (@id_delta_array || []).to_ushorts
raw += (@id_range_offset_array || []).to_ushorts
raw += (@glyph_index_array || []).to_ushorts
end
end
# Encoding table in format 6. Not implemented.
class EncodingTable6 < EncodingTable
end
# Encoding table in format 8. Not implemented.
class EncodingTable8 < EncodingTable
end
# Encoding table in format 10. Not implemented.
class EncodingTable10 < EncodingTable
end
# Encoding table in format 12. Not implemented.
class EncodingTable12 < EncodingTable
end
attr_accessor :version
# An Array of encoding_tables. You may add or remove encoding tables
# from this array.
attr_accessor :encoding_tables
def initialize(*args)
super(*args)
if exists_in_file?
@font.at_offset(@offset) do
@version = @font.read_ushort
@encoding_table_num = @font.read_ushort
@encoding_tables = []
@encoding_table_num.times do
platform_id = @font.read_ushort
encoding_id = @font.read_ushort
offset = @font.read_ulong
@font.at_offset(@offset + offset) do
format = @font.read_ushort
len = @font.read_ushort
if [0, 2, 4, 6, 8, 10, 12].include? format
klass = eval("EncodingTable%d" % format)
else
klass = EncodingTable
end
@encoding_tables << klass.new(self,
offset,
len,
platform_id,
encoding_id)
end
end
end
else
@encoding_tables = []
end
end
# Returns a new empty EncodingTable0 object that you may add to the
# encoding_tables array.
def get_new_encoding_table0
EncodingTable0.new(self)
end
# Returns a new empty EncodingTable4 object that you may add to the
# encoding_tables array.
def get_new_encoding_table4
EncodingTable4.new(self)
end
# Dumps the cmap table in binary raw format as may be found in a font
# file.
def dump
raw = (@version || 0).to_ushort
raw += (@encoding_tables.length || 0).to_ushort
dumps = []
offs = 2 * IO::SIZEOF_USHORT + @encoding_tables.length * \
(2 * IO::SIZEOF_USHORT + IO::SIZEOF_ULONG)
@encoding_tables.each do |enc_tbl|
raw += (enc_tbl.platform_id || 3).to_ushort
raw += (enc_tbl.encoding_id || 1).to_ushort
dump = enc_tbl.dump
dumps << dump
raw += offs.to_ulong
offs += dump.length
end
dumps.each do |dump|
raw += dump
end
raw
end
end
end
end
end