///|
fn pdf_addtext_replace_all_lazy(
text : String,
search : String,
replacement : () -> String,
) -> String {
let search_bytes = @utf8.encode(search)
guard search_bytes.length() > 0 else { return text }
let text_bytes = @utf8.encode(text)
let output : Array[Byte] = Array(capacity=text_bytes.length())
let mut replaced = false
for index = 0; index < text_bytes.length(); {
if text_bytes[index:].has_prefix(search_bytes) {
for byte in @utf8.encode(replacement()) {
output.push(byte)
}
replaced = true
continue index + search_bytes.length()
}
output.push(text_bytes[index])
continue index + 1
}
if replaced {
@utf8.decode_lossy(Bytes::from_array(output))
} else {
text
}
}
///|
/// Apply cpdf add-text replacements, then run the cpdf strftime pass.
///
/// Replacement callbacks are only evaluated for markers that occur in `text`,
/// matching cpdf's `string_replace_all_lazy` helper.
pub fn pdf_addtext_process_text_with_time(
time : @date.PdfStrftimeTime,
text : String,
replacements : ArrayView[(String, () -> String)],
) -> String {
let mut output = text
for replacement in replacements {
output = pdf_addtext_replace_all_lazy(output, replacement.0, replacement.1)
}
@date.pdf_strftime_with_time(time, output)
}
///|
/// Compatibility wrapper matching cpdfaddtext's `process_text`.
pub fn pdf_process_text(
time : @date.PdfStrftimeTime,
text : String,
replacements : ArrayView[(String, () -> String)],
) -> String {
pdf_addtext_process_text_with_time(time, text, replacements)
}
///|
/// Split add-text input at cpdf's literal `\n` line marker.
///
/// A doubled backslash before `n` is preserved as text, matching cpdf's
/// `split_at_newline` helper.
pub fn pdf_addtext_split_at_newline(text : String) -> Array[String] {
let bytes = @utf8.encode(text)
let lines : Array[String] = Array(capacity=bytes.length())
let mut line_start = 0
for index = 0; index < bytes.length(); {
match bytes[index:] {
[b'\\', b'\\', b'n', ..] => continue index + 3
[b'\\', b'n', ..] => {
lines.push(@utf8.decode_lossy(bytes[line_start:index].to_owned()))
line_start = index + 2
continue index + 2
}
_ => continue index + 1
}
}
lines.push(@utf8.decode_lossy(bytes[line_start:].to_owned()))
lines
}
///|
fn pdf_addtext_octal_value(a : Byte, b : Byte, c : Byte) -> Int {
(a.to_int() - '0') * 64 + (b.to_int() - '0') * 8 + c.to_int() - '0'
}
///|
/// Decode cpdf add-text backslash escapes after line splitting.
///
/// Three-digit octal escapes emit one byte, doubled backslashes emit one
/// backslash, and other non-newline escapes drop the backslash.
pub fn pdf_addtext_unescape_string(
text : String,
) -> String raise @core.PdfError {
let bytes = @utf8.encode(text)
let output : Array[Byte] = Array(capacity=bytes.length())
for index = 0; index < bytes.length(); {
match bytes[index:] {
[b'\\', b'0'..=b'7' as a, b'0'..=b'7' as b, b'0'..=b'7' as c, ..] => {
let value = pdf_addtext_octal_value(a, b, c)
if value > 255 {
raise SoftError("bad add-text octal escape")
}
output.push(value.to_byte())
continue index + 4
}
[b'\\', b'\\', ..] => {
output.push(b'\\')
continue index + 2
}
[b'\\', next, ..] if next != b'n' => {
output.push(next)
continue index + 2
}
[byte, ..] => {
output.push(byte)
continue index + 1
}
[] => break
}
}
@utf8.decode_lossy(Bytes::from_array(output))
}
///|
/// Split and unescape add-text lines, reversing them for upside-down rotations.
pub fn pdf_addtext_lines_for_rotation(
text : String,
rotation : @addtext.PdfAddTextRotation,
) -> Array[String] raise @core.PdfError {
let lines = pdf_addtext_split_at_newline(text).map(line => {
pdf_addtext_unescape_string(line)
})
match rotation {
PdfAddTextRot180 | PdfAddTextRot270 => lines.rev()
PdfAddTextRot0 | PdfAddTextRot90 => lines
}
}