Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,291 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Charset
class Charset < TTFunk::SubTable
include Enumerable
# First glyph string. This is an implicit glyph present in all charsets.
FIRST_GLYPH_STRING = '.notdef'
# Format 0.
ARRAY_FORMAT = 0
# Format 1.
RANGE_FORMAT_8 = 1
# Format 2.
RANGE_FORMAT_16 = 2
# Predefined ISOAdobe charset ID.
ISO_ADOBE_CHARSET_ID = 0
# Predefined Expert charset ID.
EXPERT_CHARSET_ID = 1
# Predefined Expert Subset charset ID.
EXPERT_SUBSET_CHARSET_ID = 2
# Default charset ID.
DEFAULT_CHARSET_ID = ISO_ADOBE_CHARSET_ID
class << self
# Standard strings defined in the spec that do not need to be defined
# in the CFF.
#
# @return [TTFunk::OneBasedArray<String>]
def standard_strings
Charsets::STANDARD_STRINGS
end
# Strings for charset ID.
#
# @param charset_id [Integer]
# @return [TTFunk::OneBasedArray<String>]
def strings_for_charset_id(charset_id)
case charset_id
when ISO_ADOBE_CHARSET_ID
Charsets::ISO_ADOBE
when EXPERT_CHARSET_ID
Charsets::EXPERT
when EXPERT_SUBSET_CHARSET_ID
Charsets::EXPERT_SUBSET
end
end
end
# Encoded entries.
# @return [TTFunk::OneBasedArray<Integer>, Array<Range<Integer>>]
attr_reader :entries
# Length of encoded charset subtable.
# @return [Integer]
attr_reader :length
# Top dict.
# @return [TTFunk::Table::Cff::TopDict]
attr_reader :top_dict
# Encodign format.
# @return [Integer]
attr_reader :format
# Number of encoded items.
# @return [Integer]
attr_reader :items_count
# Offset or charset ID.
# @return [Integer]
attr_reader :offset_or_id
# @overload initialize(top_dict, file, offset = nil, length = nil)
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param offset [Integer]
# @param length [Integer]
# @overload initialize(top_dict, file, charset_id)
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param charset_id [Integer] 0, 1, or 2
def initialize(top_dict, file, offset_or_id = nil, length = nil)
@top_dict = top_dict
@offset_or_id = offset_or_id || DEFAULT_CHARSET_ID
if offset
super(file, offset, length)
else
@items_count = self.class.strings_for_charset_id(offset_or_id).size
end
end
# Iterate over character names.
#
# @overload each()
# @yieldparam name [String]
# @return [void]
# @overload each()
# @return [Enumerator]
def each
return to_enum(__method__) unless block_given?
# +1 adjusts for the implicit .notdef glyph
(items_count + 1).times { |i| yield(self[i]) }
end
# Get character name for glyph index.
#
# @param glyph_id [Integer]
# @return [String, nil]
def [](glyph_id)
return FIRST_GLYPH_STRING if glyph_id.zero?
find_string(sid_for(glyph_id))
end
# Charset offset in the file.
#
# @return [Integer, nil]
def offset
# Numbers from 0..2 mean charset IDs instead of offsets. IDs are
# basically pre-defined sets of characters.
#
# In the case of an offset, add the CFF table's offset since the
# charset offset is relative to the start of the CFF table. Otherwise
# return nil (no offset).
if offset_or_id > 2
offset_or_id + top_dict.cff_offset
end
end
# Encode charset.
#
# @param charmap [Hash{Integer => Hash}] keys are the charac codes,
# values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# @return [String]
def encode(charmap)
# no offset means no charset was specified (i.e. we're supposed to
# use a predefined charset) so there's nothing to encode
return '' unless offset
sids =
charmap
.values
.reject { |mapping| mapping[:new].zero? }
.sort_by { |mapping| mapping[:new] }
.map { |mapping| sid_for(mapping[:old]) }
ranges = TTFunk::BinUtils.rangify(sids)
range_max = ranges.map(&:last).max
range_bytes =
if range_max.positive?
(Math.log2(range_max) / 8).floor + 1
else
# for cases when there are no sequences at all
Float::INFINITY
end
# calculate whether storing the charset as a series of ranges is
# more efficient (i.e. takes up less space) vs storing it as an
# array of SID values
total_range_size = (2 * ranges.size) + (range_bytes * ranges.size)
total_array_size = sids.size * element_width(:array_format)
if total_array_size <= total_range_size
([format_int(:array_format)] + sids).pack('Cn*')
else
fmt = range_bytes == 1 ? :range_format8 : :range_format16
element_fmt = element_format(fmt)
result = [format_int(fmt)].pack('C')
ranges.each { |range| result << range.pack(element_fmt) }
result
end
end
private
def sid_for(glyph_id)
return 0 if glyph_id.zero?
# rather than validating the glyph as part of one of the predefined
# charsets, just pass it through
return glyph_id unless offset
case format_sym
when :array_format
entries[glyph_id]
when :range_format8, :range_format16
entries.reduce(glyph_id) do |remaining, range|
if range.size >= remaining
break (range.first + remaining) - 1
end
remaining - range.size
end
end
end
def find_string(sid)
if offset
return self.class.standard_strings[sid] if sid <= 390
idx = sid - 390
if idx < file.cff.string_index.items_count
file.cff.string_index[idx]
end
else
self.class.strings_for_charset_id(offset_or_id)[sid]
end
end
def parse!
return unless offset
@format = read(1, 'C').first
case format_sym
when :array_format
@items_count = top_dict.charstrings_index.items_count - 1
@length = @items_count * element_width
@entries = OneBasedArray.new(read(length, 'n*'))
when :range_format8, :range_format16
# The number of ranges is not explicitly specified in the font.
# Instead, software utilizing this data simply processes ranges
# until all glyphs in the font are covered.
@items_count = 0
@entries = []
@length = 0
until @items_count >= top_dict.charstrings_index.items_count - 1
@length += 1 + element_width
sid, num_left = read(element_width, element_format)
@entries << (sid..(sid + num_left))
@items_count += num_left + 1
end
end
end
def element_width(fmt = format_sym)
{
array_format: 2, # SID
range_format8: 3, # SID + Card8
range_format16: 4, # SID + Card16
}[fmt]
end
def element_format(fmt = format_sym)
{
array_format: 'n',
range_format8: 'nC',
range_format16: 'nn',
}[fmt]
end
def format_sym
case @format
when ARRAY_FORMAT then :array_format
when RANGE_FORMAT_8 then :range_format8
when RANGE_FORMAT_16 then :range_format16
else
raise Error, "unsupported charset format '#{fmt}'"
end
end
def format_int(sym = format_sym)
{
array_format: ARRAY_FORMAT,
range_format8: RANGE_FORMAT_8,
range_format16: RANGE_FORMAT_16,
}[sym]
end
end
end
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# Predefined CFF data
module Charsets
autoload :EXPERT, 'ttfunk/table/cff/charsets/expert'
autoload :EXPERT_SUBSET, 'ttfunk/table/cff/charsets/expert_subset'
autoload :ISO_ADOBE, 'ttfunk/table/cff/charsets/iso_adobe'
autoload :STANDARD_STRINGS, 'ttfunk/table/cff/charsets/standard_strings'
end
end
end
end
@@ -0,0 +1,190 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
module Charsets
# Predefinde CFF Expert charset
EXPERT = OneBasedArray.new(
[
'space',
*[nil] * 11,
'comma',
'hyphen',
'period',
*[nil] * 11,
'colon',
'semicolon',
*[nil] * 70,
'fraction',
*[nil] * 9,
'fi',
'fl',
*[nil] * 39,
'onesuperior',
*[nil] * 4,
'onehalf',
nil,
nil,
'onequarter',
*[nil] * 4,
'threequarters',
'twosuperior',
*[nil] * 4,
'threesuperior',
*[nil] * 59,
'exclamsmall',
'Hungarumlautsmall',
'dollaroldstyle',
'dollarsuperior',
'ampersandsmall',
'Acutesmall',
'parenleftsuperior',
'parenrightsuperior',
'twodotenleader',
'onedotenleader',
'zerooldstyle',
'oneoldstyle',
'twooldstyle',
'threeoldstyle',
'fouroldstyle',
'fiveoldstyle',
'sixoldstyle',
'sevenoldstyle',
'eightoldstyle',
'nineoldstyle',
'commasuperior',
'threequartersemdash',
'periodsuperior',
'questionsmall',
'asuperior',
'bsuperior',
'centsuperior',
'dsuperior',
'esuperior',
'isuperior',
'lsuperior',
'msuperior',
'nsuperior',
'osuperior',
'rsuperior',
'ssuperior',
'tsuperior',
'ff',
'ffi',
'ffl',
'parenleftinferior',
'parenrightinferior',
'Circumflexsmall',
'hyphensuperior',
'Gravesmall',
'Asmall',
'Bsmall',
'Csmall',
'Dsmall',
'Esmall',
'Fsmall',
'Gsmall',
'Hsmall',
'Ismall',
'Jsmall',
'Ksmall',
'Lsmall',
'Msmall',
'Nsmall',
'Osmall',
'Psmall',
'Qsmall',
'Rsmall',
'Ssmall',
'Tsmall',
'Usmall',
'Vsmall',
'Wsmall',
'Xsmall',
'Ysmall',
'Zsmall',
'colonmonetary',
'onefitted',
'rupiah',
'Tildesmall',
'exclamdownsmall',
'centoldstyle',
'Lslashsmall',
'Scaronsmall',
'Zcaronsmall',
'Dieresissmall',
'Brevesmall',
'Caronsmall',
'Dotaccentsmall',
'Macronsmall',
'figuredash',
'hypheninferior',
'Ogoneksmall',
'Ringsmall',
'Cedillasmall',
'questiondownsmall',
'oneeighth',
'threeeighths',
'fiveeighths',
'seveneighths',
'onethird',
'twothirds',
'zerosuperior',
'foursuperior',
'fivesuperior',
'sixsuperior',
'sevensuperior',
'eightsuperior',
'ninesuperior',
'zeroinferior',
'oneinferior',
'twoinferior',
'threeinferior',
'fourinferior',
'fiveinferior',
'sixinferior',
'seveninferior',
'eightinferior',
'nineinferior',
'centinferior',
'dollarinferior',
'periodinferior',
'commainferior',
'Agravesmall',
'Aacutesmall',
'Acircumflexsmall',
'Atildesmall',
'Adieresissmall',
'Aringsmall',
'AEsmall',
'Ccedillasmall',
'Egravesmall',
'Eacutesmall',
'Ecircumflexsmall',
'Edieresissmall',
'Igravesmall',
'Iacutesmall',
'Icircumflexsmall',
'Idieresissmall',
'Ethsmall',
'Ntildesmall',
'Ogravesmall',
'Oacutesmall',
'Ocircumflexsmall',
'Otildesmall',
'Odieresissmall',
'OEsmall',
'Oslashsmall',
'Ugravesmall',
'Uacutesmall',
'Ucircumflexsmall',
'Udieresissmall',
'Yacutesmall',
'Thornsmall',
],
).freeze
end
end
end
end
@@ -0,0 +1,120 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
module Charsets
# Predefined CFF Expert Subset charset
EXPERT_SUBSET = OneBasedArray.new(
[
'space',
*[nil] * 11,
'comma',
'hyphen',
'period',
*[nil] * 11,
'colon',
'semicolon',
*[nil] * 70,
'fraction',
*[nil] * 9,
'fi',
'fl',
*[nil] * 39,
'onesuperior',
*[nil] * 4,
'onehalf',
nil,
nil,
'onequarter',
*[nil] * 4,
'threequarters',
'twosuperior',
*[nil] * 4,
'threesuperior',
*[nil] * 61,
'dollaroldstyle',
'dollarsuperior',
nil,
nil,
'parenleftsuperior',
'parenrightsuperior',
'twodotenleader',
'onedotenleader',
'zerooldstyle',
'oneoldstyle',
'twooldstyle',
'threeoldstyle',
'fouroldstyle',
'fiveoldstyle',
'sixoldstyle',
'sevenoldstyle',
'eightoldstyle',
'nineoldstyle',
'commasuperior',
'threequartersemdash',
'periodsuperior',
nil,
'asuperior',
'bsuperior',
'centsuperior',
'dsuperior',
'esuperior',
'isuperior',
'lsuperior',
'msuperior',
'nsuperior',
'osuperior',
'rsuperior',
'ssuperior',
'tsuperior',
'ff',
'ffi',
'ffl',
'parenleftinferior',
'parenrightinferior',
nil,
'hyphensuperior',
*[nil] * 27,
'colonmonetary',
'onefitted',
'rupiah',
nil,
nil,
'centoldstyle',
*[nil] * 8,
'figuredash',
'hypheninferior',
*[nil] * 4,
'oneeighth',
'threeeighths',
'fiveeighths',
'seveneighths',
'onethird',
'twothirds',
'zerosuperior',
'foursuperior',
'fivesuperior',
'sixsuperior',
'sevensuperior',
'eightsuperior',
'ninesuperior',
'zeroinferior',
'oneinferior',
'twoinferior',
'threeinferior',
'fourinferior',
'fiveinferior',
'sixinferior',
'seveninferior',
'eightinferior',
'nineinferior',
'centinferior',
'dollarinferior',
'periodinferior',
],
).freeze
end
end
end
end
@@ -0,0 +1,242 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
module Charsets
# Predefined CFF ISOAdobe charset
ISO_ADOBE = OneBasedArray.new(
[
'space',
'exclam',
'quotedbl',
'numbersign',
'dollar',
'percent',
'ampersand',
'quoteright',
'parenleft',
'parenright',
'asterisk',
'plus',
'comma',
'hyphen',
'period',
'slash',
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'colon',
'semicolon',
'less',
'equal',
'greater',
'question',
'at',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'bracketleft',
'backslash',
'bracketright',
'asciicircum',
'underscore',
'quoteleft',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'braceleft',
'bar',
'braceright',
'asciitilde',
'exclamdown 97 cent',
'sterling',
'fraction',
'yen',
'florin',
'section',
'currency',
'quotesingle',
'quotedblleft',
'guillemotleft',
'guilsinglleft',
'guilsinglright',
'fi',
'fl',
'endash',
'dagger',
'daggerdbl',
'periodcentered',
'paragraph',
'bullet',
'quotesinglbase',
'quotedblbase',
'quotedblright',
'guillemotright',
'ellipsis',
'perthousand',
'questiondown',
'grave',
'acute',
'circumflex',
'tilde',
'macron',
'breve',
'dotaccent',
'dieresis',
'ring',
'cedilla',
'hungarumlaut',
'ogonek',
'caron',
'emdash',
'AE',
'ordfeminine',
'Lslash',
'Oslash',
'OE',
'ordmasculine',
'ae',
'dotlessi',
'lslash',
'oslash',
'oe',
'germandbls',
'onesuperior',
'logicalnot',
'mu',
'trademark',
'Eth',
'onehalf',
'plusminus',
'Thorn',
'onequarter',
'divide',
'brokenbar',
'degree',
'thorn',
'threequarters',
'twosuperior',
'registered',
'minus',
'eth',
'multiply',
'threesuperior',
'copyright',
'Aacute',
'Acircumflex',
'Adieresis',
'Agrave',
'Aring',
'Atilde',
'Ccedilla',
'Eacute',
'Ecircumflex',
'Edieresis',
'Egrave',
'Iacute',
'Icircumflex',
'Idieresis',
'Igrave',
'Ntilde',
'Oacute',
'Ocircumflex',
'Odieresis',
'Ograve',
'Otilde',
'Scaron',
'Uacute',
'Ucircumflex',
'Udieresis',
'Ugrave',
'Yacute',
'Ydieresis',
'Zcaron',
'aacute',
'acircumflex',
'adieresis',
'agrave',
'aring',
'atilde',
'ccedilla',
'eacute',
'ecircumflex',
'edieresis',
'egrave',
'iacute',
'icircumflex',
'idieresis',
'igrave',
'ntilde',
'oacute',
'ocircumflex',
'odieresis',
'ograve',
'otilde',
'scaron',
'uacute',
'ucircumflex',
'udieresis',
'ugrave',
'yacute',
'ydieresis',
'zcaron',
],
).freeze
end
end
end
end
@@ -0,0 +1,405 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
module Charsets
# Standard CFF strings
STANDARD_STRINGS = OneBasedArray.new(
[
'space',
'exclam',
'quotedbl',
'numbersign',
'dollar',
'percent',
'ampersand',
'quoteright',
'parenleft',
'parenright',
'asterisk',
'plus',
'comma',
'hyphen',
'period',
'slash',
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'colon',
'semicolon',
'less',
'equal',
'greater',
'question',
'at',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'bracketleft',
'backslash',
'bracketright',
'asciicircum',
'underscore',
'quoteleft',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'braceleft',
'bar',
'braceright',
'asciitilde',
'exclamdown',
'cent',
'sterling',
'fraction',
'yen',
'florin',
'section',
'currency',
'quotesingle',
'quotedblleft',
'guillemotleft',
'guilsinglleft',
'guilsinglright',
'fi',
'fl',
'endash',
'dagger',
'daggerdbl',
'periodcentered',
'paragraph',
'bullet',
'quotesinglbase',
'quotedblbase',
'quotedblright',
'guillemotright',
'ellipsis',
'perthousand',
'questiondown',
'grave',
'acute',
'circumflex',
'tilde',
'macron',
'breve',
'dotaccent',
'dieresis',
'ring',
'cedilla',
'hungarumlaut',
'ogonek',
'caron',
'emdash',
'AE',
'ordfeminine',
'Lslash',
'Oslash',
'OE',
'ordmasculine',
'ae',
'dotlessi',
'lslash',
'oslash',
'oe',
'germandbls',
'onesuperior',
'logicalnot',
'mu',
'trademark',
'Eth',
'onehalf',
'plusminus',
'Thorn',
'onequarter',
'divide',
'brokenbar',
'degree',
'thorn',
'threequarters',
'twosuperior',
'registered',
'minus',
'eth',
'multiply',
'threesuperior',
'copyright',
'Aacute',
'Acircumflex',
'Adieresis',
'Agrave',
'Aring',
'Atilde',
'Ccedilla',
'Eacute',
'Ecircumflex',
'Edieresis',
'Egrave',
'Iacute',
'Icircumflex',
'Idieresis',
'Igrave',
'Ntilde',
'Oacute',
'Ocircumflex',
'Odieresis',
'Ograve',
'Otilde',
'Scaron',
'Uacute',
'Ucircumflex',
'Udieresis',
'Ugrave',
'Yacute',
'Ydieresis',
'Zcaron',
'aacute',
'acircumflex',
'adieresis',
'agrave',
'aring',
'atilde',
'ccedilla',
'eacute',
'ecircumflex',
'edieresis',
'egrave',
'iacute',
'icircumflex',
'idieresis',
'igrave',
'ntilde',
'oacute',
'ocircumflex',
'odieresis',
'ograve',
'otilde',
'scaron',
'uacute',
'ucircumflex',
'udieresis',
'ugrave',
'yacute',
'ydieresis',
'zcaron',
'exclamsmall',
'Hungarumlautsmall',
'dollaroldstyle',
'dollarsuperior',
'ampersandsmall',
'Acutesmall',
'parenleftsuperior',
'parenrightsuperior',
'266 ff',
'onedotenleader',
'zerooldstyle',
'oneoldstyle',
'twooldstyle',
'threeoldstyle',
'fouroldstyle',
'fiveoldstyle',
'sixoldstyle',
'sevenoldstyle',
'eightoldstyle',
'nineoldstyle',
'commasuperior',
'threequartersemdash',
'periodsuperior',
'questionsmall',
'asuperior',
'bsuperior',
'centsuperior',
'dsuperior',
'esuperior',
'isuperior',
'lsuperior',
'msuperior',
'nsuperior',
'osuperior',
'rsuperior',
'ssuperior',
'tsuperior',
'ff',
'ffi',
'ffl',
'parenleftinferior',
'parenrightinferior',
'Circumflexsmall',
'hyphensuperior',
'Gravesmall',
'Asmall',
'Bsmall',
'Csmall',
'Dsmall',
'Esmall',
'Fsmall',
'Gsmall',
'Hsmall',
'Ismall',
'Jsmall',
'Ksmall',
'Lsmall',
'Msmall',
'Nsmall',
'Osmall',
'Psmall',
'Qsmall',
'Rsmall',
'Ssmall',
'Tsmall',
'Usmall',
'Vsmall',
'Wsmall',
'Xsmall',
'Ysmall',
'Zsmall',
'colonmonetary',
'onefitted',
'rupiah',
'Tildesmall',
'exclamdownsmall',
'centoldstyle',
'Lslashsmall',
'Scaronsmall',
'Zcaronsmall',
'Dieresissmall',
'Brevesmall',
'Caronsmall',
'Dotaccentsmall',
'Macronsmall',
'figuredash',
'hypheninferior',
'Ogoneksmall',
'Ringsmall',
'Cedillasmall',
'questiondownsmall',
'oneeighth',
'threeeighths',
'fiveeighths',
'seveneighths',
'onethird',
'twothirds',
'zerosuperior',
'foursuperior',
'fivesuperior',
'sixsuperior',
'sevensuperior',
'eightsuperior',
'ninesuperior',
'zeroinferior',
'oneinferior',
'twoinferior',
'threeinferior',
'fourinferior',
'fiveinferior',
'sixinferior',
'seveninferior',
'eightinferior',
'nineinferior',
'centinferior',
'dollarinferior',
'periodinferior',
'commainferior',
'Agravesmall',
'Aacutesmall',
'Acircumflexsmall',
'Atildesmall',
'Adieresissmall',
'Aringsmall',
'AEsmall',
'Ccedillasmall',
'Egravesmall',
'Eacutesmall',
'Ecircumflexsmall',
'Edieresissmall',
'Igravesmall',
'Iacutesmall',
'Icircumflexsmall',
'Idieresissmall',
'Ethsmall',
'Ntildesmall',
'Ogravesmall',
'Oacutesmall',
'Ocircumflexsmall',
'Otildesmall',
'Odieresissmall',
'OEsmall',
'Oslashsmall',
'Ugravesmall',
'Uacutesmall',
'Ucircumflexsmall',
'Udieresissmall',
'Yacutesmall',
'Thornsmall',
'Ydieresissmall',
'001.000',
'001.001',
'001.002',
'001.003',
'Black',
'Bold',
'Book',
'Light',
'Medium',
'Regular',
'Roman',
'Semibold',
],
).freeze
end
end
end
end
@@ -0,0 +1,507 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Charstring.
class Charstring
# Type 2 charstring operators
CODE_MAP = {
1 => :hstem,
3 => :vstem,
4 => :vmoveto,
5 => :rlineto,
6 => :hlineto,
7 => :vlineto,
8 => :rrcurveto,
10 => :callsubr,
12 => :flex_select,
14 => :endchar,
18 => :hstemhm,
19 => :hintmask,
20 => :cntrmask,
21 => :rmoveto,
22 => :hmoveto,
23 => :vstemhm,
24 => :rcurveline,
25 => :rlinecurve,
26 => :vvcurveto,
27 => :hhcurveto,
28 => :shortint,
29 => :callgsubr,
30 => :vhcurveto,
31 => :hvcurveto,
}.freeze
# Type 2 Flex operators.
FLEX_CODE_MAP = {
35 => :flex,
34 => :hflex,
36 => :hflex1,
37 => :flex1,
}.freeze
# Glyph ID.
# @return [Integer]
attr_reader :glyph_id
# Encoded charstring.
# @return [String]
attr_reader :raw
# @param glyph_id [Integer]
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param font_dict [TTFunk::Table:Cff::FontDict]
# @param raw [String]
def initialize(glyph_id, top_dict, font_dict, raw)
@glyph_id = glyph_id
@top_dict = top_dict
@font_dict = font_dict
@raw = raw
default_width_x = (@font_dict || @top_dict).private_dict.default_width_x
@nominal_width_x = (@font_dict || @top_dict).private_dict.nominal_width_x
@subrs = (@font_dict || @top_dict).private_dict.subr_index
@gsubrs = @top_dict.cff.global_subr_index
@subrs_bias = @subrs.bias if @subrs
@gsubrs_bias = @gsubrs.bias if @gsubrs
@stack = []
@data = raw.bytes
@index = 0
@n_stems = 0
@have_width = false
@open = false
@width = default_width_x
@x = 0
@y = 0
end
# Get path representation of this charstring.
#
# @return [TTFunk::Table::Cff::Path]
def path
@path || begin
@path = Path.new
parse!
@path
end
end
# Get a TrueType-compatible glyph representation of this charstring.
#
# @return [TTFunk::Table::Glyf::PathBased]
def glyph
@glyph ||=
begin
horizontal_metrics = @top_dict.file.horizontal_metrics.for(glyph_id)
Glyf::PathBased.new(path, horizontal_metrics)
end
end
# Get path representation of this charstring at the specified font size.
#
# @param x [Integer, Float] new horizontal position.
# @param y [Integer, Float] new vertical position.
# @param font_size [Integer, Float] font size.
# @return [TTFunk::Table::Cff::Path]
def render(x: 0, y: 0, font_size: 72)
path.render(
x: x,
y: y,
font_size: font_size,
units_per_em: @top_dict.file.header.units_per_em,
)
end
private
def parse!
until @index >= @data.size
code = @data[@index]
@index += 1
# return from callgsubr - do nothing since we inline subrs
next if code == 11
if code >= 32 && code <= 246
@stack << (code - 139)
elsif (m = CODE_MAP[code])
__send__(m)
elsif code >= 247 && code <= 250
b0 = code
b1 = @data[@index]
@index += 1
@stack << (((b0 - 247) * 256) + b1 + 108)
elsif code >= 251 && code <= 254
b0 = code
b1 = @data[@index]
@index += 1
@stack << ((-(b0 - 251) * 256) - b1 - 108)
else
b1, b2, b3, b4 = read_bytes(4)
@stack << (((b1 << 24) | (b2 << 16) | (b3 << 8) | b4) / 65_536)
end
end
end
def read_bytes(length)
bytes = @data[@index, length]
@index += length
bytes
end
def hstem
stem
end
def vstem
stem
end
# rubocop:disable Style/EvenOdd
def stem
# The number of stem operators on the stack is always even.
# If the value is uneven, that means a width is specified.
has_width_arg = @stack.size % 2 == 1
if has_width_arg && !@have_width
@width = @stack.shift + @nominal_width_x
end
@n_stems += @stack.length >> 1
@stack.clear
@have_width = true
end
# rubocop:enable Style/EvenOdd
def vmoveto
if @stack.size > 1 && !@have_width
@width = @stack.shift + @nominal_width_x
@have_width = true
end
@y += @stack.pop
add_contour(@x, @y)
end
def add_contour(x, y)
if @open
@path.close_path
end
@path.move_to(x, y)
@open = true
end
def rlineto
until @stack.empty?
@x += @stack.shift
@y += @stack.shift
@path.line_to(@x, @y)
end
end
def hlineto
until @stack.empty?
@x += @stack.shift
@path.line_to(@x, @y)
break if @stack.empty?
@y += @stack.shift
@path.line_to(@x, @y)
end
end
def vlineto
until @stack.empty?
@y += @stack.shift
@path.line_to(@x, @y)
break if @stack.empty?
@x += @stack.shift
@path.line_to(@x, @y)
end
end
def rrcurveto
until @stack.empty?
c1x = @x + @stack.shift
c1y = @y + @stack.shift
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x + @stack.shift
@y = c2y + @stack.shift
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
end
def callsubr
code_index = @stack.pop + @subrs_bias
subr_code = @subrs[code_index].bytes
@data[@index, 0] = subr_code
end
def flex_select
flex_code = @data[@index]
@index += 1
__send__(FLEX_CODE_MAP[flex_code])
end
def flex
c1x = @x + @stack.shift # dx1
c1y = @y + @stack.shift # dy1
c2x = c1x + @stack.shift # dx2
c2y = c1y + @stack.shift # dy2
jpx = c2x + @stack.shift # dx3
jpy = c2y + @stack.shift # dy3
c3x = jpx + @stack.shift # dx4
c3y = jpy + @stack.shift # dy4
c4x = c3x + @stack.shift # dx5
c4y = c3y + @stack.shift # dy5
@x = c4x + @stack.shift # dx6
@y = c4y + @stack.shift # dy6
@stack.shift # flex depth
@path.curve_to(c1x, c1y, c2x, c2y, jpx, jpy)
@path.curve_to(c3x, c3y, c4x, c4y, @x, @y)
end
def hflex
c1x = @x + @stack.shift # dx1
c1y = @y # dy1
c2x = c1x + @stack.shift # dx2
c2y = c1y + @stack.shift # dy2
jpx = c2x + @stack.shift # dx3
jpy = c2y # dy3
c3x = jpx + stack.shift # dx4
c3y = c2y # dy4
c4x = c3x + stack.shift # dx5
c4y = @y # dy5
@x = c4x + stack.shift # dx6
@path.curve_to(c1x, c1y, c2x, c2y, jpx, jpy)
@path.curve_to(c3x, c3y, c4x, c4y, @x, @y)
end
def hflex1
c1x = @x + @stack.shift # dx1
c1y = @y + @stack.shift # dy1
c2x = c1x + @stack.shift # dx2
c2y = c1y + @stack.shift # dy2
jpx = c2x + @stack.shift # dx3
jpy = c2y # dy3
c3x = jpx + @stack.shift # dx4
c3y = c2y # dy4
c4x = c3x + @stack.shift # dx5
c4y = c3y + @stack.shift # dy5
@x = c4x + @stack.shift # dx6
@path.curve_to(c1x, c1y, c2x, c2y, jpx, jpy)
@path.curve_to(c3x, c3y, c4x, c4y, @x, @y)
end
def flex1
c1x = @x + @stack.shift # dx1
c1y = @y + @stack.shift # dy1
c2x = c1x + @stack.shift # dx2
c2y = c1y + @stack.shift # dy2
jpx = c2x + @stack.shift # dx3
jpy = c2y + @stack.shift # dy3
c3x = jpx + @stack.shift # dx4
c3y = jpy + @stack.shift # dy4
c4x = c3x + @stack.shift # dx5
c4y = c3y + @stack.shift # dy5
if (c4x - @x).abs > (c4y - @y).abs
@x = c4x + @stack.shift
else
@y = c4y + @stack.shift
end
@path.curve_to(c1x, c1y, c2x, c2y, jpx, jpy)
@path.curve_to(c3x, c3y, c4x, c4y, @x, @y)
end
def endchar
if !@stack.empty? && !@have_width
@width = @stack.shift + @nominal_width_x
@have_width = true
end
if @open
@path.close_path
@open = false
end
end
def hstemhm
stem
end
def hintmask
cntrmask
end
def cntrmask
stem
# skip past hinting data, no need to worry about it since we're not
# very concerned about rendering glyphs
read_bytes((@n_stems + 7) >> 3)
end
def rmoveto
if @stack.size > 2 && !@have_width
@width = @stack.shift + @nominal_width_x
@have_width = true
end
@y += @stack.pop
@x += @stack.pop
add_contour(@x, @y)
end
def hmoveto
if @stack.size > 1 && !@have_width
@width = @stack.shift + @nominal_width_x
@have_width = true
end
@x += @stack.pop
add_contour(@x, @y)
end
def vstemhm
stem
end
def rcurveline
while @stack.size > 2
c1x = @x + @stack.shift
c1y = @y + @stack.shift
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x + @stack.shift
@y = c2y + @stack.shift
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
@x += @stack.shift
@y += @stack.shift
@path.line_to(@x, @y)
end
def rlinecurve
while @stack.size > 6
@x += @stack.shift
@y += @stack.shift
@path.line_to(@x, @y)
end
c1x = @x + @stack.shift
c1y = @y + @stack.shift
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x + @stack.shift
@y = c2y + @stack.shift
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
# rubocop:disable Style/EvenOdd
def vvcurveto
if @stack.size % 2 == 1
@x += @stack.shift
end
until @stack.empty?
c1x = @x
c1y = @y + @stack.shift
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x
@y = c2y + @stack.shift
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
end
def hhcurveto
if @stack.size % 2 == 1
@y += @stack.shift
end
until @stack.empty?
c1x = @x + @stack.shift
c1y = @y
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x + @stack.shift
@y = c2y
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
end
# rubocop:enable Style/EvenOdd
def shortint
b1, b2 = read_bytes(2)
@stack << (((b1 << 24) | (b2 << 16)) >> 16)
end
def callgsubr
code_index = @stack.pop + @gsubrs_bias
subr_code = @gsubrs[code_index].bytes
@data[@index, 0] = subr_code
end
def vhcurveto
until @stack.empty?
c1x = @x
c1y = @y + @stack.shift
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x + @stack.shift
@y = c2y + (@stack.size == 1 ? @stack.shift : 0)
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
break if @stack.empty?
c1x = @x + @stack.shift
c1y = @y
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@y = c2y + @stack.shift
@x = c2x + (@stack.size == 1 ? @stack.shift : 0)
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
end
def hvcurveto
until @stack.empty?
c1x = @x + @stack.shift
c1y = @y
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@y = c2y + @stack.shift
@x = c2x + (@stack.size == 1 ? @stack.shift : 0)
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
break if @stack.empty?
c1x = @x
c1y = @y + @stack.shift
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x + @stack.shift
@y = c2y + (@stack.size == 1 ? @stack.shift : 0)
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
end
end
end
end
end
@@ -0,0 +1,45 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Charstrings Index.
class CharstringsIndex < TTFunk::Table::Cff::Index
# Top dict.
# @return [TTFunk::Table::Cff::TopDict]
attr_reader :top_dict
# @overload initialize(top_dict, file, offset, length = nil)
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param offset [Integer]
# @param length [Integer]
def initialize(top_dict, *remaining_args)
super(*remaining_args)
@top_dict = top_dict
end
private
def decode_item(index, _offset, _length)
TTFunk::Table::Cff::Charstring.new(index, top_dict, font_dict_for(index), super)
end
def encode_items(charmap)
charmap
.reject { |code, mapping| mapping[:new].zero? && !code.zero? }
.sort_by { |_code, mapping| mapping[:new] }
.map { |(_code, mapping)| items[mapping[:old]] }
end
def font_dict_for(index)
# only CID-keyed fonts contain an FD selector and font dicts
if top_dict.is_cid_font?
fd_index = top_dict.font_dict_selector[index]
top_dict.font_index[fd_index]
end
end
end
end
end
end
@@ -0,0 +1,298 @@
# frozen_string_literal: true
require 'bigdecimal'
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Dict.
class Dict < TTFunk::SubTable
# Indicates malformed operand.
class InvalidOperandError < StandardError; end
# Indicates too many operands.
class TooManyOperandsError < StandardError; end
# Single-byte operators.
OPERATOR_BZERO = (0..21).freeze
# Bytes indicating an operand.
OPERAND_BZERO = [28..30, 32..254].freeze
# Two-byte operator
WIDE_OPERATOR_BZERO = 12
# Two-byte operator adjustment. Used for encoding and decoding of wide
# operators.
WIDE_OPERATOR_ADJUSTMENT = 1200
# Maximum number of operands allowed per operator.
MAX_OPERANDS = 48
# Scientific notation operand significand validation regular
# experession.
VALID_SCI_SIGNIFICAND_RE = /\A-?(\.\d+|\d+|\d+\.\d+)\z/.freeze
# Scientific notation operand exponent validation regular experession.
VALID_SCI_EXPONENT_RE = /\A-?\d+\z/.freeze
include Enumerable
# Get dict value by operator.
#
# @param operator [Integer]
# @return [Array<Integer, TTFunk::SciForm>]
def [](operator)
@dict[operator]
end
# Add dict entry.
#
# @param operator [Integer] Entry operator. Must be in range 0..255. Wide operators must be in range 1200..1455.
# @param operands [Array<Integer, TTFunk::SciForm>]
def []=(operator, *operands)
@dict[operator] = Array(*operands)
end
# Iterate over dict entries.
#
# @yieldparam key [Integer]
# @yieldparam value [Array<Integer, TTFunk::SciForm>]
# @return [void]
def each(&block)
@dict.each(&block)
end
alias each_pair each
# Encode dict.
#
# @return [String]
def encode
sort_by(&:first)
.map { |(operator, operands)|
operands.map { |operand| encode_operand(operand) }.join +
encode_operator(operator)
}
.join
end
private
def encode_operator(operator)
if operator >= WIDE_OPERATOR_ADJUSTMENT
[
WIDE_OPERATOR_BZERO,
operator - WIDE_OPERATOR_ADJUSTMENT,
].pack('C*')
else
[operator].pack('C')
end
end
def encode_operand(operand)
case operand
when Integer
encode_integer(operand)
when Float, BigDecimal
encode_float(operand)
when SciForm
encode_sci(operand)
end
end
def encode_integer(int)
case int
when -107..107
[int + 139].pack('C')
when 108..1131
int -= 108
[(int >> 8) + 247, int & 0xFF].pack('C*')
when -1131..-108
int = -int - 108
[(int >> 8) + 251, int & 0xFF].pack('C*')
when -32_768..32_767
[28, (int >> 8) & 0xFF, int & 0xFF].pack('C*')
else
encode_integer32(int)
end
end
def encode_integer32(int)
[29, int].pack('CN')
end
def encode_float(float)
pack_decimal_nibbles(encode_significand(float))
end
def encode_sci(sci)
sig_bytes = encode_significand(sci.significand)
exp_bytes = encode_exponent(sci.exponent)
pack_decimal_nibbles(sig_bytes + exp_bytes)
end
def encode_exponent(exp)
return [] if exp.zero?
[exp.positive? ? 0xB : 0xC, *encode_significand(exp.abs)]
end
def encode_significand(sig)
sig.to_s.each_char.with_object([]) do |char, ret|
case char
when '0'..'9'
ret << Integer(char)
when '.'
ret << 0xA
when '-'
ret << 0xE
else
break ret
end
end
end
def pack_decimal_nibbles(nibbles)
bytes = [30]
nibbles.each_slice(2).each do |(high_nb, low_nb)|
# low_nb can be nil if nibbles contains an odd number of elements
low_nb ||= 0xF
bytes << ((high_nb << 4) | low_nb)
end
bytes << 0xFF if nibbles.size.even?
bytes.pack('C*')
end
def parse!
@dict = {}
operands = []
# @length must be set via the constructor
while io.pos < table_offset + length
case b_zero = read(1, 'C').first
when WIDE_OPERATOR_BZERO
operator = decode_wide_operator
@dict[operator] = operands
operands = []
when OPERATOR_BZERO
@dict[b_zero] = operands unless operands.empty?
operands = []
when *OPERAND_BZERO
operands << decode_operand(b_zero)
if operands.size > MAX_OPERANDS
raise TooManyOperandsError,
"found one too many operands at position #{io.pos} in dict at position #{table_offset}"
end
else
raise Error, "dict byte value #{b_zero} is reserved"
end
end
end
def decode_wide_operator
WIDE_OPERATOR_ADJUSTMENT + read(1, 'C').first
end
def decode_operand(b_zero)
case b_zero
when 30
decode_sci
else
decode_integer(b_zero)
end
end
def decode_sci
significand = ''.b
exponent = ''.b
loop do
current = read(1, 'C').first
break if current == 0xFF
high_nibble = current >> 4
low_nibble = current & 0x0F # 0b00001111
[high_nibble, low_nibble].each do |nibble|
case nibble
when 0..9
(exponent.empty? ? significand : exponent) << nibble.to_s
when 0xA
significand << '.'
when 0xB
# take advantage of Integer#to_i not caring about whitespace
exponent << ' '
when 0xC
exponent << '-'
when 0xE
significand << '-'
end
end
break if low_nibble == 0xF
end
validate_sci!(significand, exponent)
exponent = 0 if exponent.empty?
SciForm.new(Float(significand), Integer(exponent))
end
def validate_sci!(significand, exponent)
unless valid_significand?(significand) && valid_exponent?(exponent)
raise InvalidOperandError,
'invalid scientific notation operand with significand ' \
"'#{significand}' and exponent '#{exponent}' ending at " \
"position #{io.pos} in dict at position #{table_offset}"
end
end
def valid_significand?(significand)
!(significand.strip =~ VALID_SCI_SIGNIFICAND_RE).nil?
end
def valid_exponent?(exponent)
exponent = exponent.strip
return true if exponent.empty?
!(exponent.strip =~ VALID_SCI_EXPONENT_RE).nil?
end
def decode_integer(b_zero)
case b_zero
when 32..246
# 1 byte
b_zero - 139
when 247..250
# 2 bytes
b_one = read(1, 'C').first
((b_zero - 247) * 256) + b_one + 108
when 251..254
# 2 bytes
b_one = read(1, 'C').first
(-(b_zero - 251) * 256) - b_one - 108
when 28
# 2 bytes in number (3 total)
b_one, b_two = read(2, 'C*')
BinUtils.twos_comp_to_int((b_one << 8) | b_two, bit_width: 16)
when 29
# 4 bytes in number (5 total)
b_one, b_two, b_three, b_four = read(4, 'C*')
BinUtils.twos_comp_to_int((b_one << 24) | (b_two << 16) | (b_three << 8) | b_four, bit_width: 32)
end
end
end
end
end
end
@@ -0,0 +1,277 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Encoding.
class Encoding < TTFunk::SubTable
include Enumerable
# Predefined Standard Encoding ID.
STANDARD_ENCODING_ID = 0
# Predefined Expert Encoding ID.
EXPERT_ENCODING_ID = 1
# Default encoding ID.
DEFAULT_ENCODING_ID = STANDARD_ENCODING_ID
class << self
# Get predefined encoding by ID.
#
# @param encoding_id [Integer]
# @return [TTFunk::OneBasedArray<Integer>]
def codes_for_encoding_id(encoding_id)
case encoding_id
when STANDARD_ENCODING_ID
Encodings::STANDARD
when EXPERT_ENCODING_ID
Encodings::EXPERT
end
end
end
# Top dict.
# @return [TTFunk::Table::Cff::TopDict]
attr_reader :top_dict
# Encodign format.
# @return [Integer]
attr_reader :format
# Number of encoded items.
# @return [Integer]
attr_reader :items_count
# Offset or encoding ID.
# @return [Integer]
attr_reader :offset_or_id
# @overload initialize(top_dict, file, offset = nil, length = nil)
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param offset [Integer]
# @param length [Integer]
# @overload initialize(top_dict, file, charset_id)
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param encoding_id [Integer] 0, 1, or 2
def initialize(top_dict, file, offset_or_id = nil, length = nil)
@top_dict = top_dict
@offset_or_id = offset_or_id || DEFAULT_ENCODING_ID
if offset
super(file, offset, length)
@supplemental = format >> 7 == 1
else
@items_count = self.class.codes_for_encoding_id(offset_or_id).size
@supplemental = false
end
end
# Iterate over character codes.
#
# @overload each()
# @yieldparam code [Integer]
# @return [void]
# @overload each()
# @return [Enumerator]
def each
return to_enum(__method__) unless block_given?
# +1 adjusts for the implicit .notdef glyph
(items_count + 1).times { |i| yield(self[i]) }
end
# Get character code for glyph index.
#
# @param glyph_id [Integer]
# @return [Integer, nil]
def [](glyph_id)
return 0 if glyph_id.zero?
return code_for(glyph_id) if offset
self.class.codes_for_encoding_id(offset_or_id)[glyph_id]
end
# Encoding offset in the file.
#
# @return [Integer, nil]
def offset
# Numbers from 0..1 mean encoding IDs instead of offsets. IDs are
# pre-defined, generic encodings that define the characters present
# in the font.
#
# In the case of an offset, add the CFF table's offset since the
# charset offset is relative to the start of the CFF table. Otherwise
# return nil (no offset).
if offset_or_id > 1
offset_or_id + top_dict.cff_offset
end
end
# Encode encoding.
#
# @param charmap [Hash{Integer => Hash}] keys are the charac codes,
# values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# @return [String]
def encode(charmap)
# Any subset encoding is all but guaranteed to be different from the
# standard encoding so we don't even attempt to see if it matches. We
# assume it's different and just encode it anew.
return encode_supplemental(charmap) if supplemental?
codes =
charmap
.reject { |_code, mapping| mapping[:new].zero? }
.sort_by { |_code, mapping| mapping[:new] }
.map { |(code, _m)| code }
ranges = TTFunk::BinUtils.rangify(codes)
# calculate whether storing the charset as a series of ranges is
# more efficient (i.e. takes up less space) vs storing it as an
# array of SID values
total_range_size = (2 * ranges.size) +
(element_width(:range_format) * ranges.size)
total_array_size = codes.size * element_width(:array_format)
if total_array_size <= total_range_size
([format_int(:array_format), codes.size] + codes).pack('C*')
else
element_fmt = element_format(:range_format)
result = [format_int(:range_format), ranges.size].pack('CC')
ranges.each { |range| result << range.pack(element_fmt) }
result
end
end
# Is this a supplemental encoding?
#
# @return [Boolean]
def supplemental?
# high-order bit set to 1 indicates supplemental encoding
@supplemental
end
private
def encode_supplemental(charmap)
new_entries =
charmap
.reject { |_code, mapping| mapping[:new].zero? }
.transform_values { |mapping| mapping[:new] }
result = [format_int(:supplemental), new_entries.size].pack('CC')
fmt = element_format(:supplemental)
new_entries.each do |code, new_gid|
result << [code, new_gid].pack(fmt)
end
result
end
def code_for(glyph_id)
return 0 if glyph_id.zero?
# rather than validating the glyph as part of one of the predefined
# encodings, just pass it through
return glyph_id unless offset
case format_sym
when :array_format, :supplemental
@entries[glyph_id]
when :range_format
remaining = glyph_id
@entries.each do |range|
if range.size >= remaining
return (range.first + remaining) - 1
end
remaining -= range.size
end
0
end
end
def parse!
@format, entry_count = read(2, 'C*')
@length = entry_count * element_width
case format_sym
when :array_format
@items_count = entry_count
@entries = OneBasedArray.new(read(length, 'C*'))
when :range_format
@entries = []
@items_count = 0
entry_count.times do
code, num_left = read(element_width, element_format)
@entries << (code..(code + num_left))
@items_count += num_left + 1
end
when :supplemental
@entries = {}
@items_count = entry_count
entry_count.times do
code, glyph = read(element_width, element_format)
@entries[code] = glyph
end
end
end
def element_format(fmt = format_sym)
{
array_format: 'C',
range_format: 'CC',
supplemental: 'Cn',
}[fmt]
end
# @TODO: handle supplemental encoding (necessary?)
def element_width(fmt = format_sym)
case fmt
when :array_format then 1
when :range_format then 2
when :supplemental then 3
else
raise Error, "'#{fmt}' is an unsupported encoding format"
end
end
def format_sym
return :supplemental if supplemental?
case @format
when 0 then :array_format
when 1 then :range_format
else
raise Error, "unsupported charset format '#{fmt}'"
end
end
def format_int(sym = format_sym)
case sym
when :array_format then 0
when :range_format then 1
when :supplemental then 129
else
raise Error, "unsupported charset format '#{sym}'"
end
end
end
end
end
end
@@ -0,0 +1,13 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# Predefined encodings.
module Encodings
autoload :EXPERT, 'ttfunk/table/cff/encodings/expert'
autoload :STANDARD, 'ttfunk/table/cff/encodings/standard'
end
end
end
end
@@ -0,0 +1,207 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
module Encodings
# CFF predefined Expert encoding.
EXPERT = OneBasedArray.new(
[
*[0] * 31,
1,
229,
230,
0,
231,
232,
233,
234,
235,
236,
237,
238,
13,
14,
15,
99,
239,
240,
241,
242,
243,
244,
245,
246,
247,
248,
27,
28,
249,
250,
251,
252,
0,
253,
254,
255,
256,
257,
0,
0,
0,
258,
0,
0,
259,
260,
261,
262,
0,
0,
263,
264,
265,
0,
266,
109,
110,
267,
268,
269,
0,
270,
271,
272,
273,
274,
275,
276,
277,
278,
279,
280,
281,
282,
283,
284,
285,
286,
287,
288,
289,
290,
291,
292,
293,
294,
295,
296,
297,
298,
299,
300,
301,
302,
303,
*[0] * 34,
304,
305,
306,
0,
0,
307,
308,
309,
310,
311,
0,
312,
0,
0,
313,
0,
0,
314,
315,
0,
0,
316,
317,
318,
0,
0,
0,
158,
155,
163,
319,
320,
321,
322,
323,
324,
325,
0,
0,
326,
150,
164,
169,
327,
328,
329,
330,
331,
332,
333,
334,
335,
336,
337,
338,
339,
340,
341,
342,
343,
344,
345,
346,
347,
348,
349,
350,
351,
352,
353,
354,
355,
356,
357,
358,
359,
360,
361,
362,
363,
364,
365,
366,
367,
368,
369,
370,
371,
372,
373,
374,
375,
376,
377,
378,
],
).freeze
end
end
end
end
@@ -0,0 +1,182 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
module Encodings
# CFF predefined Standard encoding.
STANDARD = OneBasedArray.new(
[
*[0] * 31,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
*[0] * 34,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
0,
111,
112,
113,
114,
0,
115,
116,
117,
118,
119,
120,
121,
122,
0,
123,
0,
124,
125,
126,
127,
128,
129,
130,
131,
0,
132,
133,
0,
134,
135,
136,
137,
*[0] * 16,
138,
0,
139,
*[0] * 4,
140,
141,
142,
143,
*[0] * 5,
144,
0,
0,
0,
145,
0,
0,
146,
147,
148,
149,
*[0] * 4,
],
).freeze
end
end
end
end
@@ -0,0 +1,193 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF FDSelect.
class FdSelector < TTFunk::SubTable
include Enumerable
# Array format.
ARRAY_FORMAT = 0
# Range format.
RANGE_FORMAT = 3
# Range entry size.
RANGE_ENTRY_SIZE = 3
# Array entry size.
ARRAY_ENTRY_SIZE = 1
# Top dict.
# @return [TTFunk::Table::Cff::TopDict]
attr_reader :top_dict
# Number of encoded items.
# @return [Integer]
attr_reader :items_count
# Number of entries.
# @return [Array<Integer>] if format is array.
# @return [Array<Array(Range, Integer)>] if format is range.
attr_reader :entries
# Number of glyphs.
# @return [Integer]
attr_reader :n_glyphs
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param offset [Integer]
# @param length [Integer]
def initialize(top_dict, file, offset, length = nil)
@top_dict = top_dict
super(file, offset, length)
end
# Get font dict index for glyph ID.
#
# @return [Integer]
def [](glyph_id)
case format_sym
when :array_format
entries[glyph_id]
when :range_format
if (entry = range_cache[glyph_id])
return entry
end
range, entry =
entries.bsearch { |rng, _|
if rng.cover?(glyph_id)
0
elsif glyph_id < rng.first
-1
else
1
end
}
range.each { |i| range_cache[i] = entry }
entry
end
end
# Iterate over font dicts for each glyph ID.
#
# @yieldparam [Integer] font dict index.
# @return [void]
def each
return to_enum(__method__) unless block_given?
items_count.times { |i| yield(self[i]) }
end
# Encode Font dict selector.
#
# @param charmap [Hash{Integer => Hash}] keys are the charac codes,
# values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# @return [String]
def encode(charmap)
# get list of [new_gid, fd_index] pairs
new_indices =
charmap
.reject { |code, mapping| mapping[:new].zero? && !code.zero? }
.sort_by { |_code, mapping| mapping[:new] }
.map { |(_code, mapping)| [mapping[:new], self[mapping[:old]]] }
ranges = rangify_gids(new_indices)
total_range_size = ranges.size * RANGE_ENTRY_SIZE
total_array_size = new_indices.size * ARRAY_ENTRY_SIZE
''.b.tap do |result|
if total_array_size <= total_range_size
result << [ARRAY_FORMAT].pack('C')
result << new_indices.map(&:last).pack('C*')
else
result << [RANGE_FORMAT, ranges.size].pack('Cn')
ranges.each { |range| result << range.pack('nC') }
# "A sentinel GID follows the last range element and serves to
# delimit the last range in the array. (The sentinel GID is set
# equal to the number of glyphs in the font. That is, its value
# is 1 greater than the last GID in the font)."
result << [new_indices.size].pack('n')
end
end
end
private
def range_cache
@range_cache ||= {}
end
# values is an array of [new_gid, fd_index] pairs
def rangify_gids(values)
start_gid = 0
[].tap do |ranges|
values.each_cons(2) do |(_, first_idx), (sec_gid, sec_idx)|
if first_idx != sec_idx
ranges << [start_gid, first_idx]
start_gid = sec_gid
end
end
ranges << [start_gid, values.last[1]]
end
end
def parse!
@format = read(1, 'C').first
@length = 1
case format_sym
when :array_format
@n_glyphs = top_dict.charstrings_index.items_count
data = io.read(n_glyphs)
@length += data.bytesize
@items_count = data.bytesize
@entries = data.bytes
when :range_format
# +2 for sentinel GID, +2 for num_ranges
num_ranges = read(2, 'n').first
@length += (num_ranges * RANGE_ENTRY_SIZE) + 4
ranges = Array.new(num_ranges) { read(RANGE_ENTRY_SIZE, 'nC') }
@entries =
ranges.each_cons(2).map { |first, second|
first_gid, fd_index = first
second_gid, = second
[(first_gid...second_gid), fd_index]
}
# read the sentinel GID, otherwise known as the number of glyphs
# in the font
@n_glyphs = read(2, 'n').first
last_start_gid, last_fd_index = ranges.last
@entries << [(last_start_gid...(n_glyphs + 1)), last_fd_index]
@items_count = entries.reduce(0) { |sum, entry| sum + entry.first.size }
end
end
def format_sym
case @format
when ARRAY_FORMAT then :array_format
when RANGE_FORMAT then :range_format
else
raise Error, "unsupported fd select format '#{@format}'"
end
end
end
end
end
end
@@ -0,0 +1,91 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Font dict.
class FontDict < TTFunk::Table::Cff::Dict
# Length of placeholders.
PLACEHOLDER_LENGTH = 5
# Operators we care about in this dict.
OPERATORS = { private: 18 }.freeze
# Inverse operator mapping.
OPERATOR_CODES = OPERATORS.invert
# Top dict.
# @return [TTFunk::Table::Cff::TopDict]
attr_reader :top_dict
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param offset [Integer]
# @param length [Integer]
def initialize(top_dict, file, offset, length = nil)
@top_dict = top_dict
super(file, offset, length)
end
# Encode dict.
#
# @return [TTFunk::EncodedString]
def encode
EncodedString.new do |result|
each do |operator, operands|
case OPERATOR_CODES[operator]
when :private
result << encode_private
else
operands.each { |operand| result << encode_operand(operand) }
end
result << encode_operator(operator)
end
end
end
# Finalize dict.
#
# @param new_cff_data [TTFunk::EncodedString]
# @return [void]
def finalize(new_cff_data)
encoded_private_dict = private_dict.encode
encoded_offset = encode_integer32(new_cff_data.length)
encoded_length = encode_integer32(encoded_private_dict.length)
new_cff_data.resolve_placeholder(:"private_length_#{@table_offset}", encoded_length)
new_cff_data.resolve_placeholder(:"private_offset_#{@table_offset}", encoded_offset)
private_dict.finalize(encoded_private_dict)
new_cff_data << encoded_private_dict
end
# Private dict.
#
# @return [TTFunk::Table::Cff::PrivateDict, nil]
def private_dict
@private_dict ||=
if (info = self[OPERATORS[:private]])
private_dict_length, private_dict_offset = info
PrivateDict.new(
file,
top_dict.cff_offset + private_dict_offset,
private_dict_length,
)
end
end
private
def encode_private
EncodedString.new do |result|
result << Placeholder.new(:"private_length_#{@table_offset}", length: PLACEHOLDER_LENGTH)
result << Placeholder.new(:"private_offset_#{@table_offset}", length: PLACEHOLDER_LENGTH)
end
end
end
end
end
end
@@ -0,0 +1,42 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Font Dict Index.
class FontIndex < TTFunk::Table::Cff::Index
# Top dict.
# @return [TTFunk::Table::Cff::TopDict]
attr_reader :top_dict
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param offset [Integer]
# @param length [Integer]
def initialize(top_dict, file, offset, length = nil)
super(file, offset, length)
@top_dict = top_dict
end
# Finalize index.
#
# @param new_cff_data [TTFunk::EncodedString]
# @return [void]
def finalize(new_cff_data)
each { |font_dict| font_dict.finalize(new_cff_data) }
end
private
def decode_item(_index, offset, length)
TTFunk::Table::Cff::FontDict.new(top_dict, file, offset, length)
end
def encode_items(*)
# Re-encode font dicts
map(&:encode)
end
end
end
end
end
@@ -0,0 +1,46 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Header.
class Header < TTFunk::SubTable
# CFF table major version.
# @return [Integer]
attr_reader :major
# CFF table minor version.
# @return [Integer]
attr_reader :minor
# Size of the header itself.
# @return [Integer]
attr_reader :header_size
# Size of all offsets from beginning of table.
# @return [Integer]
attr_reader :absolute_offset_size
# Length of header.
#
# @return [Integer]
def length
4
end
# Encode header.
#
# @return [String]
def encode
[major, minor, header_size, absolute_offset_size].pack('C*')
end
private
def parse!
@major, @minor, @header_size, @absolute_offset_size = read(4, 'C*')
end
end
end
end
end
@@ -0,0 +1,158 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Index.
class Index < TTFunk::SubTable
include Enumerable
# Get value by index.
#
# @param index [Integer]
# @return [any]
def [](index)
return if index >= items_count
entry_cache[index] ||=
decode_item(
index,
data_reference_offset + offsets[index],
offsets[index + 1] - offsets[index],
)
end
# Iterate over index items.
#
# @overload each()
# @yieldparam item [any]
# @return [void]
# @overload each()
# @return [Enumerator]
def each(&block)
return to_enum(__method__) unless block
items_count.times do |i|
yield(self[i])
end
end
# Numer of items in this index.
#
# @return [Integer]
def items_count
items.length
end
# Encode index.
#
# @param args all arguments are passed to `encode_item` method.
# @return [TTFunk::EncodedString]
def encode(*args)
new_items = encode_items(*args)
if new_items.empty?
return [0].pack('n')
end
if new_items.length > 0xffff
raise Error, 'Too many items in a CFF index'
end
offsets_array =
new_items
.each_with_object([1]) { |item, offsets|
offsets << (offsets.last + item.length)
}
offset_size = (offsets_array.last.bit_length / 8.0).ceil
offsets_array.map! { |offset| encode_offset(offset, offset_size) }
EncodedString.new.concat(
[new_items.length, offset_size].pack('nC'),
*offsets_array,
*new_items,
)
end
private
attr_reader :items
attr_reader :offsets
attr_reader :data_reference_offset
def entry_cache
@entry_cache ||= {}
end
# Returns an array of EncodedString elements (plain strings,
# placeholders, or EncodedString instances). Each element is supposed to
# represent an encoded item.
#
# This is the place to do all the filtering, reordering, or individual
# item encoding.
#
# It gets all the arguments `encode` gets.
def encode_items(*)
items
end
# By default do nothing
def decode_item(index, _offset, _length)
items[index]
end
def encode_offset(offset, offset_size)
case offset_size
when 1
[offset].pack('C')
when 2
[offset].pack('n')
when 3
[offset].pack('N')[1..]
when 4
[offset].pack('N')
end
end
def parse!
@entry_cache = {}
num_entries = read(2, 'n').first
if num_entries.zero?
@length = 2
@items = []
return
end
offset_size = read(1, 'C').first
@offsets =
Array.new(num_entries + 1) {
unpack_offset(io.read(offset_size), offset_size)
}
@data_reference_offset = table_offset + 3 + (offsets.length * offset_size) - 1
@length =
2 + # num entries
1 + # offset size
(offsets.length * offset_size) + # offsets
offsets.last - 1 # items
@items =
offsets.each_cons(2).map { |offset, next_offset|
io.read(next_offset - offset)
}
end
def unpack_offset(offset_data, offset_size)
padding = "\x00" * (4 - offset_size)
(padding + offset_data).unpack1('N')
end
end
end
end
end
@@ -0,0 +1,45 @@
# frozen_string_literal: true
require 'forwardable'
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Index with indexing starting at 1.
class OneBasedIndex
extend Forwardable
def_delegators :base_index,
:each,
:table_offset,
:items_count,
:length,
:encode
# Underlaying Index.
# @return [TTFunk::Table::Cff::Index]
attr_reader :base_index
# @param args [Array] all params are passed to the base index.
# @see Index
def initialize(*args)
@base_index = Index.new(*args)
end
# Get item by index.
#
# @param idx [Integer]
# @return [any]
# @raise [IndexError] when requested index is 0.
def [](idx)
if idx.zero?
raise IndexError,
"index #{idx} was outside the bounds of the index"
end
base_index[idx - 1]
end
end
end
end
end
@@ -0,0 +1,107 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# Path. Mostly used for CFF glyph outlines.
class Path
# Close path command.
CLOSE_PATH_CMD = [:close].freeze
# Commands in this path.
# @return [Array]
attr_reader :commands
# Number of contours in this path.
# @return [Integer]
attr_reader :number_of_contours
def initialize
@commands = []
@number_of_contours = 0
end
# Move implicit cursor to coordinates.
#
# @param x [Integer, Float]
# @param y [Integer, Float]
# @return [void]
def move_to(x, y)
@commands << [:move, x, y]
end
# Add a line to coordinates.
#
# @param x [Integer, Float]
# @param y [Integer, Float]
# @return [void]
def line_to(x, y)
@commands << [:line, x, y]
end
# Add a Bézier curve. Current position is the first control point, (`x1`,
# `y1`) is the second, (`x2`, `y2`) is the third, and (`x`, `y`) is the
# last control point.
#
# @param x1 [Integer, Float]
# @param y1 [Integer, Float]
# @param x2 [Integer, Float]
# @param y2 [Integer, Float]
# @param x [Integer, Float]
# @param y [Integer, Float]
# @return [void]
def curve_to(x1, y1, x2, y2, x, y) # rubocop: disable Metrics/ParameterLists
@commands << [:curve, x1, y1, x2, y2, x, y]
end
# Close current contour.
#
# @return [void]
def close_path
@commands << CLOSE_PATH_CMD
@number_of_contours += 1
end
# Reposition and scale path.
#
# @param x [Integer, Float] new horizontal position.
# @param y [Integer, Float] new vertical position.
# @param font_size [Integer, Float] font size.
# @param units_per_em [Integer] units per Em as defined in the font.
# @return [TTFunk::Table::Cff::Path]
def render(x: 0, y: 0, font_size: 72, units_per_em: 1000)
new_path = self.class.new
scale = 1.0 / units_per_em * font_size
commands.each do |cmd|
case cmd[:type]
when :move
new_path.move_to(x + (cmd[1] * scale), y + (-cmd[2] * scale))
when :line
new_path.line_to(x + (cmd[1] * scale), y + (-cmd[2] * scale))
when :curve
new_path.curve_to(
x + (cmd[1] * scale),
y + (-cmd[2] * scale),
x + (cmd[3] * scale),
y + (-cmd[4] * scale),
x + (cmd[5] * scale),
y + (-cmd[6] * scale),
)
when :close
new_path.close_path
end
end
new_path
end
private
def format_values(command)
command[1..].map { |k| format('%.2f', k) }.join(' ')
end
end
end
end
end
@@ -0,0 +1,104 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Private dict.
class PrivateDict < TTFunk::Table::Cff::Dict
# Default value of Default Width X.
DEFAULT_WIDTH_X_DEFAULT = 0
# Default value of Nominal Width X.
DEFAULT_WIDTH_X_NOMINAL = 0
# Length of placeholders.
PLACEHOLDER_LENGTH = 5
# Operators we care about in this dict.
OPERATORS = {
subrs: 19,
default_width_x: 20,
nominal_width_x: 21,
}.freeze
# Inverse operator mapping.
OPERATOR_CODES = OPERATORS.invert
# Encode dict.
#
# @return [TTFunk::EncodedString]
def encode
# TODO: use mapping to determine which subroutines are still used.
# For now, just encode them all.
EncodedString.new do |result|
each do |operator, operands|
case OPERATOR_CODES[operator]
when :subrs
result << encode_subrs
else
operands.each { |operand| result << encode_operand(operand) }
end
result << encode_operator(operator)
end
end
end
# Finalize dict.
#
# @param private_dict_data [TTFunk::EncodedString]
# @return [void]
def finalize(private_dict_data)
return unless subr_index
encoded_subr_index = subr_index.encode
encoded_offset = encode_integer32(private_dict_data.length)
private_dict_data.resolve_placeholder(:"subrs_#{@table_offset}", encoded_offset)
private_dict_data << encoded_subr_index
end
# Subroutine index.
#
# @return [TTFunk::Table::Cff::SubrIndex, nil]
def subr_index
@subr_index ||=
if (subr_offset = self[OPERATORS[:subrs]])
SubrIndex.new(file, table_offset + subr_offset.first)
end
end
# Default Width X.
#
# @return [Integer]
def default_width_x
if (width = self[OPERATORS[:default_width_x]])
width.first
else
DEFAULT_WIDTH_X_DEFAULT
end
end
# Nominal Width X.
#
# @return [Integer]
def nominal_width_x
if (width = self[OPERATORS[:nominal_width_x]])
width.first
else
DEFAULT_WIDTH_X_NOMINAL
end
end
private
def encode_subrs
EncodedString.new do |result|
result << Placeholder.new(:"subrs_#{@table_offset}", length: PLACEHOLDER_LENGTH)
end
end
end
end
end
end
@@ -0,0 +1,24 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Subroutine index.
class SubrIndex < TTFunk::Table::Cff::Index
# Subroutine index biase. For correct subroutine selection the
# calculated bias must be added to the subroutine number operand before
# accessing the index.
# @return [Integer]
def bias
if items.length < 1240
107
elsif items.length < 33_900
1131
else
32_768
end
end
end
end
end
end
@@ -0,0 +1,254 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF top dict.
class TopDict < TTFunk::Table::Cff::Dict
# Default charstring type.
DEFAULT_CHARSTRING_TYPE = 2
# Length of placeholders for pointer operators.
POINTER_PLACEHOLDER_LENGTH = 5
# Length of placeholders for other operators.
PLACEHOLDER_LENGTH = 5
# Operators whose values are offsets that point to other parts
# of the file.
POINTER_OPERATORS = {
charset: 15,
encoding: 16,
charstrings_index: 17,
private: 18,
font_index: 1236,
font_dict_selector: 1237,
}.freeze
# All the operators we currently care about.
OPERATORS = {
**POINTER_OPERATORS,
ros: 1230,
charstring_type: 1206,
}.freeze
# Inverse operator mapping.
OPERATOR_CODES = OPERATORS.invert
# Encode dict.
#
# @return [TTFunk::EncodedString]
def encode(*)
EncodedString.new do |result|
each_with_index do |(operator, operands), _idx|
if operator == OPERATORS[:private]
result << encode_private
elsif pointer_operator?(operator)
result << Placeholder.new(
OPERATOR_CODES[operator],
length: POINTER_PLACEHOLDER_LENGTH,
)
else
operands.each { |operand| result << encode_operand(operand) }
end
result << encode_operator(operator)
end
end
end
# Finalize the table.
#
# @param new_cff_data [TTFunk::EncodedString]
# @param charmap [Hash{Integer => Hash}] keys are the charac codes,
# values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# @return [void]
def finalize(new_cff_data, charmap)
if charset
finalize_subtable(new_cff_data, :charset, charset.encode(charmap))
end
if encoding
finalize_subtable(new_cff_data, :encoding, encoding.encode(charmap))
end
if charstrings_index
finalize_subtable(new_cff_data, :charstrings_index, charstrings_index.encode(charmap))
end
if font_index
finalize_subtable(new_cff_data, :font_index, font_index.encode)
font_index.finalize(new_cff_data)
end
if font_dict_selector
finalize_subtable(new_cff_data, :font_dict_selector, font_dict_selector.encode(charmap))
end
if private_dict
encoded_private_dict = private_dict.encode
encoded_offset = encode_integer32(new_cff_data.length)
encoded_length = encode_integer32(encoded_private_dict.length)
new_cff_data.resolve_placeholder(:"private_length_#{@table_offset}", encoded_length)
new_cff_data.resolve_placeholder(:"private_offset_#{@table_offset}", encoded_offset)
private_dict.finalize(encoded_private_dict)
new_cff_data << encoded_private_dict
end
end
# Registry Ordering Supplement.
#
# @return [Array(Integer, Integer, Integer), nil]
def ros
self[OPERATORS[:ros]]
end
# Is Registry Ordering Supplement present in this dict?
#
# @return [Boolean]
def ros?
!ros.nil?
end
alias is_cid_font? ros?
# Charset specified in this dict.
#
# @return [TTFunk::Table::Cff::Charset, nil]
def charset
@charset ||=
if (charset_offset_or_id = self[OPERATORS[:charset]])
if charset_offset_or_id.empty?
Charset.new(self, file)
else
Charset.new(self, file, charset_offset_or_id.first)
end
end
end
# Encoding specified in this dict.
#
# @return [TTFunk::Table::Cff::Encoding, nil]
def encoding
# PostScript type 1 fonts, i.e. CID fonts, i.e. some fonts that use
# the CFF table, don't specify an encoding, so this can be nil
@encoding ||=
if (encoding_offset_or_id = self[OPERATORS[:encoding]])
Encoding.new(self, file, encoding_offset_or_id.first)
end
end
# Charstrings index specified in this dict.
#
# > OpenType fonts with TrueType outlines use a glyph index to specify
# and access glyphs within a font; e.g., to index within the `loca`
# table and thereby access glyph data in the `glyf` table. This
# concept is retained in OpenType CFF fonts, except that glyph data is
# accessed through the CharStrings INDEX of the CFF table.
#
# > --- [CFF — Compact Font Format Table](https://www.microsoft.com/typography/otspec/cff.htm)
#
# @return [TTFunk::Table::Cff::CharstringsIndex, nil]
def charstrings_index
@charstrings_index ||=
if (charstrings_offset = self[OPERATORS[:charstrings_index]])
CharstringsIndex.new(self, file, cff_offset + charstrings_offset.first)
end
end
# Charstring type specified in this dict.
#
# @return [Integer]
def charstring_type
@charstring_type =
self[OPERATORS[:charstring_type]] || DEFAULT_CHARSTRING_TYPE
end
# Font index specified in this dict.
#
# @return [TTFunk::Table::Cff::FontIndex, nil]
def font_index
@font_index ||=
if (font_index_offset = self[OPERATORS[:font_index]])
FontIndex.new(self, file, cff_offset + font_index_offset.first)
end
end
# Font dict selector specified in this dict.
#
# @return [TTFunk::Table::Cff::FdSelector, nil]
def font_dict_selector
@font_dict_selector ||=
if (fd_select_offset = self[OPERATORS[:font_dict_selector]])
FdSelector.new(self, file, cff_offset + fd_select_offset.first)
end
end
# Private dict specified in this dict.
#
# @return [TTFunk::Table::Cff::PrivateDict, nil]
def private_dict
@private_dict ||=
if (info = self[OPERATORS[:private]])
private_dict_length, private_dict_offset = info
PrivateDict.new(file, cff_offset + private_dict_offset, private_dict_length)
end
end
# CFF table in this file.
#
# @return [TTFunk::Table::Cff]
def cff
file.cff
end
# Ofsset of CFF table in the file.
#
# @return [Integer]
def cff_offset
cff.offset
end
private
def encode_private
EncodedString.new do |result|
result << Placeholder.new(
:"private_length_#{@table_offset}",
length: PLACEHOLDER_LENGTH,
)
result << Placeholder.new(
:"private_offset_#{@table_offset}",
length: PLACEHOLDER_LENGTH,
)
end
end
def finalize_subtable(new_cff_data, name, table_data)
encoded = encode_integer32(new_cff_data.length)
new_cff_data.resolve_placeholder(name, encoded)
new_cff_data << table_data
end
def pointer_operator?(operator)
POINTER_OPERATORS.include?(OPERATOR_CODES[operator])
end
def encode_charstring_type(charstring_type)
if charstring_type == DEFAULT_CHARSTRING_TYPE
''
else
encode_operand(charstring_type)
end
end
end
end
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Index conatining Top dict.
class TopIndex < TTFunk::Table::Cff::Index
private
def decode_item(_index, offset, length)
TTFunk::Table::Cff::TopDict.new(file, offset, length)
end
def encode_items(*)
# Re-encode the top dict
map(&:encode)
end
end
end
end
end