# 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
require 'ttf/datatypes'
require 'ttf/exceptions'
require 'ttf/fontchunk'
require 'ttf/encodings'
require 'ttf/table/cmap'
require 'ttf/table/cvt'
require 'ttf/table/fpgm'
require 'ttf/table/gasp'
require 'ttf/table/glyf'
require 'ttf/table/head'
require 'ttf/table/hhea'
require 'ttf/table/hmtx'
require 'ttf/table/kern'
require 'ttf/table/loca'
require 'ttf/table/maxp'
require 'ttf/table/name'
require 'ttf/table/os2'
require 'ttf/table/post'
require 'ttf/table/prep'
require 'ttf/table/vhea'
require 'ttf/table/vmtx'
module Font
module TTF
# TTF/Ruby is a library to read and write TrueType fonts in Ruby.
#
# Author:: Mathieu Blondel
# Copyright:: Copyright (c) 2006 Mathieu Blondel
# License:: GPL
#
# This Font::TTF::File class is TTF/Ruby's main class and is a subclass
# of Ruby's File class. Here is some sample code:
#
# require 'ttf'
#
# font = Font::TTF::File.new("copy.ttf")
#
# cmap_tbl = font.get_table(:cmap)
# enc_tbl4 = cmap_tbl.encoding_tables.find { |t| t.format == 4 }
# m_unicode = "m".unpack("U")[0]
# glyph_id = enc_tbl4.charmaps[m_unicode]
#
# loca_tbl = font.get_table(:loca)
# glyph_offset = loca_tbl.glyph_offsets[glyph_id]
#
# glyf_tbl = font.get_table(:glyf)
# glyph = glyf_tbl.get_glyph_at_offset(glyph_offset)
# unless glyph.composite?
# glyph.abs_coordinates.each { |x, y| puts x, y }
# end
#
# Here are the tables and their associated Symbol:
#
# * :cmap => Font::TTF::Table::Cmap
# * :cvt => Font::TTF::Table::Cvt
# * :fpgm => Font::TTF::Table::Fpgm
# * :gasp => Font::TTF::Table::Gasp
# * :glyf => Font::TTF::Table::Glyf
# * :head => Font::TTF::Table::Head
# * :hhea => Font::TTF::Table::Hhea
# * :hmtx => Font::TTF::Table::Hmtx
# * :kern => Font::TTF::Table::Kern
# * :loca => Font::TTF::Table::Loca
# * :maxp => Font::TTF::Table::Maxp
# * :name => Font::TTF::Table::Name
# * :"OS/2" => Font::TTF::Table::OS2
# * :post => Font::TTF::Table::Post
# * :prep => Font::TTF::Table::Prep
# * :vhea => Font::TTF::Table::Vhea
# * :vmtx => Font::TTF::Table::Vmtx
#
# Of course, you may modify attributes and generate a new font file.
#
# require "ttf"
#
# font = Font::TTF::File.new("file.ttf", "w")
# name_tbl = font.get_table(:name)
# nr = name_tbl.name_records[0]
# nr.utf8_str = "blablabla"
# font.write(font.dump)
#
class File < File
TABLES = {:cmap => Font::TTF::Table::Cmap,
:cvt => Font::TTF::Table::Cvt,
:fpgm => Font::TTF::Table::Fpgm,
:gasp => Font::TTF::Table::Gasp,
:glyf => Font::TTF::Table::Glyf,
:head => Font::TTF::Table::Head,
:hhea => Font::TTF::Table::Hhea,
:hmtx => Font::TTF::Table::Hmtx,
:kern => Font::TTF::Table::Kern,
:loca => Font::TTF::Table::Loca,
:maxp => Font::TTF::Table::Maxp,
:name => Font::TTF::Table::Name,
:"OS/2" => Font::TTF::Table::OS2,
:post => Font::TTF::Table::Post,
:prep => Font::TTF::Table::Prep,
:vhea => Font::TTF::Table::Vhea,
:vmtx => Font::TTF::Table::Vmtx}
DIR_ENTRY_SIZE = 4 * IO::SIZEOF_ULONG
attr_reader :filename, :version, :search_range,
:entry_selector, :range_shift, :table_list, :tables_infos
attr_writer :version, :search_range, :entry_selector, :range_shift
# Font::TTF::File being a subclass of Ruby's File class, you may
# create new objects with the same parameters as Ruby's File class.
#
# But you may also create new objects without parameters in case you want
# to create a font from scratch and you don't need to write it to a file.
def initialize(*args)
if args.length == 0
@filename = nil
@version = 0x00010000 # 1.0
else
super(*args)
@filename = args[0]
end
@table_list = [] # ordered list
@tables = {} # Tables are kept in this arr so we don't
# have to create new objects everytime
@tables_infos = {} # infos about tables present in file
if not @filename.nil? and FileTest.exists? @filename
begin
at_offset(0) do
@version = read_fixed
table_num = read_ushort
@search_range = read_ushort
@entry_selector = read_ushort
@range_shift = read_ushort
table_num.times do
tag = read_ulong_as_text.strip.to_sym
@table_list << tag
@tables_infos[tag] = {:checksum => read_ulong,
:offset => read_ulong,
:len => read_ulong}
end
end
rescue
raise MalformedFont
end
end
end
# Returns table associated with tag tbl_tag. It may return one of
# Font::TTF::Table::* object or a Font::TTF::FontChunk object if the
# table is not implemented yet by ttf-ruby.
#
# The table returned is kept internally so that every future call to
# get_table with the same tbl_tag will return the same object.
def get_table(tbl_tag)
tbli = @tables_infos[tbl_tag]
if @tables.include? tbl_tag
@tables[tbl_tag]
elsif tbli.nil?
raise TableMissing, "Table #{tbl_tag.to_s} neither exists " + \
"in file nor was defined by user!"
elsif TABLES.include? tbl_tag
@tables[tbl_tag] = \
TABLES[tbl_tag].new(self, tbli[:offset], tbli[:len])
else
Font::TTF::FontChunk.new(self, tbli[:offset], tbli[:len])
end
end
# Returns whether table with tag tbl_tag is already in font or not.
def tables_include?(tbl_tag)
@table_list.include? tbl_tag
end
# Gets a new empty table that then may be set with set_table.
# tbl_tag is a Symbol.
def get_new_table(tbl_tag)
if TABLES.include? tbl_tag
TABLES[tbl_tag].new(self)
else
Font::TTF::FontChunk.new(self)
end
end
# Updates some member variables that change everytime a table is added
# to the font.
def table_list_update!
@table_list.sort!
res = 1
power = 0
while res <= @table_list.length
res *= 2
power += 1
end
@search_range = res * 16
@entry_selector = power - 1
@range_shift = @table_list.length * 16 - @search_range
end
private :table_list_update!
# Adds tbl to font. tbl is a table object
# (e.g. an instance of Font::TTF::Table::Loca).
def set_table(tbl)
tbl_tag = tbl.tag
unless tables_include? tbl_tag
@table_list << tbl_tag
table_list_update!
end
@tables[tbl_tag] = tbl
end
# Removes tbl from font. tbl may be a Symbol (e.g. :loca for the loca table)
# or a table object (e.g. an instance of Font::TTF::Table::Loca).
def unset_table(tbl)
if tbl.kind_of? Symbol
tbl_tag = tbl
else
tbl_tag = tbl.tag
end
@table_list.delete(tbl_tag)
@tables.delete(tbl_tag)
table_list_update!
end
# Dumps the whole font in binary raw format as found in a font file.
def dump
raw = ""
raw += (@version || 0).to_fixed
raw += (@table_list || []).length.to_ushort
raw += (@search_range || 0).to_ushort
raw += (@entry_selector || 0).to_ushort
raw += (@range_shift || 0).to_ushort
offs = IO::SIZEOF_FIXED + 4 * IO::SIZEOF_USHORT + \
@table_list.length * DIR_ENTRY_SIZE
dumps = []
@table_list.each do |tbl_tag|
tbl = get_table(tbl_tag)
tag = tbl_tag.to_s
tag.four_chars!
raw += tag
# FIXME: FontChunk#checksum method is buggy
# For now, I set it to 0
raw += 0.to_ulong
raw += offs.to_ulong
dump = tbl.dump
dumps << dump
len = dump.length
raw += len.to_ulong
offs += len
end
dumps.each do |dump|
raw += dump
end
raw
end
end
end
end