///|
pub using @core {type PdfBytes, type PdfError}
///|
/// Parsed Adobe Font Metrics data used by standard-font helpers.
///
/// Headers preserve raw key/value bytes. Character metrics store character
/// codes and widths, kerns store pair adjustments, and `char_name_widths`
/// preserves name-based widths for encodings that refer to glyph names.
pub(all) struct PdfAfmData {
headers : Array[(PdfBytes, PdfBytes)]
char_metrics : Array[(Int, Int)]
kerns : Array[(Int, Int, Int)]
char_name_widths : Array[(PdfBytes, Int)]
} derive(Debug, Eq, ToJson)
///|
/// Return whether two byte views contain the same bytes.
pub fn pdf_afm_view_equals(left : BytesView, right : BytesView) -> Bool {
if left.length() != right.length() {
false
} else {
for index in 0.. Bool {
let bytes = @ascii.encode(text)
pdf_afm_view_equals(view, bytes)
}
///|
fn pdf_afm_line_starts_with_ascii(line : BytesView, text : String) -> Bool {
let prefix = @ascii.encode(text)
if line.length() < prefix.length() {
false
} else {
pdf_afm_view_equals(line[:prefix.length()], prefix)
}
}
///|
fn pdf_afm_trim_line_end(line : BytesView) -> BytesView {
let mut end = line.length()
while end > 0 &&
(line[end - 1].to_int() == 0x0A || line[end - 1].to_int() == 0x0D) {
end -= 1
}
line[:end]
}
///|
/// Parse a signed AFM integer token.
pub fn pdf_afm_parse_int(token : BytesView) -> Int raise PdfError {
if token.length() == 0 {
raise NumberExpected
}
let mut sign = 1
let mut index = 0
if token[0].to_int() == 0x2D {
sign = -1
index = 1
} else if token[0].to_int() == 0x2B {
index = 1
}
if index >= token.length() {
raise NumberExpected
}
let mut value = 0
while index < token.length() {
let byte = token[index].to_int()
if !@core.pdf_is_digit_byte(byte) {
raise NumberExpected
}
value = value * 10 + byte - 0x30
index += 1
}
sign * value
}
///|
fn pdf_afm_header_entry(line : BytesView) -> (PdfBytes, PdfBytes)? {
let trimmed = pdf_afm_trim_line_end(line)
let mut index = 0
while index < trimmed.length() &&
!@core.pdf_is_whitespace_byte(trimmed[index].to_int()) {
index += 1
}
if index == 0 {
None
} else {
let key = trimmed[:index].to_owned()
while index < trimmed.length() &&
@core.pdf_is_whitespace_byte(trimmed[index].to_int()) {
index += 1
}
Some((key, trimmed[index:].to_owned()))
}
}
///|
fn pdf_afm_section_lines(
lines : ArrayView[BytesView],
start_marker : String,
end_marker : String,
) -> Array[BytesView] {
let output : Array[BytesView] = Array(capacity=lines.length())
let mut collecting = false
let mut done = false
for line in lines {
if !done {
if collecting {
if pdf_afm_line_starts_with_ascii(line, end_marker) {
done = true
} else {
output.push(line)
}
} else if pdf_afm_line_starts_with_ascii(line, start_marker) {
collecting = true
}
}
}
output
}
///|
fn pdf_afm_char_metric(line : BytesView) -> (PdfBytes, Int, Int) raise PdfError {
let tokens = @core.pdf_split_whitespace_tokens(line)
if tokens.length() >= 7 &&
pdf_afm_view_equals_ascii(tokens[0], "C") &&
pdf_afm_view_equals_ascii(tokens[2], ";") &&
pdf_afm_view_equals_ascii(tokens[3], "WX") &&
pdf_afm_view_equals_ascii(tokens[5], ";") &&
pdf_afm_view_equals_ascii(tokens[6], "N") {
let char_number = pdf_afm_parse_int(tokens[1])
let width = pdf_afm_parse_int(tokens[4])
if tokens.length() < 8 {
raise AfmExpected
}
(tokens[7].to_owned(), char_number, width)
} else {
raise AfmExpected
}
}
///|
fn pdf_afm_lookup_char_number(
metrics : ArrayView[(PdfBytes, Int, Int)],
name : BytesView,
) -> Int raise PdfError {
for metric in metrics {
let (candidate, char_number, _) = metric
if pdf_afm_view_equals(candidate, name) {
break char_number
}
} nobreak {
raise AfmExpected
}
}
///|
fn pdf_afm_kern(
line : BytesView,
metrics : ArrayView[(PdfBytes, Int, Int)],
) -> (Int, Int, Int)? raise PdfError {
let tokens = @core.pdf_split_whitespace_tokens(line)
if tokens.length() >= 4 && pdf_afm_view_equals_ascii(tokens[0], "KPX") {
let first = pdf_afm_lookup_char_number(metrics, tokens[1])
let second = pdf_afm_lookup_char_number(metrics, tokens[2])
let kern = pdf_afm_parse_int(tokens[3])
if first > -1 && second > -1 {
Some((first, second, kern))
} else {
None
}
} else {
raise AfmExpected
}
}
///|
/// Parses Adobe Font Metrics data from bytes.
///
/// The parser extracts top-level headers, character widths, name-width pairs,
/// and kerning pairs. Raises `PdfError::AfmExpected` or
/// `PdfError::NumberExpected` for malformed metric sections.
pub fn pdf_read_afm(data : BytesView) -> PdfAfmData raise PdfError {
let lines = @core.byte_cursor_of_view(data).read_line_views()
let headers : Array[(PdfBytes, PdfBytes)] = Array(capacity=lines.length())
for line in lines {
if pdf_afm_line_starts_with_ascii(line, "C ") {
break
}
match pdf_afm_header_entry(line) {
Some(entry) => headers.push(entry)
None => ()
}
}
let metric_lines = pdf_afm_section_lines(
lines, "StartCharMetrics", "EndCharMetrics",
)
let raw_metrics : Array[(PdfBytes, Int, Int)] = Array(
capacity=metric_lines.length(),
)
for line in metric_lines {
if pdf_afm_trim_line_end(line).length() >= 5 {
raw_metrics.push(pdf_afm_char_metric(line))
}
}
let char_metrics : Array[(Int, Int)] = Array(capacity=raw_metrics.length())
let char_name_widths : Array[(PdfBytes, Int)] = Array(
capacity=raw_metrics.length(),
)
for metric in raw_metrics {
let (name, char_number, width) = metric
if char_number > -1 {
char_metrics.push((char_number, width))
}
char_name_widths.push((name, width))
}
let kern_lines = pdf_afm_section_lines(
lines, "StartKernPairs", "EndKernPairs",
)
let kerns : Array[(Int, Int, Int)] = Array(capacity=kern_lines.length())
for line in kern_lines {
if pdf_afm_trim_line_end(line).length() >= 5 {
match pdf_afm_kern(line, raw_metrics) {
Some(kern) => kerns.push(kern)
None => ()
}
}
}
{ headers, char_metrics, kerns, char_name_widths, }
}