///|
fn pdf_truetype_required_subset_table(tag : String) -> Bool {
tag == "head" ||
tag == "hhea" ||
tag == "loca" ||
tag == "cmap" ||
tag == "maxp" ||
tag == "cvt " ||
tag == "glyf" ||
tag == "prep" ||
tag == "hmtx" ||
tag == "fpgm"
}
///|
/// Write the format-6 cmap table used by cpdf for implicit-font-file subsets.
///
/// Character codes start at `33`; each Unicode value in `subset_codepoints`
/// contributes its original glyph index, or `0` if the original cmap has no
/// mapping. The returned bytes are padded, while `length` is the unpadded table
/// length for directory metadata.
pub fn pdf_truetype_subset_cmap_table(
data : BytesView,
subset_codepoints : ArrayView[Int],
) -> @truetype.PdfTrueTypeSubsetTable raise @core.PdfError {
let cmap_entries = pdf_truetype_cmap_glyphs(data)
let glyph_indices : Array[Int] = []
for codepoint in subset_codepoints {
glyph_indices.push(
match pdf_truetype_cmap_glyph_from_entries(cmap_entries, codepoint) {
Some(glyph_index) => glyph_index
None => 0
},
)
}
let length = 22 + glyph_indices.length() * 2
let output = Array::make(length + pdf_truetype_table_padding(length), b'\x00')
let mut position = pdf_truetype_write_u16_at(output, 0, 0)
position = pdf_truetype_write_u16_at(output, position, 1)
position = pdf_truetype_write_u16_at(output, position, 1)
position = pdf_truetype_write_u16_at(output, position, 0)
position = pdf_truetype_write_u32_at(output, position, 12)
position = pdf_truetype_write_u16_at(output, position, 6)
position = pdf_truetype_write_u16_at(
output,
position,
10 + glyph_indices.length() * 2,
)
position = pdf_truetype_write_u16_at(output, position, 0)
position = pdf_truetype_write_u16_at(output, position, 33)
position = pdf_truetype_write_u16_at(output, position, glyph_indices.length())
for glyph_index in glyph_indices {
position = pdf_truetype_write_u16_at(
output,
position,
pdf_truetype_normalize_u16(glyph_index),
)
}
{ length, bytes: Bytes::from_array(output), }
}
///|
fn pdf_truetype_sorted_tables(
tables : ArrayView[@truetype.PdfTrueTypeTableRecord],
) -> Array[@truetype.PdfTrueTypeTableRecord] {
let sorted : Array[@truetype.PdfTrueTypeTableRecord] = []
for table in tables {
sorted.push(table)
}
sorted.sort_by(fn(left, right) { left.offset.compare(right.offset) })
sorted
}
///|
fn pdf_truetype_subset_font_kept_tables(
sorted_tables : ArrayView[@truetype.PdfTrueTypeTableRecord],
) -> Array[@truetype.PdfTrueTypeTableRecord] {
let kept : Array[@truetype.PdfTrueTypeTableRecord] = []
let mut removed_data_before = 0
for index in 0.. Int {
length + pdf_truetype_table_padding(length)
}
///|
fn pdf_truetype_subset_original_table_span(
data : BytesView,
sorted_tables : ArrayView[@truetype.PdfTrueTypeTableRecord],
tag : String,
) -> (Int, Int) raise @core.PdfError {
for index in 0.. data.length() {
raise SoftError("Truncated TrueType " + tag + " table")
}
break (table.offset, length)
}
} nobreak {
raise SoftError("No " + tag + " table found in TrueType font")
}
}
///|
fn pdf_truetype_subset_original_table(
data : BytesView,
sorted_tables : ArrayView[@truetype.PdfTrueTypeTableRecord],
tag : String,
directory_length : Int,
) -> @truetype.PdfTrueTypeSubsetTable raise @core.PdfError {
let (offset, length) = pdf_truetype_subset_original_table_span(
data, sorted_tables, tag,
)
{ length: directory_length, bytes: data[offset:offset + length].to_owned(), }
}
///|
fn pdf_truetype_subset_font_table_payload(
data : BytesView,
sorted_tables : ArrayView[@truetype.PdfTrueTypeTableRecord],
record : @truetype.PdfTrueTypeTableRecord,
subset_codepoints : ArrayView[Int],
rewrite_cmap : Bool,
) -> @truetype.PdfTrueTypeSubsetTable raise @core.PdfError {
if record.tag == "loca" {
{
length: record.length,
bytes: pdf_truetype_subset_loca_table(data, subset_codepoints),
}
} else if record.tag == "glyf" {
pdf_truetype_subset_glyf_table(data, subset_codepoints)
} else if record.tag == "cmap" && rewrite_cmap {
pdf_truetype_subset_cmap_table(data, subset_codepoints)
} else {
pdf_truetype_subset_original_table(
data,
sorted_tables,
record.tag,
record.length,
)
}
}
///|
fn pdf_truetype_subset_font_directory(
data : BytesView,
sorted_tables : ArrayView[@truetype.PdfTrueTypeTableRecord],
subset_codepoints : ArrayView[Int],
rewrite_cmap : Bool,
) -> (
Array[@truetype.PdfTrueTypeTableRecord],
Array[@truetype.PdfTrueTypeSubsetTable],
) raise @core.PdfError {
let kept = pdf_truetype_subset_font_kept_tables(sorted_tables)
let header_size_reduction = 16 * (sorted_tables.length() - kept.length())
let records : Array[@truetype.PdfTrueTypeTableRecord] = []
let payloads : Array[@truetype.PdfTrueTypeSubsetTable] = []
let mut glyf_table_size_reduction = 0
let mut cmap_table_size_reduction = 0
for kept_record in kept {
let payload = pdf_truetype_subset_font_table_payload(
data, sorted_tables, kept_record, subset_codepoints, rewrite_cmap,
)
let mut table_length = kept_record.length
if kept_record.tag == "glyf" {
table_length = payload.length
glyf_table_size_reduction = pdf_truetype_table_padded_length(
kept_record.length,
) -
payload.bytes.length()
} else if kept_record.tag == "cmap" && rewrite_cmap {
table_length = payload.length
cmap_table_size_reduction = pdf_truetype_table_padded_length(
kept_record.length,
) -
payload.bytes.length()
}
let prior_table_reduction = if kept_record.tag == "glyf" {
cmap_table_size_reduction
} else if kept_record.tag == "cmap" {
glyf_table_size_reduction
} else {
glyf_table_size_reduction + cmap_table_size_reduction
}
records.push({
tag: kept_record.tag,
checksum: kept_record.checksum,
offset: kept_record.offset - header_size_reduction - prior_table_reduction,
length: table_length,
})
payloads.push(payload)
}
(records, payloads)
}
///|
/// Build a cpdf-style subset TrueType font file.
///
/// This ports the `subset_font` table-directory assembly around the already
/// ported `loca`, `glyf`, and format-6 `cmap` writers. Non-required TrueType
/// tables are removed, source checksums are preserved, table offsets are
/// adjusted by removed header/data spans and rewritten table sizes, and table
/// payloads are written in source table-order. Set `rewrite_cmap=true` for the
/// source `ImplicitInFontFile` higher-subset path; the default preserves the
/// original cmap table like the main subset path.
///
/// The `head` table is the source font's, bounding box included, as cpdf
/// writes it. Set `recompute_head=true` to write it as TTFunk (Prawn)
/// writes a subset's instead: the bounding box (`xMin`, `yMin`, `xMax`,
/// `yMax`) is the union of the bounding boxes in the headers of the glyphs
/// the subset keeps, simple and composite, every table's directory checksum
/// is recomputed from the subset's bytes, and `checkSumAdjustment` is set
/// so that the whole font sums to `0xB1B0AFBA`. A renderer may size its
/// glyph cache from the font's bounding box, so a subset carrying the full
/// font's box can rasterize differently from one carrying its own.
///
/// Only glyphs with an empty `loca` range are left out of the box. As an
/// exception kept for parity with TTFunk 1.7.0, a glyph that has data but
/// `numberOfContours == 0` still contributes the box stored in its header,
/// where OpenType's `head` box covers only glyphs with contours. The box is
/// `(0, 0, 0, 0)` when every kept glyph has an empty `loca` range. The
/// headers are trusted as stored: a box that underreports a glyph's
/// outline is not corrected.
pub fn pdf_truetype_subset_font(
data : BytesView,
subset_codepoints : ArrayView[Int],
rewrite_cmap? : Bool = false,
recompute_head? : Bool = false,
) -> @core.PdfBytes raise @core.PdfError {
let major = pdf_truetype_required_u16(data, 0, "font header")
let minor = pdf_truetype_required_u16(data, 2, "font header")
let sorted_tables = pdf_truetype_sorted_tables(pdf_truetype_tables(data))
let (records, payloads) = pdf_truetype_subset_font_directory(
data, sorted_tables, subset_codepoints, rewrite_cmap,
)
let table_count = records.length()
let search_power = pdf_truetype_power_two_leq(table_count)
let mut output_length = 12 + table_count * 16
for payload in payloads {
output_length += payload.bytes.length()
}
let output = Array::make(output_length, b'\x00')
let mut position = pdf_truetype_write_u16_at(output, 0, major)
position = pdf_truetype_write_u16_at(output, position, minor)
position = pdf_truetype_write_u16_at(output, position, table_count)
position = pdf_truetype_write_u16_at(output, position, 16 * search_power)
position = pdf_truetype_write_u16_at(
output,
position,
pdf_truetype_source_entry_selector(search_power),
)
position = pdf_truetype_write_u16_at(output, position, table_count * 16)
for record in records {
position = pdf_truetype_write_tag_at(output, position, record.tag)
position = pdf_truetype_write_u32_uint_at(output, position, record.checksum)
position = pdf_truetype_write_u32_at(output, position, record.offset)
position = pdf_truetype_write_u32_at(output, position, record.length)
}
for payload in payloads {
position = pdf_truetype_write_view_at(output, position, payload.bytes)
}
if recompute_head {
pdf_truetype_recompute_head(Bytes::from_array(output))
} else {
Bytes::from_array(output)
}
}
///|
/// The checksum of `length` bytes of a font from `offset` (OpenType `head`
/// table, "Calculating Checksums"): the sum, modulo 2^32, of its big-endian
/// 32-bit words, a short last word padded with zeros.
fn pdf_truetype_checksum(
data : ArrayView[Byte],
offset : Int,
length : Int,
) -> UInt {
let end = offset + length
let mut sum = 0U
for word_start = offset; word_start < end; word_start = word_start + 4 {
let mut word = 0U
for index in word_start..<(word_start + 4) {
let byte = if index < end && index < data.length() {
data[index].to_uint()
} else {
0U
}
word = (word << 8) | byte
}
sum = sum + word
}
sum
}
///|
/// The union of the bounding boxes in the headers of a font's glyphs
/// (`xMin`, `yMin`, `xMax`, `yMax`), simple and composite alike, as TTFunk
/// 1.7.0 aggregates them: only a glyph with a zero-length `loca` range is
/// skipped, so a glyph with data but no contours still counts its stored
/// header box (OpenType would leave it out). A font whose glyphs are all
/// empty has the box `(0, 0, 0, 0)`.
fn pdf_truetype_glyph_bounds(
data : BytesView,
) -> (Int, Int, Int, Int) raise @core.PdfError {
let glyf = pdf_truetype_required_table(data, "glyf")
let offsets = pdf_truetype_loca_offsets(data)
let mut bounds : (Int, Int, Int, Int)? = None
for index in 0..<(offsets.length() - 1) {
let start = offsets[index]
if offsets[index + 1] <= start {
continue
}
let at = glyf.offset + start
let x_min = pdf_truetype_required_i16(data, at + 2, "glyf table")
let y_min = pdf_truetype_required_i16(data, at + 4, "glyf table")
let x_max = pdf_truetype_required_i16(data, at + 6, "glyf table")
let y_max = pdf_truetype_required_i16(data, at + 8, "glyf table")
bounds = Some(
match bounds {
None => (x_min, y_min, x_max, y_max)
Some((left, bottom, right, top)) =>
(left.min(x_min), bottom.min(y_min), right.max(x_max), top.max(y_max))
},
)
}
bounds.unwrap_or((0, 0, 0, 0))
}
///|
/// Rewrite a font's `head` table as TTFunk's subsetter writes a subset's:
/// the bounding box is the union of the glyphs the font has outlines for,
/// every table's directory checksum is recomputed, and `checkSumAdjustment`
/// makes the whole font sum to `0xB1B0AFBA`.
fn pdf_truetype_recompute_head(
font : Bytes,
) -> @core.PdfBytes raise @core.PdfError {
let head = pdf_truetype_required_table(font, "head")
if head.length < 54 || head.offset + 54 > font.length() {
raise SoftError("Truncated TrueType head table")
}
let (x_min, y_min, x_max, y_max) = pdf_truetype_glyph_bounds(font)
let output = font.to_array()
// checkSumAdjustment is zero while the checksums are taken
ignore(pdf_truetype_write_u32_uint_at(output, head.offset + 8, 0U))
for index, value in [x_min, y_min, x_max, y_max] {
ignore(
pdf_truetype_write_u16_at(
output,
head.offset + 36 + index * 2,
value & 0xFFFF,
),
)
}
for index, record in pdf_truetype_tables(font) {
ignore(
pdf_truetype_write_u32_uint_at(
output,
12 + index * 16 + 4,
pdf_truetype_checksum(output, record.offset, record.length),
),
)
}
ignore(
pdf_truetype_write_u32_uint_at(
output,
head.offset + 8,
0xB1B0AFBAU - pdf_truetype_checksum(output, 0, output.length()),
),
)
Bytes::from_array(output)
}