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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 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
|