///|
/// Text rotation values used by cpdf's add-text positioning helpers.
pub(all) enum PdfAddTextRotation {
PdfAddTextRot0
PdfAddTextRot90
PdfAddTextRot180
PdfAddTextRot270
} derive(Debug, Eq, ToJson)
///|
/// Text justification values used by cpdf's add-text positioning helpers.
pub(all) enum PdfAddTextJustification {
PdfAddTextLeftJustify
PdfAddTextCentreJustify
PdfAddTextRightJustify
} derive(Debug, Eq, ToJson)
///|
/// Font source options for cpdf add-text.
///
/// This ports the source-level non-embedding `Cpdfembed.cpdffont` variants.
/// TrueType `EmbedInfo` remains a separate embedding slice.
pub(all) enum PdfAddTextFontSource {
PdfAddTextPreMadeFontPack(PdfFontPack)
PdfAddTextExistingNamedFont
} derive(Debug, Eq, ToJson)
///|
/// A URL span parsed from cpdf add-text `%URL[text|url]` markup.
///
/// `start` and `end` are byte offsets in the visible output line, matching
/// OCaml cpdf's `String.length` accounting.
pub(all) struct PdfAddTextUrl {
url : String
start : Int
end : Int
} derive(Debug, Eq, ToJson)
///|
/// One add-text line after stripping `%URL[text|url]` markup.
pub(all) struct PdfAddTextUrlLine {
text : String
urls : Array[PdfAddTextUrl]
} derive(Debug, Eq, ToJson)
///|
/// Transformed geometry for one add-text URL annotation.
pub(all) struct PdfAddTextUrlAnnotationGeometry {
p0 : Point2
p1 : Point2
p2 : Point2
p3 : Point2
rectangle : PdfRectangle
} derive(Debug, Eq, ToJson)
///|
/// Resolved placement values for one cpdf add-text line.
pub(all) struct PdfAddTextLinePlacement {
origin : Point2
base_rotation : Double
text_rotation : Double
rotation_offset : Point2
horizontal_offset : Double
vertical_offset : Double
justification_offset : Double
} derive(Debug, Eq, ToJson)
///|
/// Width measurements for one cpdf add-text line and its expanded paragraph.
pub(all) struct PdfAddTextLineWidths {
text_width : Double
widths : Array[Double]
longest_width : Double
} derive(Debug, Eq, ToJson)
///|
/// One cpdf add-text line after replacement expansion, URL stripping, width
/// measurement, and placement resolution.
pub(all) struct PdfAddTextPreparedLine {
text : String
text_bytes : PdfBytes
urls : Array[PdfAddTextUrl]
expanded_lines : Array[String]
expanded_line_bytes : Array[PdfBytes]
widths : PdfAddTextLineWidths
placement : PdfAddTextLinePlacement
} derive(Debug, Eq, ToJson)
///|
/// Page resources prepared for one cpdf add-text line.
pub(all) struct PdfAddTextResourceSetup {
resources : PdfObject
font_dictionary : PdfObject
unique_fontname : PdfName
unique_fontnames : Array[PdfName]
extgstate_name : PdfName?
} derive(Debug, Eq, ToJson)
///|
/// Values used to build cpdf add-text replacement callbacks.
pub(all) struct PdfAddTextReplacementValues {
page_number : Int
end_page : Int
filename : String
page_label : String
end_label : String
bates : Int
bates_pad : Int?
bookmarks : Array[String]
} derive(Debug, Eq, ToJson)
///|
let pdf_addtext_cpdf_stamp_name : PdfName = pdf_name_of_bytes(
@ascii.encode("/CPDFSTAMP"),
)
///|
let pdf_addtext_extgstate_key_name : PdfName = pdf_name_of_bytes(
@ascii.encode("/ExtGState"),
)
///|
let pdf_addtext_font_key_name : PdfName = pdf_name_of_bytes(
@ascii.encode("/Font"),
)
///|
let pdf_addtext_annots_key_name : PdfName = pdf_name_of_bytes(
@ascii.encode("/Annots"),
)
///|
let pdf_addtext_ca_key_name : PdfName = pdf_name_of_bytes(@ascii.encode("/ca"))
///|
let pdf_addtext_ca_stroke_key_name : PdfName = pdf_name_of_bytes(
@ascii.encode("/CA"),
)
///|
let pdf_addtext_subtype_key_name : PdfName = pdf_name_of_bytes(
@ascii.encode("/Subtype"),
)
///|
let pdf_addtext_link_name : PdfName = pdf_name_of_bytes(@ascii.encode("/Link"))
///|
let pdf_addtext_quadpoints_key_name : PdfName = pdf_name_of_bytes(
@ascii.encode("/QuadPoints"),
)
///|
let pdf_addtext_rect_key_name : PdfName = pdf_name_of_bytes(
@ascii.encode("/Rect"),
)
///|
let pdf_addtext_bs_key_name : PdfName = pdf_name_of_bytes(@ascii.encode("/BS"))
///|
let pdf_addtext_w_key_name : PdfName = pdf_name_of_bytes(@ascii.encode("/W"))
///|
let pdf_addtext_a_key_name : PdfName = pdf_name_of_bytes(@ascii.encode("/A"))
///|
let pdf_addtext_uri_key_name : PdfName = pdf_name_of_bytes(
@ascii.encode("/URI"),
)
///|
let pdf_addtext_type_key_name : PdfName = pdf_name_of_bytes(
@ascii.encode("/Type"),
)
///|
let pdf_addtext_action_name : PdfName = pdf_name_of_bytes(
@ascii.encode("/Action"),
)
///|
let pdf_addtext_s_key_name : PdfName = pdf_name_of_bytes(@ascii.encode("/S"))
///|
fn pdf_addtext_cpdf_stamp() -> PdfName {
pdf_addtext_cpdf_stamp_name
}
///|
fn pdf_addtext_extgstate_key() -> PdfName {
pdf_addtext_extgstate_key_name
}
///|
fn pdf_addtext_font_key() -> PdfName {
pdf_addtext_font_key_name
}
///|
fn pdf_addtext_annots_key() -> PdfName {
pdf_addtext_annots_key_name
}
///|
fn pdf_addtext_ca_key() -> PdfName {
pdf_addtext_ca_key_name
}
///|
fn pdf_addtext_ca_stroke_key() -> PdfName {
pdf_addtext_ca_stroke_key_name
}
///|
fn pdf_addtext_validate_page_numbers(
page_numbers : ArrayView[Int],
end_page : Int,
context : String,
) -> Unit raise PdfError {
for page_number in page_numbers {
if page_number < 1 || page_number > end_page {
raise BadPageSpecification(
context + ": range contains pages not present in pdf",
)
}
}
}
///|
fn PdfDocument::pdf_addtext_opacity_resources(
self : PdfDocument,
resources : PdfObject,
opacity : Double,
) -> (PdfObject, PdfName?) raise PdfError {
if opacity < 1.0 {
let extgstate = match
self.lookup_direct(pdf_addtext_extgstate_key(), resources) {
Some(dictionary) => dictionary
None => PdfDictionary([])
}
let name = extgstate.unique_key("gs")
let opacity_dictionary = PdfDictionary([
(pdf_addtext_ca_key(), PdfReal(opacity)),
(pdf_addtext_ca_stroke_key(), PdfReal(opacity)),
])
(
resources.add_dict_entry(
pdf_addtext_extgstate_key(),
extgstate.add_dict_entry(name, opacity_dictionary),
),
Some(name),
)
} else {
(resources, None)
}
}
///|
/// Return resources updated with cpdf add-text opacity graphics state.
///
/// Opacity values below `1.0` allocate a unique `/gs*` entry under
/// `/ExtGState` with both fill and stroke alpha set. Opaque values leave the
/// resource dictionary unchanged and return no graphics-state name.
pub fn PdfDocument::addtext_opacity_resources(
self : PdfDocument,
resources : PdfObject,
opacity : Double,
) -> (PdfObject, PdfName?) raise PdfError {
self.pdf_addtext_opacity_resources(resources, opacity)
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_opacity_resources`.
pub fn pdf_addtext_opacity_resources(
document : PdfDocument,
resources : PdfObject,
opacity : Double,
) -> (PdfObject, PdfName?) raise PdfError {
document.addtext_opacity_resources(resources, opacity)
}
///|
/// Build the cpdf add-text fill colour operator.
pub fn pdf_addtext_colour_op(colour : PdfContentColour) -> PdfContentOp {
colour.pdf_tweak_fill_op()
}
///|
/// Source-spelled compatibility wrapper for cpdfaddtext's `colour_op`.
pub fn pdf_colour_op(colour : PdfContentColour) -> PdfContentOp {
pdf_addtext_colour_op(colour)
}
///|
/// Build the cpdf add-text stroke colour operator.
pub fn pdf_addtext_colour_op_stroke(colour : PdfContentColour) -> PdfContentOp {
colour.pdf_tweak_stroke_op()
}
///|
/// Source-spelled compatibility wrapper for cpdfaddtext's `colour_op_stroke`.
pub fn pdf_colour_op_stroke(colour : PdfContentColour) -> PdfContentOp {
pdf_addtext_colour_op_stroke(colour)
}
///|
/// Convert UTF-8 add-text input to PDF charcode bytes for `font`.
///
/// Codepoints not representable by the font are skipped, matching cpdf's
/// `option_map` behavior in `charcodes_of_utf8`.
pub fn pdf_addtext_charcodes_of_utf8(
font : PdfFont,
text : String,
) -> PdfBytes raise PdfError {
let extractor = pdf_charcode_extractor_of_font_real(font)
let codepoints = pdf_codepoints_of_utf8(@utf8.encode(text))
let mut length = 0
for codepoint in codepoints {
match extractor(codepoint) {
Some(_) => length += 1
None => ()
}
}
let output = Array::make(length, b'\x00')
let mut position = 0
for codepoint in codepoints {
match extractor(codepoint) {
Some(charcode) => {
output[position] = pdf_byte_of_int(charcode)
position += 1
}
None => ()
}
}
Bytes::from_array(output)
}
///|
/// Measure cpdf add-text charcode bytes with simple-font metrics.
///
/// This mirrors cpdf's narrow `width_of_text` helper: only simple fonts with
/// explicit `fontmetrics` are measured, and any missing metric makes the whole
/// result `0`.
pub fn pdf_addtext_width_of_text(font : PdfFont, text : BytesView) -> Double {
match font {
PdfFontSimple({ fontmetrics: Some(metrics), .. }) => {
let mut width = 0.0
for byte in text {
let code = byte.to_int()
if code >= metrics.length() {
return 0.0
}
width += metrics[code]
}
width
}
_ => 0.0
}
}
///|
/// Measure add-text bytes with the selected parsed font at `font_size`.
pub fn pdf_addtext_width_of_selected_font(
font : PdfFont,
font_size : Double,
text : BytesView,
) -> Double {
let raw_width = match font {
PdfFontStandard({ font, .. }) =>
font.text_width(PdfImplicitInFontFile, text).to_double()
_ => pdf_addtext_width_of_text(font, text)
}
raw_width * font_size / 1000.0
}
///|
/// Measure UTF-8 add-text with a cpdf font pack at `font_size`.
pub fn pdf_addtext_width_of_fontpack(
fontpack : PdfFontPack,
font_size : Double,
text : String,
) -> Double raise PdfError {
let width_tables : Array[Array[Double]] = Array::new(
capacity=fontpack.fonts.length(),
)
for font in fontpack.fonts {
width_tables.push(pdf_type_font_widths(font, font_size))
}
let mut width = 0.0
for codepoint in pdf_codepoints_of_utf8(@utf8.encode(text)) {
match fontpack.get_char(codepoint) {
Some((charcode, font_index, _)) => {
let widths = width_tables[font_index]
if charcode < 0 || charcode >= widths.length() {
raise FontWidthExpected
}
width += widths[charcode]
}
None => ()
}
}
width
}
///|
/// Measure text bytes with cpdf add-text's selected font mode.
///
/// This ports the local `calc_textwidth` branch in `cpdfaddtext.ml`. Font packs
/// interpret `text` as UTF-8 so codepoints can select pack members; selected
/// parsed fonts and existing page fonts treat `text` as the PDF text bytes to
/// measure.
pub fn PdfDocument::addtext_calc_textwidth(
self : PdfDocument,
page : PdfPage,
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_size : Double,
text : BytesView,
) -> Double raise PdfError {
match fontpack {
Some(fontpack) =>
pdf_addtext_width_of_fontpack(
fontpack,
font_size,
pdf_addtext_utf8_string(text),
)
None =>
match font {
Some(font) => pdf_addtext_width_of_selected_font(font, font_size, text)
None => {
let (_, width) = self.addtext_width_of_existing_font_resource(
page, fontname, font_size, text,
)
width
}
}
}
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_calc_textwidth`.
pub fn pdf_addtext_calc_textwidth(
document : PdfDocument,
page : PdfPage,
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_size : Double,
text : BytesView,
) -> Double raise PdfError {
document.addtext_calc_textwidth(
page, font, fontpack, fontname, font_size, text,
)
}
///|
/// Measure cpdf add-text's current line and expanded paragraph widths.
///
/// This ports the local `textwidth`, `allwidths`, and `longest_w` block in
/// `cpdfaddtext.ml` after replacement expansion and URL stripping.
pub fn PdfDocument::addtext_line_widths(
self : PdfDocument,
page : PdfPage,
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_size : Double,
text : BytesView,
expanded_lines : ArrayView[Bytes],
) -> PdfAddTextLineWidths raise PdfError {
let text_width = self.addtext_calc_textwidth(
page, font, fontpack, fontname, font_size, text,
)
let widths : Array[Double] = Array::new(capacity=expanded_lines.length())
for line in expanded_lines {
widths.push(
self.addtext_calc_textwidth(
page, font, fontpack, fontname, font_size, line,
),
)
}
{ text_width, widths, longest_width: pdf_addtext_longest_width(widths) }
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_line_widths`.
pub fn pdf_addtext_line_widths(
document : PdfDocument,
page : PdfPage,
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_size : Double,
text : BytesView,
expanded_lines : ArrayView[Bytes],
) -> PdfAddTextLineWidths raise PdfError {
document.addtext_line_widths(
page, font, fontpack, fontname, font_size, text, expanded_lines,
)
}
///|
/// Return the byte string cpdf add-text uses for measuring a visible line.
///
/// Raw mode and font-pack mode keep UTF-8 bytes. Selected simple-font mode
/// converts Unicode text to PDF character codes first, matching cpdf's
/// non-raw line measurement path.
pub fn pdf_addtext_measurement_bytes(
font : PdfFont?,
fontpack : PdfFontPack?,
raw : Bool,
text : String,
) -> PdfBytes raise PdfError {
if raw || fontpack is Some(_) {
@utf8.encode(text)
} else {
match font {
Some(font) => pdf_addtext_charcodes_of_utf8(font, text)
None => @utf8.encode(text)
}
}
}
///|
fn PdfDocument::pdf_addtext_measurement_bytes_for_page(
self : PdfDocument,
page : PdfPage,
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
raw : Bool,
text : String,
) -> PdfBytes raise PdfError {
if raw || fontpack is Some(_) {
@utf8.encode(text)
} else {
match font {
Some(font) => pdf_addtext_charcodes_of_utf8(font, text)
None => {
let (_, font_object) = self.addtext_existing_font_resource(
page, fontname,
)
pdf_addtext_charcodes_of_utf8(self.read_font(font_object), text)
}
}
}
}
///|
/// Prepare one cpdf add-text line for later content and annotation emission.
///
/// This ports the source-level block which expands page-local replacement
/// fields, strips `%URL[text|url]` markup, measures the current and paragraph
/// line widths, and resolves the final cpdf placement offsets.
pub fn PdfDocument::addtext_prepare_line(
self : PdfDocument,
page : PdfPage,
time : PdfStrftimeTime,
lines : ArrayView[String],
replacements : ArrayView[(String, () -> String)],
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_size : Double,
text : String,
position : PdfPosition,
relative_to_box? : PdfName = pdf_page_mediabox_key(),
horizontal_offset? : Double = 0.0,
vertical_offset? : Double = 0.0,
justification? : PdfAddTextJustification = PdfAddTextLeftJustify,
rotation? : PdfAddTextRotation = PdfAddTextRot0,
raw? : Bool = false,
) -> PdfAddTextPreparedLine raise PdfError {
let processed = pdf_addtext_process_text_with_time(time, text, replacements)
let parsed = pdf_addtext_get_urls_line(processed)
let expanded_lines = pdf_addtext_expand_lines_with_time(
time, lines, replacements,
)
let text_bytes = self.pdf_addtext_measurement_bytes_for_page(
page,
font,
fontpack,
fontname,
raw,
parsed.text,
)
let expanded_line_bytes : Array[PdfBytes] = Array::new(
capacity=expanded_lines.length(),
)
for line in expanded_lines {
expanded_line_bytes.push(
self.pdf_addtext_measurement_bytes_for_page(
page, font, fontpack, fontname, raw, line,
),
)
}
let widths = self.addtext_line_widths(
page, font, fontpack, fontname, font_size, text_bytes, expanded_line_bytes,
)
let bounds = self.pdf_addtext_rectangle_bounds(page, relative_to_box)
let placement = pdf_addtext_line_placement(
bounds,
position,
rotation,
widths.text_width,
widths.longest_width,
horizontal_offset,
vertical_offset,
justification,
)
{
text: parsed.text,
text_bytes,
urls: parsed.urls,
expanded_lines,
expanded_line_bytes,
widths,
placement,
}
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_prepare_line`.
pub fn pdf_addtext_prepare_line(
document : PdfDocument,
page : PdfPage,
time : PdfStrftimeTime,
lines : ArrayView[String],
replacements : ArrayView[(String, () -> String)],
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_size : Double,
text : String,
position : PdfPosition,
relative_to_box? : PdfName = pdf_page_mediabox_key(),
horizontal_offset? : Double = 0.0,
vertical_offset? : Double = 0.0,
justification? : PdfAddTextJustification = PdfAddTextLeftJustify,
rotation? : PdfAddTextRotation = PdfAddTextRot0,
raw? : Bool = false,
) -> PdfAddTextPreparedLine raise PdfError {
document.addtext_prepare_line(
page,
time,
lines,
replacements,
font,
fontpack,
fontname,
font_size,
text,
position,
relative_to_box~,
horizontal_offset~,
vertical_offset~,
justification~,
rotation~,
raw~,
)
}
///|
/// Return the widest add-text line width.
pub fn pdf_addtext_longest_width(
widths : ArrayView[Double],
) -> Double raise PdfError {
if widths.length() == 0 {
raise SoftError("empty add-text width list")
}
let mut longest = widths[0]
for index in 1.. longest {
longest = widths[index]
}
}
longest
}
///|
/// Build cpdf add-rectangle content operators after placement is resolved.
pub fn pdf_addtext_rectangle_ops(
x : Double,
y : Double,
width : Double,
height : Double,
colour : PdfContentColour,
outline : Bool,
line_width : Double,
extgstate_name : PdfName?,
) -> Array[PdfContentOp] {
let ops = [
pdf_content_begin_artifact(),
Op_q,
Op_BMC(pdf_addtext_cpdf_stamp()),
pdf_addtext_colour_op(colour),
pdf_addtext_colour_op_stroke(colour),
]
if outline {
ops.push(Op_w(line_width))
}
match extgstate_name {
Some(name) => ops.push(Op_gs(name))
None => ()
}
ops.push(Op_re(x, y, width, height))
ops.push(if outline { Op_s } else { Op_f })
ops.push(Op_EMC)
ops.push(Op_Q)
ops.push(pdf_content_end_artifact())
ops
}
///|
fn pdf_addtext_fontpack_text_ops(
fontpack : PdfFontPack,
unique_fontnames : ArrayView[PdfName],
font_size : Double,
text : String,
) -> Array[PdfContentOp] raise PdfError {
let codepoints = pdf_codepoints_of_utf8(@utf8.encode(text))
let triples : Array[(Int, Int, PdfFont)] = Array::new(
capacity=codepoints.length(),
)
for codepoint in codepoints {
match fontpack.get_char(codepoint) {
Some(triple) => triples.push(triple)
None => ()
}
}
let ops : Array[PdfContentOp] = Array::new(capacity=triples.length() * 2)
for run in pdf_fontpack_collate_runs(triples) {
guard run.length() > 0 else { continue }
let font_index = run[0].1
if font_index < 0 || font_index >= unique_fontnames.length() {
raise SoftError("bad add-text fontpack font name list")
}
let bytes = Array::make(run.length(), b'\x00')
for index, triple in run {
bytes[index] = pdf_byte_of_int(triple.0)
}
ops.push(Op_Tf(unique_fontnames[font_index], font_size))
ops.push(Op_Tj(Bytes::from_array(bytes)))
}
ops
}
///|
fn pdf_addtext_font_text_ops_with_bytes(
fontpack : PdfFontPack?,
unique_fontname : PdfName,
unique_fontnames : ArrayView[PdfName],
font_size : Double,
text : String,
text_bytes : PdfBytes,
) -> Array[PdfContentOp] raise PdfError {
match fontpack {
Some(fontpack) =>
pdf_addtext_fontpack_text_ops(fontpack, unique_fontnames, font_size, text)
None => [Op_Tf(unique_fontname, font_size), Op_Tj(text_bytes)]
}
}
///|
fn pdf_addtext_text_matrix(
x : Double,
y : Double,
base_rotation : Double,
horizontal_offset : Double,
vertical_offset : Double,
text_rotation : Double,
rotation_h_offset : Double,
rotation_v_offset : Double,
) -> TransformMatrix {
transform_matrix_of_transform([
Translate(
x - horizontal_offset + rotation_h_offset,
y - vertical_offset + rotation_v_offset,
),
Rotate(point2(0.0, 0.0), base_rotation + text_rotation),
])
}
///|
fn pdf_addtext_text_ops_with_bytes(
fontpack : PdfFontPack?,
x : Double,
y : Double,
base_rotation : Double,
horizontal_offset : Double,
vertical_offset : Double,
outline : Bool,
line_width : Double,
unique_fontname : PdfName,
unique_fontnames : ArrayView[PdfName],
extgstate_name : PdfName?,
colour : PdfContentColour,
font_size : Double,
text : String,
text_bytes : PdfBytes,
text_rotation : Double,
rotation_h_offset : Double,
rotation_v_offset : Double,
) -> Array[PdfContentOp] raise PdfError {
let textops = pdf_addtext_font_text_ops_with_bytes(
fontpack, unique_fontname, unique_fontnames, font_size, text, text_bytes,
)
let ops = [
pdf_content_begin_artifact(),
Op_q,
Op_BMC(pdf_addtext_cpdf_stamp()),
Op_cm(
pdf_addtext_text_matrix(
x, y, base_rotation, horizontal_offset, vertical_offset, text_rotation, rotation_h_offset,
rotation_v_offset,
),
),
Op_BT,
]
if outline {
ops.push(Op_w(line_width))
ops.push(Op_Tr(1))
} else {
ops.push(Op_Tr(0))
}
ops.push(pdf_addtext_colour_op(colour))
ops.push(pdf_addtext_colour_op_stroke(colour))
match extgstate_name {
Some(name) => ops.push(Op_gs(name))
None => ()
}
for op in textops {
ops.push(op)
}
ops.push(Op_ET)
ops.push(Op_EMC)
ops.push(Op_Q)
ops.push(pdf_content_end_artifact())
ops
}
///|
/// Build cpdf add-text text-stamp content operators after placement is resolved.
///
/// This ports the source-level `cpdfaddtext.ml` `ops` helper, including
/// `/CPDFSTAMP` artifact wrapping, placement matrix, outline rendering mode,
/// colour operators, optional opacity graphics state, simple-font charcode
/// conversion, and fontpack run switching.
pub fn pdf_addtext_text_ops(
font : PdfFont?,
fontpack : PdfFontPack?,
x : Double,
y : Double,
base_rotation : Double,
horizontal_offset : Double,
vertical_offset : Double,
outline : Bool,
line_width : Double,
unique_fontname : PdfName,
unique_fontnames : ArrayView[PdfName],
extgstate_name : PdfName?,
colour : PdfContentColour,
font_size : Double,
text : String,
text_rotation : Double,
rotation_h_offset : Double,
rotation_v_offset : Double,
) -> Array[PdfContentOp] raise PdfError {
let text_bytes = match font {
Some(font) => pdf_addtext_charcodes_of_utf8(font, text)
None => @utf8.encode(text)
}
pdf_addtext_text_ops_with_bytes(
fontpack, x, y, base_rotation, horizontal_offset, vertical_offset, outline, line_width,
unique_fontname, unique_fontnames, extgstate_name, colour, font_size, text, text_bytes,
text_rotation, rotation_h_offset, rotation_v_offset,
)
}
///|
fn pdf_addtext_degrees_to_radians(degrees : Double) -> Double {
degrees * @math.PI / 180.0
}
///|
/// Convert cpdf add-text's explicit rotation enum to content rotation radians.
pub fn pdf_addtext_rotation_radians(rotation : PdfAddTextRotation) -> Double {
match rotation {
PdfAddTextRot0 => 0.0
PdfAddTextRot90 => pdf_addtext_degrees_to_radians(270.0)
PdfAddTextRot180 => pdf_addtext_degrees_to_radians(180.0)
PdfAddTextRot270 => pdf_addtext_degrees_to_radians(90.0)
}
}
///|
/// Return cpdf's rotation-dependent text origin offset.
pub fn pdf_addtext_rotation_offsets(
position : PdfPosition,
rotation : PdfAddTextRotation,
text_width : Double,
) -> (Double, Double) {
match position {
PdfPositionTopLeft(_, _) =>
match rotation {
PdfAddTextRot0 | PdfAddTextRot90 => (0.0, 0.0)
PdfAddTextRot180 => (text_width, 0.0)
PdfAddTextRot270 => (0.0, -text_width)
}
PdfPositionTop(_) =>
match rotation {
PdfAddTextRot0 => (0.0, 0.0)
PdfAddTextRot90 => (text_width / 2.0, 0.0)
PdfAddTextRot180 => (text_width, 0.0)
PdfAddTextRot270 => (text_width / 2.0, -text_width)
}
PdfPositionTopRight(_, _) =>
match rotation {
PdfAddTextRot0 => (0.0, 0.0)
PdfAddTextRot90 | PdfAddTextRot180 => (text_width, 0.0)
PdfAddTextRot270 => (text_width, -text_width)
}
PdfPositionRight(_) | PdfPositionPosRight(_, _) =>
match rotation {
PdfAddTextRot0 => (0.0, 0.0)
PdfAddTextRot90 => (text_width, text_width / 2.0)
PdfAddTextRot180 => (text_width, 0.0)
PdfAddTextRot270 => (text_width, -text_width / 2.0)
}
PdfPositionBottomRight(_, _) =>
match rotation {
PdfAddTextRot0 => (0.0, 0.0)
PdfAddTextRot90 => (text_width, text_width)
PdfAddTextRot180 => (text_width, 0.0)
PdfAddTextRot270 => (text_width, 0.0)
}
PdfPositionBottom(_) =>
match rotation {
PdfAddTextRot0 => (0.0, 0.0)
PdfAddTextRot90 => (text_width / 2.0, text_width)
PdfAddTextRot180 => (text_width, 0.0)
PdfAddTextRot270 => (text_width / 2.0, 0.0)
}
PdfPositionBottomLeft(_, _) =>
match rotation {
PdfAddTextRot0 => (0.0, 0.0)
PdfAddTextRot90 => (0.0, text_width)
PdfAddTextRot180 => (text_width, 0.0)
PdfAddTextRot270 => (0.0, 0.0)
}
PdfPositionLeft(_) | PdfPositionPosLeft(_, _) =>
match rotation {
PdfAddTextRot0 => (0.0, 0.0)
PdfAddTextRot90 => (0.0, text_width / 2.0)
PdfAddTextRot180 => (text_width, 0.0)
PdfAddTextRot270 => (0.0, -text_width / 2.0)
}
PdfPositionCentre | PdfPositionPosCentre(_, _) =>
match rotation {
PdfAddTextRot0 => (0.0, 0.0)
PdfAddTextRot90 => (text_width / 2.0, text_width / 2.0)
PdfAddTextRot180 => (text_width, 0.0)
PdfAddTextRot270 => (text_width / 2.0, -(text_width / 2.0))
}
PdfPositionDiagonal | PdfPositionReverseDiagonal => (0.0, 0.0)
}
}
///|
/// Adjust cpdf add-text offsets for diagonal and reverse-diagonal placement.
pub fn pdf_addtext_diagonal_offsets(
bounds : PdfRectangle,
position : PdfPosition,
rotation : PdfAddTextRotation,
text_width : Double,
horizontal_offset : Double,
vertical_offset : Double,
base_rotation : Double,
) -> (Double, Double) {
match position {
PdfPositionDiagonal | PdfPositionReverseDiagonal => {
let rotate = if rotation == PdfAddTextRot180 {
base_rotation + @math.PI
} else {
base_rotation
}
let horizontal = -(@math.cos(@math.PI / 2.0 - rotate) * vertical_offset)
let vertical = @math.sin(@math.PI / 2.0 - rotate) * vertical_offset
if rotation == PdfAddTextRot180 {
let angle = @math.atan(
(bounds.max_y - bounds.min_y) / (bounds.max_x - bounds.min_x),
)
let dx = @math.cos(angle) * text_width
let dy = @math.sin(angle) * text_width
match position {
PdfPositionDiagonal => (horizontal - dx, vertical - dy)
PdfPositionReverseDiagonal => (horizontal - dx, vertical + dy)
_ => (horizontal, vertical)
}
} else {
(horizontal, vertical)
}
}
_ => (horizontal_offset, vertical_offset)
}
}
///|
/// Return cpdf's initial `(horizontal, vertical)` offset for a multi-line
/// add-text paragraph.
pub fn pdf_addtext_initial_line_offsets(
position : PdfPosition,
line_count : Int,
line_spacing : Double,
font_size : Double,
) -> (Double, Double) {
let line_delta = (line_count - 1).to_double()
let paragraph_height = line_spacing * font_size * line_delta
let vertical = match position {
PdfPositionBottomLeft(_, _)
| PdfPositionBottom(_)
| PdfPositionBottomRight(_, _) => -paragraph_height
PdfPositionLeft(_) | PdfPositionRight(_) => -(paragraph_height / 2.0)
PdfPositionDiagonal | PdfPositionReverseDiagonal =>
-(paragraph_height / 2.0)
_ => 0.0
}
(0.0, vertical)
}
///|
/// Return cpdf's per-line `(horizontal, vertical)` advance for add-text.
pub fn pdf_addtext_line_advance(
rotation : PdfAddTextRotation,
line_spacing : Double,
font_size : Double,
) -> (Double, Double) {
let advance = line_spacing * font_size
match rotation {
PdfAddTextRot90 | PdfAddTextRot270 => (advance, 0.0)
PdfAddTextRot0 | PdfAddTextRot180 => (0.0, advance)
}
}
///|
fn pdf_addtext_rotation_adjustment_offsets(
rotation : PdfAddTextRotation,
adjustment : Double,
) -> (Double, Double) {
match rotation {
PdfAddTextRot0 => (0.0, adjustment)
PdfAddTextRot90 => (adjustment, 0.0)
PdfAddTextRot180 => (0.0, -adjustment)
PdfAddTextRot270 => (-adjustment, 0.0)
}
}
///|
/// Return cpdf's `(horizontal, vertical)` baseline shift for midline/topline
/// add-text placement.
pub fn pdf_addtext_baseline_offsets(
font : PdfFont?,
font_size : Double,
position : PdfPosition,
rotation : PdfAddTextRotation,
midline : Bool,
topline : Bool,
) -> (Double, Double) {
if midline {
match font {
Some(PdfFontStandard({ font, .. })) =>
pdf_addtext_rotation_adjustment_offsets(
rotation,
font_size *
pdf_standard14_baseline_adjustment(font).to_double() /
1000.0,
)
Some(PdfFontSimple({ fontdescriptor: Some(descriptor), .. })) =>
pdf_addtext_rotation_adjustment_offsets(
rotation,
font_size * descriptor.capheight / 2.0 / 1000.0,
)
_ => (0.0, 0.0)
}
} else if topline {
match font {
Some(PdfFontStandard({ font, .. })) => {
let adjustment = font_size *
pdf_standard14_baseline_adjustment(font).to_double() *
2.0 /
1000.0
match position {
PdfPositionDiagonal | PdfPositionReverseDiagonal => (0.0, adjustment)
_ => pdf_addtext_rotation_adjustment_offsets(rotation, adjustment)
}
}
Some(PdfFontSimple({ fontdescriptor: Some(descriptor), .. })) => {
let adjustment = font_size * descriptor.capheight / 1000.0
match position {
PdfPositionDiagonal | PdfPositionReverseDiagonal => (0.0, adjustment)
_ => pdf_addtext_rotation_adjustment_offsets(rotation, adjustment)
}
}
_ => (0.0, 0.0)
}
} else {
(0.0, 0.0)
}
}
///|
/// Return the text encoding cpdf add-text uses for the selected font.
pub fn pdf_addtext_effective_encoding(font : PdfFont?) -> PdfEncoding {
match font {
Some(PdfFontStandard({ encoding, .. })) => encoding
Some(PdfFontSimple({ encoding, .. })) => encoding
_ => PdfWinAnsiEncoding
}
}
///|
/// Return the page font resource object used by cpdf add-text setup.
///
/// Missing `/Font` resources are treated as an empty dictionary. Present values
/// are resolved through the document and returned as-is, matching cpdf's
/// `Pdf.lookup_direct` setup before unique font-name allocation.
pub fn PdfDocument::addtext_page_font_dictionary(
self : PdfDocument,
page : PdfPage,
) -> PdfObject {
match self.lookup_direct(pdf_addtext_font_key(), page.resources) {
Some(fontdict) => fontdict
None => PdfDictionary([])
}
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_page_font_dictionary`.
pub fn pdf_addtext_page_font_dictionary(
document : PdfDocument,
page : PdfPage,
) -> PdfObject {
document.addtext_page_font_dictionary(page)
}
///|
/// Allocate cpdf add-text font resource names for the selected font and
/// optional font pack.
pub fn pdf_addtext_allocate_font_names(
fontdict : PdfObject,
fontpack : PdfFontPack?,
) -> (PdfName, PdfObject, Array[PdfName]) raise PdfError {
let unique_fontname = fontdict.unique_key("F")
let mut updated = fontdict
let unique_fontnames : Array[PdfName] = match fontpack {
Some(pack) => Array::new(capacity=pack.fonts.length())
None => []
}
match fontpack {
Some(pack) =>
for _ in pack.fonts {
let key = updated.unique_key("F")
updated = updated.add_dict_entry(key, PdfNull)
unique_fontnames.push(key)
}
None => ()
}
(unique_fontname, updated, unique_fontnames)
}
///|
/// Replace cpdf add-text fontpack placeholders with embedded font references.
pub fn pdf_addtext_fontpack_resource_dictionary(
fontdict : PdfObject,
unique_fontnames : ArrayView[PdfName],
font_objects : ArrayView[Int],
) -> PdfObject raise PdfError {
if font_objects.length() != unique_fontnames.length() {
raise SoftError("bad add-text fontpack resource list")
}
let mut updated = fontdict
for index in 0.. PdfObject raise PdfError {
match font_object {
Some(font_object) =>
resources.add_dict_entry(
pdf_addtext_font_key(),
fontdict.add_dict_entry(unique_fontname, font_object),
)
None => resources
}
}
///|
/// Install cpdf add-text fontpack resources into a page resource dictionary.
pub fn pdf_addtext_install_fontpack_resources(
resources : PdfObject,
fontdict : PdfObject,
unique_fontnames : ArrayView[PdfName],
font_objects : ArrayView[Int],
) -> PdfObject raise PdfError {
resources.add_dict_entry(
pdf_addtext_font_key(),
pdf_addtext_fontpack_resource_dictionary(
fontdict, unique_fontnames, font_objects,
),
)
}
///|
/// Write the selected cpdf add-text font and return its indirect reference.
///
/// Existing-named-font mode has no selected font to write, so `None` is
/// preserved for callers that later reuse the page's current font resource.
pub fn PdfDocument::addtext_selected_font_object(
self : PdfDocument,
font : PdfFont?,
) -> PdfObject? raise PdfError {
match font {
Some(font) => Some(PdfIndirect(self.write_font(font)))
None => None
}
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_selected_font_object`.
pub fn pdf_addtext_selected_font_object(
document : PdfDocument,
font : PdfFont?,
) -> PdfObject? raise PdfError {
document.addtext_selected_font_object(font)
}
///|
/// Prepare page resources for one cpdf add-text line.
///
/// Selected-font and fontpack modes install new `/Font` entries and optional
/// opacity resources. Existing-named-font mode reuses the page resources
/// unchanged and suppresses opacity graphics state selection, matching cpdf's
/// `font = None` branch.
pub fn PdfDocument::addtext_resource_setup(
self : PdfDocument,
page : PdfPage,
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_object : PdfObject?,
fontpack_font_objects : ArrayView[Int],
opacity : Double,
) -> PdfAddTextResourceSetup raise PdfError {
let fontdict = self.addtext_page_font_dictionary(page)
match (font, fontpack) {
(None, None) =>
{
resources: page.resources,
font_dictionary: fontdict,
unique_fontname: fontname,
unique_fontnames: [],
extgstate_name: None,
}
(_, Some(fontpack)) => {
let (resources, extgstate_name) = self.addtext_opacity_resources(
page.resources,
opacity,
)
let (unique_fontname, reserved_fontdict, unique_fontnames) = pdf_addtext_allocate_font_names(
fontdict,
Some(fontpack),
)
{
resources: pdf_addtext_install_fontpack_resources(
resources, reserved_fontdict, unique_fontnames, fontpack_font_objects,
),
font_dictionary: reserved_fontdict,
unique_fontname,
unique_fontnames,
extgstate_name,
}
}
(Some(_), None) => {
let (resources, extgstate_name) = self.addtext_opacity_resources(
page.resources,
opacity,
)
let (unique_fontname, fontdict, unique_fontnames) = pdf_addtext_allocate_font_names(
fontdict,
None,
)
{
resources: pdf_addtext_install_font_resource(
resources, fontdict, unique_fontname, font_object,
),
font_dictionary: fontdict,
unique_fontname,
unique_fontnames,
extgstate_name,
}
}
}
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_resource_setup`.
pub fn pdf_addtext_resource_setup(
document : PdfDocument,
page : PdfPage,
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_object : PdfObject?,
fontpack_font_objects : ArrayView[Int],
opacity : Double,
) -> PdfAddTextResourceSetup raise PdfError {
document.addtext_resource_setup(
page, font, fontpack, fontname, font_object, fontpack_font_objects, opacity,
)
}
///|
fn pdf_addtext_height_font_name(fontname : PdfName) -> String {
let text = @ascii.decode_lossy(fontname.bytes())
if text.has_prefix("/") {
text[1:].to_owned()
} else {
text
}
}
///|
fn PdfDocument::pdf_addtext_effective_fontname(
self : PdfDocument,
page : PdfPage,
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
) -> PdfName raise PdfError {
match (font, fontpack) {
(None, None) => {
let (resource_name, _) = self.addtext_existing_font_resource(
page, fontname,
)
resource_name
}
_ => fontname
}
}
///|
/// Add one prepared cpdf add-text line to a page.
///
/// This is the single-page counterpart of cpdf's local `addtext_page`: it
/// prepares text and placement, installs resources, emits `/CPDFSTAMP` text
/// operators, creates URL annotations, and appends or prepends the content.
pub fn PdfDocument::addtext_line_page(
self : PdfDocument,
page : PdfPage,
time : PdfStrftimeTime,
lines : ArrayView[String],
replacements : ArrayView[(String, () -> String)],
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_object : PdfObject?,
fontpack_font_objects : ArrayView[Int],
font_size : Double,
text : String,
colour : PdfContentColour,
position : PdfPosition,
relative_to_box? : PdfName = pdf_page_mediabox_key(),
horizontal_offset? : Double = 0.0,
vertical_offset? : Double = 0.0,
shift_x? : Double = 0.0,
shift_y? : Double = 0.0,
opacity? : Double = 1.0,
justification? : PdfAddTextJustification = PdfAddTextLeftJustify,
rotation? : PdfAddTextRotation = PdfAddTextRot0,
outline? : Bool = false,
line_width? : Double = 1.0,
underneath? : Bool = false,
fast? : Bool = false,
raw? : Bool = false,
url_border? : Bool = false,
) -> PdfPage raise PdfError {
let effective_fontname = self.pdf_addtext_effective_fontname(
page, font, fontpack, fontname,
)
let prepared = self.addtext_prepare_line(
page,
time,
lines,
replacements,
font,
fontpack,
effective_fontname,
font_size,
text,
position,
relative_to_box~,
horizontal_offset~,
vertical_offset~,
justification~,
rotation~,
raw~,
)
let setup = self.addtext_resource_setup(
page, font, fontpack, effective_fontname, font_object, fontpack_font_objects,
opacity,
)
let ops = pdf_addtext_text_ops_with_bytes(
fontpack,
prepared.placement.origin.x + shift_x,
prepared.placement.origin.y + shift_y,
prepared.placement.base_rotation,
prepared.placement.horizontal_offset,
prepared.placement.vertical_offset,
outline,
line_width,
setup.unique_fontname,
setup.unique_fontnames,
setup.extgstate_name,
colour,
font_size,
prepared.text,
prepared.text_bytes,
prepared.placement.text_rotation,
prepared.placement.rotation_offset.x,
prepared.placement.rotation_offset.y,
)
let annotations = self.addtext_url_annotation_references(
prepared.text,
prepared.urls,
fn(prefix) raise PdfError {
let bytes = self.pdf_addtext_measurement_bytes_for_page(
page, font, fontpack, effective_fontname, raw, prefix,
)
self.addtext_calc_textwidth(
page, font, fontpack, effective_fontname, font_size, bytes,
)
},
prepared.placement.origin,
prepared.placement.horizontal_offset,
prepared.placement.vertical_offset,
prepared.placement.justification_offset,
prepared.placement.text_rotation,
prepared.placement.rotation_offset,
prepared.placement.base_rotation,
font,
pdf_addtext_height_font_name(effective_fontname),
font_size,
url_border,
)
let page = {
..page,
resources: setup.resources,
rest: self.addtext_page_rest_with_annotations(page.rest, annotations),
}
if underneath {
self.prepend_operators(page, ops, fast~)
} else {
self.postpend_operators(page, ops, fast~)
}
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_line_page`.
pub fn pdf_addtext_line_page(
document : PdfDocument,
page : PdfPage,
time : PdfStrftimeTime,
lines : ArrayView[String],
replacements : ArrayView[(String, () -> String)],
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_object : PdfObject?,
fontpack_font_objects : ArrayView[Int],
font_size : Double,
text : String,
colour : PdfContentColour,
position : PdfPosition,
relative_to_box? : PdfName = pdf_page_mediabox_key(),
horizontal_offset? : Double = 0.0,
vertical_offset? : Double = 0.0,
shift_x? : Double = 0.0,
shift_y? : Double = 0.0,
opacity? : Double = 1.0,
justification? : PdfAddTextJustification = PdfAddTextLeftJustify,
rotation? : PdfAddTextRotation = PdfAddTextRot0,
outline? : Bool = false,
line_width? : Double = 1.0,
underneath? : Bool = false,
fast? : Bool = false,
raw? : Bool = false,
url_border? : Bool = false,
) -> PdfPage raise PdfError {
document.addtext_line_page(
page,
time,
lines,
replacements,
font,
fontpack,
fontname,
font_object,
fontpack_font_objects,
font_size,
text,
colour,
position,
relative_to_box~,
horizontal_offset~,
vertical_offset~,
shift_x~,
shift_y~,
opacity~,
justification~,
rotation~,
outline~,
line_width~,
underneath~,
fast~,
raw~,
url_border~,
)
}
///|
/// Add all cpdf add-text lines to one page using cpdf paragraph advancement.
///
/// This ports the outer per-line loop around cpdf's local `addtext` call:
/// initial multi-line offsets, midline/topline baseline adjustment, repeated
/// line emission, and rotation-dependent line advancement.
pub fn PdfDocument::addtext_page_lines(
self : PdfDocument,
page : PdfPage,
time : PdfStrftimeTime,
lines : ArrayView[String],
replacements : ArrayView[(String, () -> String)],
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_object : PdfObject?,
fontpack_font_objects : ArrayView[Int],
font_size : Double,
line_spacing : Double,
colour : PdfContentColour,
position : PdfPosition,
relative_to_box? : PdfName = pdf_page_mediabox_key(),
opacity? : Double = 1.0,
justification? : PdfAddTextJustification = PdfAddTextLeftJustify,
rotation? : PdfAddTextRotation = PdfAddTextRot0,
midline? : Bool = false,
topline? : Bool = false,
shift_x? : Double = 0.0,
shift_y? : Double = 0.0,
outline? : Bool = false,
line_width? : Double = 1.0,
underneath? : Bool = false,
fast? : Bool = false,
raw? : Bool = false,
url_border? : Bool = false,
) -> PdfPage raise PdfError {
let (initial_h_offset, initial_v_offset) = pdf_addtext_initial_line_offsets(
position,
lines.length(),
line_spacing,
font_size,
)
let (baseline_h_offset, baseline_v_offset) = pdf_addtext_baseline_offsets(
font, font_size, position, rotation, midline, topline,
)
let advance = pdf_addtext_line_advance(rotation, line_spacing, font_size)
let mut horizontal_offset = initial_h_offset + baseline_h_offset
let mut vertical_offset = initial_v_offset + baseline_v_offset
let mut output = page
for line in lines {
output = self.addtext_line_page(
output,
time,
lines,
replacements,
font,
fontpack,
fontname,
font_object,
fontpack_font_objects,
font_size,
line,
colour,
position,
relative_to_box~,
horizontal_offset~,
vertical_offset~,
shift_x~,
shift_y~,
opacity~,
justification~,
rotation~,
outline~,
line_width~,
underneath~,
fast~,
raw~,
url_border~,
)
horizontal_offset += advance.0
vertical_offset += advance.1
}
output
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_page_lines`.
pub fn pdf_addtext_page_lines(
document : PdfDocument,
page : PdfPage,
time : PdfStrftimeTime,
lines : ArrayView[String],
replacements : ArrayView[(String, () -> String)],
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_object : PdfObject?,
fontpack_font_objects : ArrayView[Int],
font_size : Double,
line_spacing : Double,
colour : PdfContentColour,
position : PdfPosition,
relative_to_box? : PdfName = pdf_page_mediabox_key(),
opacity? : Double = 1.0,
justification? : PdfAddTextJustification = PdfAddTextLeftJustify,
rotation? : PdfAddTextRotation = PdfAddTextRot0,
midline? : Bool = false,
topline? : Bool = false,
shift_x? : Double = 0.0,
shift_y? : Double = 0.0,
outline? : Bool = false,
line_width? : Double = 1.0,
underneath? : Bool = false,
fast? : Bool = false,
raw? : Bool = false,
url_border? : Bool = false,
) -> PdfPage raise PdfError {
document.addtext_page_lines(
page,
time,
lines,
replacements,
font,
fontpack,
fontname,
font_object,
fontpack_font_objects,
font_size,
line_spacing,
colour,
position,
relative_to_box~,
opacity~,
justification~,
rotation~,
midline~,
topline~,
shift_x~,
shift_y~,
outline~,
line_width~,
underneath~,
fast~,
raw~,
url_border~,
)
}
///|
/// Add cpdf add-text lines to selected pages with an already resolved font.
///
/// This ports the document/page-selection layer after font selection: selected
/// one-based pages get page-local replacement callbacks, cpdf shift-coordinate
/// parsing, and page-line emission. Font embedding and existing-font discovery
/// remain separate slices.
pub fn PdfDocument::addtext_pages_with_font(
self : PdfDocument,
page_numbers : ArrayView[Int],
lines : ArrayView[String],
time : PdfStrftimeTime,
bookmarks : ArrayView[PdfBookmark],
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_object : PdfObject?,
fontpack_font_objects : ArrayView[Int],
font_size : Double,
line_spacing : Double,
colour : PdfContentColour,
position : PdfPosition,
filename? : String = "",
bates? : Int = 1,
bates_pad? : Int? = None,
relative_to_box? : PdfName = pdf_page_mediabox_key(),
shift? : String = "0 0",
opacity? : Double = 1.0,
justification? : PdfAddTextJustification = PdfAddTextLeftJustify,
rotation? : PdfAddTextRotation = PdfAddTextRot0,
midline? : Bool = false,
topline? : Bool = false,
outline? : Bool = false,
line_width? : Double = 1.0,
underneath? : Bool = false,
fast? : Bool = false,
raw? : Bool = false,
url_border? : Bool = false,
) -> PdfDocument raise PdfError {
if page_numbers.length() == 0 {
self
} else {
let pages = self.pages_of_pagetree()
pdf_addtext_validate_page_numbers(
page_numbers,
pages.length(),
"addtext_pages_with_font",
)
let shifts = self.parse_coordinates(shift)
let selected_pages = @number_set.pdf_number_set(page_numbers)
let rewritten : Array[PdfPage] = Array::new(capacity=pages.length())
for index in 0.. PdfDocument raise PdfError {
document.addtext_pages_with_font(
page_numbers,
lines,
time,
bookmarks,
font,
fontpack,
fontname,
font_object,
fontpack_font_objects,
font_size,
line_spacing,
colour,
position,
filename~,
bates~,
bates_pad~,
relative_to_box~,
shift~,
opacity~,
justification~,
rotation~,
midline~,
topline~,
outline~,
line_width~,
underneath~,
fast~,
raw~,
url_border~,
)
}
///|
fn pdf_addtext_append_unique_codepoints(
used : Array[Int],
codepoints : ArrayView[Int],
) -> Unit {
for codepoint in codepoints {
if !pdf_addtext_codepoints_contains(used, codepoint) {
used.push(codepoint)
}
}
}
///|
fn PdfDocument::pdf_addtext_used_codepoints_for_pages(
self : PdfDocument,
pages : ArrayView[PdfPage],
page_numbers : ArrayView[Int],
lines : ArrayView[String],
time : PdfStrftimeTime,
bookmarks : ArrayView[PdfBookmark],
filename : String,
bates : Int,
bates_pad : Int?,
) -> Array[Int] raise PdfError {
let used : Array[Int] = Array::new(
capacity=page_numbers.length() * lines.length(),
)
for page_number in page_numbers {
let values = self.addtext_replacement_values(
bookmarks,
page_number,
pages.length(),
filename,
bates,
bates_pad,
)
let replacement_pairs = pdf_addtext_replacement_pairs(values)
let expanded = pdf_addtext_expand_lines_with_time(
time, lines, replacement_pairs,
)
pdf_addtext_append_unique_codepoints(
used,
pdf_addtext_used_codepoints(expanded),
)
}
used
}
///|
fn PdfDocument::pdf_addtext_write_fontpack_objects(
self : PdfDocument,
fontpack : PdfFontPack?,
) -> Array[Int] raise PdfError {
let objects : Array[Int] = match fontpack {
Some(pack) => Array::new(capacity=pack.fonts.length())
None => []
}
match fontpack {
Some(fontpack) =>
for font in fontpack.fonts {
objects.push(self.write_font(font))
}
None => ()
}
objects
}
///|
fn pdf_addtext_resolve_font_source(
source : PdfAddTextFontSource,
) -> (PdfFont?, PdfFontPack?) raise PdfError {
match source {
PdfAddTextPreMadeFontPack(pack) =>
if pack.fonts.length() == 0 {
raise SoftError("addtexts: fontpack is empty")
} else {
(Some(pack.fonts[0]), None)
}
PdfAddTextExistingNamedFont => (None, None)
}
}
///|
/// Add cpdf add-text to selected pages with deterministic time and resolved fonts.
///
/// This ports the outer source-level `addtexts` orchestration once callers have
/// already chosen an existing font, parsed font, or font pack. It prepares the
/// input text lines, skips empty visible output before writing fonts, writes
/// selected/fontpack font objects, and delegates page rewriting to
/// `addtext_pages_with_font`.
pub fn PdfDocument::addtexts_with_font(
self : PdfDocument,
page_numbers : ArrayView[Int],
text : String,
time : PdfStrftimeTime,
bookmarks : ArrayView[PdfBookmark],
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_size : Double,
line_spacing : Double,
colour : PdfContentColour,
position : PdfPosition,
filename? : String = "",
bates? : Int = 1,
bates_pad? : Int? = None,
relative_to_box? : PdfName = pdf_page_mediabox_key(),
shift? : String = "0 0",
opacity? : Double = 1.0,
justification? : PdfAddTextJustification = PdfAddTextLeftJustify,
rotation? : PdfAddTextRotation = PdfAddTextRot0,
midline? : Bool = false,
topline? : Bool = false,
outline? : Bool = false,
line_width? : Double = 1.0,
underneath? : Bool = false,
fast? : Bool = false,
raw? : Bool = false,
url_border? : Bool = false,
) -> PdfDocument raise PdfError {
if page_numbers.length() == 0 {
self
} else {
let pages = self.pages_of_pagetree()
pdf_addtext_validate_page_numbers(
page_numbers,
pages.length(),
"addtexts_with_font",
)
let lines = pdf_addtext_lines_for_rotation(text, rotation)
let used = self.pdf_addtext_used_codepoints_for_pages(
pages, page_numbers, lines, time, bookmarks, filename, bates, bates_pad,
)
if used.length() == 0 {
self
} else {
let font_object = match fontpack {
Some(_) => None
None => self.addtext_selected_font_object(font)
}
let fontpack_font_objects = self.pdf_addtext_write_fontpack_objects(
fontpack,
)
self.addtext_pages_with_font(
page_numbers,
lines,
time,
bookmarks,
font,
fontpack,
fontname,
font_object,
fontpack_font_objects,
font_size,
line_spacing,
colour,
position,
filename~,
bates~,
bates_pad~,
relative_to_box~,
shift~,
opacity~,
justification~,
rotation~,
midline~,
topline~,
outline~,
line_width~,
underneath~,
fast~,
raw~,
url_border~,
)
}
}
}
///|
/// Compatibility wrapper for `PdfDocument::addtexts_with_font`.
pub fn pdf_addtexts_with_font(
document : PdfDocument,
page_numbers : ArrayView[Int],
text : String,
time : PdfStrftimeTime,
bookmarks : ArrayView[PdfBookmark],
font : PdfFont?,
fontpack : PdfFontPack?,
fontname : PdfName,
font_size : Double,
line_spacing : Double,
colour : PdfContentColour,
position : PdfPosition,
filename? : String = "",
bates? : Int = 1,
bates_pad? : Int? = None,
relative_to_box? : PdfName = pdf_page_mediabox_key(),
shift? : String = "0 0",
opacity? : Double = 1.0,
justification? : PdfAddTextJustification = PdfAddTextLeftJustify,
rotation? : PdfAddTextRotation = PdfAddTextRot0,
midline? : Bool = false,
topline? : Bool = false,
outline? : Bool = false,
line_width? : Double = 1.0,
underneath? : Bool = false,
fast? : Bool = false,
raw? : Bool = false,
url_border? : Bool = false,
) -> PdfDocument raise PdfError {
document.addtexts_with_font(
page_numbers,
text,
time,
bookmarks,
font,
fontpack,
fontname,
font_size,
line_spacing,
colour,
position,
filename~,
bates~,
bates_pad~,
relative_to_box~,
shift~,
opacity~,
justification~,
rotation~,
midline~,
topline~,
outline~,
line_width~,
underneath~,
fast~,
raw~,
url_border~,
)
}
///|
/// Add cpdf add-text to selected pages from a non-embedding font source.
pub fn PdfDocument::addtexts_with_source(
self : PdfDocument,
page_numbers : ArrayView[Int],
text : String,
time : PdfStrftimeTime,
bookmarks : ArrayView[PdfBookmark],
font_source : PdfAddTextFontSource,
fontname : PdfName,
font_size : Double,
line_spacing : Double,
colour : PdfContentColour,
position : PdfPosition,
filename? : String = "",
bates? : Int = 1,
bates_pad? : Int? = None,
relative_to_box? : PdfName = pdf_page_mediabox_key(),
shift? : String = "0 0",
opacity? : Double = 1.0,
justification? : PdfAddTextJustification = PdfAddTextLeftJustify,
rotation? : PdfAddTextRotation = PdfAddTextRot0,
midline? : Bool = false,
topline? : Bool = false,
outline? : Bool = false,
line_width? : Double = 1.0,
underneath? : Bool = false,
fast? : Bool = false,
raw? : Bool = false,
url_border? : Bool = false,
) -> PdfDocument raise PdfError {
if page_numbers.length() == 0 {
self
} else {
let pages = self.pages_of_pagetree()
pdf_addtext_validate_page_numbers(
page_numbers,
pages.length(),
"addtexts_with_source",
)
let lines = pdf_addtext_lines_for_rotation(text, rotation)
let used = self.pdf_addtext_used_codepoints_for_pages(
pages, page_numbers, lines, time, bookmarks, filename, bates, bates_pad,
)
if used.length() == 0 {
self
} else {
let (font, fontpack) = pdf_addtext_resolve_font_source(font_source)
self.addtexts_with_font(
page_numbers,
text,
time,
bookmarks,
font,
fontpack,
fontname,
font_size,
line_spacing,
colour,
position,
filename~,
bates~,
bates_pad~,
relative_to_box~,
shift~,
opacity~,
justification~,
rotation~,
midline~,
topline~,
outline~,
line_width~,
underneath~,
fast~,
raw~,
url_border~,
)
}
}
}
///|
/// Compatibility wrapper for `PdfDocument::addtexts_with_source`.
pub fn pdf_addtexts_with_source(
document : PdfDocument,
page_numbers : ArrayView[Int],
text : String,
time : PdfStrftimeTime,
bookmarks : ArrayView[PdfBookmark],
font_source : PdfAddTextFontSource,
fontname : PdfName,
font_size : Double,
line_spacing : Double,
colour : PdfContentColour,
position : PdfPosition,
filename? : String = "",
bates? : Int = 1,
bates_pad? : Int? = None,
relative_to_box? : PdfName = pdf_page_mediabox_key(),
shift? : String = "0 0",
opacity? : Double = 1.0,
justification? : PdfAddTextJustification = PdfAddTextLeftJustify,
rotation? : PdfAddTextRotation = PdfAddTextRot0,
midline? : Bool = false,
topline? : Bool = false,
outline? : Bool = false,
line_width? : Double = 1.0,
underneath? : Bool = false,
fast? : Bool = false,
raw? : Bool = false,
url_border? : Bool = false,
) -> PdfDocument raise PdfError {
document.addtexts_with_source(
page_numbers,
text,
time,
bookmarks,
font_source,
fontname,
font_size,
line_spacing,
colour,
position,
filename~,
bates~,
bates_pad~,
relative_to_box~,
shift~,
opacity~,
justification~,
rotation~,
midline~,
topline~,
outline~,
line_width~,
underneath~,
fast~,
raw~,
url_border~,
)
}
///|
fn pdf_addtext_font_not_found(fontname : PdfName) -> PdfError {
SoftError(
"addtext: font " + @ascii.decode_lossy(fontname.bytes()) + " not found",
)
}
///|
/// Resolve an existing page font for cpdf add-text.
///
/// The requested name is first treated as a `/Font` resource key. If that is
/// absent, the page font dictionary is scanned for an entry whose `/BaseFont`
/// matches the requested name; the returned `PdfName` is the resource key to
/// use in emitted text operators.
pub fn PdfDocument::addtext_existing_font_resource(
self : PdfDocument,
page : PdfPage,
fontname : PdfName,
) -> (PdfName, PdfObject) raise PdfError {
let fontdict = match
self.lookup_direct(pdf_addtext_font_key(), page.resources) {
Some(fontdict) => fontdict
None => raise SoftError("addtext: font dictionary not present")
}
match self.lookup_direct(fontname, fontdict) {
Some(font) => (fontname, font)
None =>
match fontdict {
PdfDictionary(entries) =>
for entry in entries {
match self.lookup_direct(pdf_text_base_font_key_name, entry.1) {
Some(PdfNameObject(basefont)) if basefont == fontname =>
break (entry.0, entry.1)
_ => ()
}
} nobreak {
raise pdf_addtext_font_not_found(fontname)
}
_ => raise pdf_addtext_font_not_found(fontname)
}
}
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_existing_font_resource`.
pub fn pdf_addtext_existing_font_resource(
document : PdfDocument,
page : PdfPage,
fontname : PdfName,
) -> (PdfName, PdfObject) raise PdfError {
document.addtext_existing_font_resource(page, fontname)
}
///|
/// Measure add-text bytes using an existing page font resource.
pub fn PdfDocument::addtext_width_of_existing_font_resource(
self : PdfDocument,
page : PdfPage,
fontname : PdfName,
font_size : Double,
text : BytesView,
) -> (PdfName, Double) raise PdfError {
let (resource_name, font_object) = self.addtext_existing_font_resource(
page, fontname,
)
let font = self.read_font(font_object)
(resource_name, pdf_addtext_width_of_selected_font(font, font_size, text))
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_width_of_existing_font_resource`.
pub fn pdf_addtext_width_of_existing_font_resource(
document : PdfDocument,
page : PdfPage,
fontname : PdfName,
font_size : Double,
text : BytesView,
) -> (PdfName, Double) raise PdfError {
document.addtext_width_of_existing_font_resource(
page, fontname, font_size, text,
)
}
///|
/// Build cpdf's URL link annotation object for add-text markup.
pub fn pdf_addtext_url_annotation(
p0 : Point2,
p1 : Point2,
p2 : Point2,
p3 : Point2,
rectangle : PdfRectangle,
url : String,
url_border : Bool,
) -> PdfObject {
PdfDictionary([
(pdf_addtext_subtype_key_name, PdfNameObject(pdf_addtext_link_name)),
(
pdf_addtext_quadpoints_key_name,
PdfArray([
PdfReal(p0.x),
PdfReal(p0.y),
PdfReal(p3.x),
PdfReal(p3.y),
PdfReal(p1.x),
PdfReal(p1.y),
PdfReal(p2.x),
PdfReal(p2.y),
]),
),
(
pdf_addtext_rect_key_name,
PdfArray([
PdfReal(rectangle.min_x),
PdfReal(rectangle.min_y),
PdfReal(rectangle.max_x),
PdfReal(rectangle.max_y),
]),
),
(
pdf_addtext_bs_key_name,
PdfDictionary([
(pdf_addtext_w_key_name, PdfInteger(if url_border { 1 } else { 0 })),
]),
),
(
pdf_addtext_a_key_name,
PdfDictionary([
(pdf_addtext_uri_key_name, PdfString(@utf8.encode(url))),
(pdf_addtext_type_key_name, PdfNameObject(pdf_addtext_action_name)),
(pdf_addtext_s_key_name, PdfNameObject(pdf_addtext_uri_key_name)),
]),
),
])
}
///|
fn pdf_addtext_bounds_of_points(
p0 : Point2,
p1 : Point2,
p2 : Point2,
p3 : Point2,
) -> PdfRectangle {
{
min_x: double_min(double_min(p0.x, p1.x), double_min(p2.x, p3.x)),
min_y: double_min(double_min(p0.y, p1.y), double_min(p2.y, p3.y)),
max_x: double_max(double_max(p0.x, p1.x), double_max(p2.x, p3.x)),
max_y: double_max(double_max(p0.y, p1.y), double_max(p2.y, p3.y)),
}
}
///|
/// Return cpdf's add-text URL annotation height for a font selection.
pub fn pdf_addtext_url_annotation_height(
font : PdfFont?,
fontname : String,
font_size : Double,
) -> Double {
match pdf_addtext_cap_height(font, fontname) {
Some(cap_height) => cap_height * font_size / 1000.0
None => font_size
}
}
///|
/// Calculate the transformed quad points and bounds for an add-text URL span.
pub fn pdf_addtext_url_annotation_geometry(
origin : Point2,
horizontal_offset : Double,
vertical_offset : Double,
justification_offset : Double,
rotation : Double,
rotation_offset : Point2,
page_rotation : Double,
start_width : Double,
end_width : Double,
height : Double,
) -> PdfAddTextUrlAnnotationGeometry {
let base_x = origin.x - horizontal_offset - justification_offset
let base_y = origin.y - vertical_offset
let min_x = base_x + start_width
let min_y = base_y
let max_x = base_x + end_width
let max_y = base_y + height
let matrix = transform_matrix_of_transform([
Translate(rotation_offset.x, rotation_offset.y),
Rotate(point2(base_x, base_y), rotation + page_rotation),
])
let p0 = matrix.apply(point2(min_x, min_y))
let p1 = matrix.apply(point2(max_x, max_y))
let p2 = matrix.apply(point2(min_x, max_y))
let p3 = matrix.apply(point2(max_x, min_y))
{ p0, p1, p2, p3, rectangle: pdf_addtext_bounds_of_points(p0, p1, p2, p3) }
}
///|
fn pdf_addtext_url_annotation_prefix(
text : String,
position : Int,
) -> String raise PdfError {
let bytes = @utf8.encode(text)
if position < 0 || position > bytes.length() {
raise SoftError("bad add-text URL byte offset")
}
@utf8.decode(bytes[:position]) catch {
_ => raise SoftError("bad add-text URL byte offset")
}
}
///|
fn pdf_addtext_url_annotation_widths_with_measure(
text : String,
url : PdfAddTextUrl,
measure : (String) -> Double raise PdfError,
) -> (Double, Double) raise PdfError {
(
measure(pdf_addtext_url_annotation_prefix(text, url.start)),
measure(pdf_addtext_url_annotation_prefix(text, url.end)),
)
}
///|
/// Measure cpdf's start and end prefix widths for an add-text URL span.
pub fn pdf_addtext_url_annotation_widths(
text : String,
url : PdfAddTextUrl,
measure : (String) -> Double,
) -> (Double, Double) raise PdfError {
pdf_addtext_url_annotation_widths_with_measure(text, url, fn(prefix) {
measure(prefix)
})
}
///|
/// Build indirect URL link annotations for one cpdf add-text line.
///
/// This mirrors cpdf's add-text annotation pass: each parsed URL span is
/// measured against the visible line, converted to transformed quad points, and
/// written as a new indirect annotation object.
pub fn PdfDocument::addtext_url_annotation_references(
self : PdfDocument,
text : String,
urls : ArrayView[PdfAddTextUrl],
measure : (String) -> Double raise PdfError,
origin : Point2,
horizontal_offset : Double,
vertical_offset : Double,
justification_offset : Double,
rotation : Double,
rotation_offset : Point2,
page_rotation : Double,
font : PdfFont?,
fontname : String,
font_size : Double,
url_border : Bool,
) -> Array[PdfObject] raise PdfError {
let height = pdf_addtext_url_annotation_height(font, fontname, font_size)
let annotations : Array[PdfObject] = Array::new(capacity=urls.length())
for url in urls {
let (start_width, end_width) = pdf_addtext_url_annotation_widths_with_measure(
text, url, measure,
)
let geometry = pdf_addtext_url_annotation_geometry(
origin, horizontal_offset, vertical_offset, justification_offset, rotation,
rotation_offset, page_rotation, start_width, end_width, height,
)
let object_number = self.add_object(
pdf_addtext_url_annotation(
geometry.p0,
geometry.p1,
geometry.p2,
geometry.p3,
geometry.rectangle,
url.url,
url_border,
),
)
annotations.push(PdfIndirect(object_number))
}
annotations
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_url_annotation_references`.
pub fn pdf_addtext_url_annotation_references(
document : PdfDocument,
text : String,
urls : ArrayView[PdfAddTextUrl],
measure : (String) -> Double raise PdfError,
origin : Point2,
horizontal_offset : Double,
vertical_offset : Double,
justification_offset : Double,
rotation : Double,
rotation_offset : Point2,
page_rotation : Double,
font : PdfFont?,
fontname : String,
font_size : Double,
url_border : Bool,
) -> Array[PdfObject] raise PdfError {
document.addtext_url_annotation_references(
text, urls, measure, origin, horizontal_offset, vertical_offset, justification_offset,
rotation, rotation_offset, page_rotation, font, fontname, font_size, url_border,
)
}
///|
/// Return a page rest dictionary with cpdf add-text annotations prepended.
///
/// Empty new annotation lists leave `rest` unchanged. Existing `/Annots`
/// arrays are resolved through `document`; missing or malformed values are
/// treated as empty, matching cpdf's add-text page update.
pub fn PdfDocument::addtext_page_rest_with_annotations(
self : PdfDocument,
rest : PdfObject,
annotations : ArrayView[PdfObject],
) -> PdfObject raise PdfError {
if annotations.length() == 0 {
rest
} else {
let updated : Array[PdfObject] = [
for annotation in annotations => annotation
]
match self.lookup_direct(pdf_addtext_annots_key(), rest) {
Some(PdfArray(existing)) =>
for annotation in existing {
updated.push(annotation)
}
_ => ()
}
rest.add_dict_entry(pdf_addtext_annots_key(), PdfArray(updated))
}
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_page_rest_with_annotations`.
pub fn pdf_addtext_page_rest_with_annotations(
document : PdfDocument,
rest : PdfObject,
annotations : ArrayView[PdfObject],
) -> PdfObject raise PdfError {
document.addtext_page_rest_with_annotations(rest, annotations)
}
///|
/// Calculate the cpdf add-rectangle lower-left origin after placement.
pub fn pdf_addtext_rectangle_origin(
bounds : PdfRectangle,
position : PdfPosition,
width : Double,
height : Double,
) -> (Double, Double) {
let placed = pdf_calculate_position(false, width, bounds, position)
let y = match position {
PdfPositionTop(_) | PdfPositionTopLeft(_, _) | PdfPositionTopRight(_, _) =>
placed.y - height
PdfPositionCentre | PdfPositionPosCentre(_, _) => placed.y - height / 2.0
_ => placed.y
}
(placed.x, y)
}
///|
fn PdfDocument::pdf_addtext_rectangle_bounds(
self : PdfDocument,
page : PdfPage,
relative_to_box : PdfName,
) -> PdfRectangle raise PdfError {
if relative_to_box == pdf_page_mediabox_key() {
self.parse_rectangle(page.mediabox)
} else {
match self.lookup_direct(relative_to_box, page.rest) {
Some(box_object) => self.parse_rectangle(box_object)
None => self.parse_rectangle(page.mediabox)
}
}
}
///|
fn PdfDocument::pdf_addtext_rectangle_dimensions(
self : PdfDocument,
page : PdfPage,
coord : String,
) -> (Double, Double) raise PdfError {
match self.parse_units_string(page, coord) {
[width, height] => (width, height)
_ => raise SoftError("bad coordinate specification")
}
}
///|
fn PdfDocument::pdf_addtext_rectangle_page(
self : PdfDocument,
page : PdfPage,
coord : String,
colour : PdfContentColour,
position : PdfPosition,
relative_to_box : PdfName,
outline : Bool,
line_width : Double,
opacity : Double,
underneath : Bool,
fast : Bool,
) -> PdfPage raise PdfError {
let (resources, extgstate_name) = self.pdf_addtext_opacity_resources(
page.resources,
opacity,
)
let bounds = self.pdf_addtext_rectangle_bounds(page, relative_to_box)
let (width, height) = self.pdf_addtext_rectangle_dimensions(page, coord)
let (x, y) = pdf_addtext_rectangle_origin(bounds, position, width, height)
let ops = pdf_addtext_rectangle_ops(
x, y, width, height, colour, outline, line_width, extgstate_name,
)
let page = { ..page, resources, }
if underneath {
self.prepend_operators(page, ops, fast~)
} else {
self.postpend_operators(page, ops, fast~)
}
}
///|
/// Add a cpdf-style rectangle stamp to selected one-based pages.
///
/// The rectangle dimensions are parsed with cpdf coordinate syntax, placed
/// relative to `relative_to_box`, wrapped as `/CPDFSTAMP` artifact content, and
/// appended unless `underneath` is set.
pub fn PdfDocument::addtext_rectangle(
self : PdfDocument,
page_numbers : ArrayView[Int],
coord : String,
colour : PdfContentColour,
position : PdfPosition,
relative_to_box? : PdfName = pdf_page_mediabox_key(),
outline? : Bool = false,
line_width? : Double = 1.0,
opacity? : Double = 1.0,
underneath? : Bool = false,
fast? : Bool = false,
) -> PdfDocument raise PdfError {
let pages = self.pages_of_pagetree()
pdf_addtext_validate_page_numbers(
page_numbers,
pages.length(),
"addtext_rectangle",
)
let selected_pages = @number_set.pdf_number_set(page_numbers)
let rewritten : Array[PdfPage] = Array::new(capacity=pages.length())
for index in 0.. PdfDocument raise PdfError {
document.addtext_rectangle(
page_numbers,
coord,
colour,
position,
relative_to_box~,
outline~,
line_width~,
opacity~,
underneath~,
fast~,
)
}
///|
/// Source-spelled compatibility wrapper matching cpdfaddtext's `addrectangle`.
pub fn pdf_addrectangle(
fast : Bool,
coord : String,
colour : PdfContentColour,
outline : Bool,
line_width : Double,
opacity : Double,
position : PdfPosition,
relative_to_box : String,
underneath : Bool,
page_numbers : ArrayView[Int],
document : PdfDocument,
) -> PdfDocument raise PdfError {
document.addtext_rectangle(
page_numbers,
coord,
colour,
position,
relative_to_box=pdf_page_box_name_of_string(relative_to_box),
outline~,
line_width~,
opacity~,
underneath~,
fast~,
)
}
///|
fn pdf_addtext_vertical_justification(
rotation : PdfAddTextRotation,
justification : PdfAddTextJustification,
) -> PdfAddTextJustification {
match justification {
PdfAddTextCentreJustify => PdfAddTextCentreJustify
PdfAddTextLeftJustify =>
if rotation == PdfAddTextRot270 {
PdfAddTextRightJustify
} else {
PdfAddTextLeftJustify
}
PdfAddTextRightJustify =>
if rotation == PdfAddTextRot270 {
PdfAddTextLeftJustify
} else {
PdfAddTextRightJustify
}
}
}
///|
fn pdf_addtext_horizontal_justification(
rotation : PdfAddTextRotation,
justification : PdfAddTextJustification,
) -> PdfAddTextJustification {
match justification {
PdfAddTextCentreJustify => PdfAddTextCentreJustify
PdfAddTextLeftJustify =>
if rotation == PdfAddTextRot180 {
PdfAddTextRightJustify
} else {
PdfAddTextLeftJustify
}
PdfAddTextRightJustify =>
if rotation == PdfAddTextRot180 {
PdfAddTextLeftJustify
} else {
PdfAddTextRightJustify
}
}
}
///|
fn pdf_addtext_vertical_justification_offset(
delta : Double,
position : PdfPosition,
justification : PdfAddTextJustification,
) -> Double {
match justification {
PdfAddTextLeftJustify =>
match position {
PdfPositionTopLeft(_, _)
| PdfPositionTopRight(_, _)
| PdfPositionTop(_)
| PdfPositionDiagonal
| PdfPositionReverseDiagonal => 0.0
PdfPositionLeft(_)
| PdfPositionPosLeft(_, _)
| PdfPositionPosCentre(_, _)
| PdfPositionCentre
| PdfPositionPosRight(_, _)
| PdfPositionRight(_) => delta / 2.0
PdfPositionBottomLeft(_, _)
| PdfPositionBottom(_)
| PdfPositionBottomRight(_, _) => delta
}
PdfAddTextRightJustify =>
match position {
PdfPositionTopLeft(_, _)
| PdfPositionTopRight(_, _)
| PdfPositionTop(_) => -delta
PdfPositionLeft(_)
| PdfPositionPosLeft(_, _)
| PdfPositionPosCentre(_, _)
| PdfPositionCentre
| PdfPositionPosRight(_, _)
| PdfPositionRight(_) => -(delta / 2.0)
PdfPositionBottomLeft(_, _)
| PdfPositionBottom(_)
| PdfPositionBottomRight(_, _)
| PdfPositionDiagonal
| PdfPositionReverseDiagonal => 0.0
}
PdfAddTextCentreJustify =>
match position {
PdfPositionTopLeft(_, _)
| PdfPositionTopRight(_, _)
| PdfPositionTop(_) => -(delta / 2.0)
PdfPositionLeft(_)
| PdfPositionPosLeft(_, _)
| PdfPositionPosCentre(_, _)
| PdfPositionCentre
| PdfPositionPosRight(_, _)
| PdfPositionRight(_)
| PdfPositionDiagonal
| PdfPositionReverseDiagonal => 0.0
PdfPositionBottomLeft(_, _)
| PdfPositionBottom(_)
| PdfPositionBottomRight(_, _) => delta / 2.0
}
}
}
///|
fn pdf_addtext_horizontal_justification_offset(
delta : Double,
position : PdfPosition,
justification : PdfAddTextJustification,
) -> Double {
match justification {
PdfAddTextLeftJustify =>
match position {
PdfPositionTopLeft(_, _)
| PdfPositionLeft(_)
| PdfPositionPosLeft(_, _)
| PdfPositionBottomLeft(_, _)
| PdfPositionDiagonal
| PdfPositionReverseDiagonal => 0.0
PdfPositionTop(_)
| PdfPositionPosCentre(_, _)
| PdfPositionBottom(_)
| PdfPositionCentre => delta / 2.0
PdfPositionTopRight(_, _)
| PdfPositionBottomRight(_, _)
| PdfPositionPosRight(_, _)
| PdfPositionRight(_) => delta
}
PdfAddTextRightJustify =>
match position {
PdfPositionTopLeft(_, _)
| PdfPositionLeft(_)
| PdfPositionPosLeft(_, _)
| PdfPositionBottomLeft(_, _) => -delta
PdfPositionTop(_)
| PdfPositionPosCentre(_, _)
| PdfPositionBottom(_)
| PdfPositionCentre => -(delta / 2.0)
PdfPositionTopRight(_, _)
| PdfPositionBottomRight(_, _)
| PdfPositionPosRight(_, _)
| PdfPositionRight(_)
| PdfPositionDiagonal
| PdfPositionReverseDiagonal => 0.0
}
PdfAddTextCentreJustify =>
match position {
PdfPositionTopLeft(_, _)
| PdfPositionLeft(_)
| PdfPositionPosLeft(_, _)
| PdfPositionBottomLeft(_, _) => -(delta / 2.0)
PdfPositionTop(_)
| PdfPositionPosCentre(_, _)
| PdfPositionBottom(_)
| PdfPositionCentre
| PdfPositionDiagonal
| PdfPositionReverseDiagonal => 0.0
PdfPositionTopRight(_, _)
| PdfPositionBottomRight(_, _)
| PdfPositionPosRight(_, _)
| PdfPositionRight(_) => delta / 2.0
}
}
}
///|
/// Return cpdf's add-text justification offset for the current line.
///
/// `longest_width` is the maximum rendered width among the expanded text lines,
/// and `width` is the current line width.
pub fn pdf_addtext_justification_offset(
rotation : PdfAddTextRotation,
longest_width : Double,
width : Double,
position : PdfPosition,
justification : PdfAddTextJustification,
) -> Double {
let delta = longest_width - width
match rotation {
PdfAddTextRot90 | PdfAddTextRot270 =>
pdf_addtext_vertical_justification_offset(
delta,
position,
pdf_addtext_vertical_justification(rotation, justification),
)
PdfAddTextRot0 | PdfAddTextRot180 =>
pdf_addtext_horizontal_justification_offset(
delta,
position,
pdf_addtext_horizontal_justification(rotation, justification),
)
}
}
///|
/// Apply cpdf's scalar justification offset to `(horizontal, vertical)`.
pub fn pdf_addtext_apply_justification_offset(
rotation : PdfAddTextRotation,
horizontal_offset : Double,
vertical_offset : Double,
justification_offset : Double,
) -> (Double, Double) {
match rotation {
PdfAddTextRot90 | PdfAddTextRot270 =>
(horizontal_offset, vertical_offset - justification_offset)
PdfAddTextRot0 | PdfAddTextRot180 =>
(horizontal_offset + justification_offset, vertical_offset)
}
}
///|
/// Resolve cpdf add-text placement values for one line.
///
/// This combines cpdf's `calculate_position`, diagonal offset correction,
/// justification offset application, explicit text rotation, and
/// rotation-origin offset selection.
pub fn pdf_addtext_line_placement(
bounds : PdfRectangle,
position : PdfPosition,
rotation : PdfAddTextRotation,
text_width : Double,
longest_width : Double,
horizontal_offset : Double,
vertical_offset : Double,
justification : PdfAddTextJustification,
) -> PdfAddTextLinePlacement {
let placed = pdf_calculate_position(false, text_width, bounds, position)
let justification_offset = pdf_addtext_justification_offset(
rotation, longest_width, text_width, position, justification,
)
let (diagonal_h_offset, diagonal_v_offset) = pdf_addtext_diagonal_offsets(
bounds,
position,
rotation,
text_width,
horizontal_offset,
vertical_offset,
placed.rotation,
)
let (final_h_offset, final_v_offset) = pdf_addtext_apply_justification_offset(
rotation, diagonal_h_offset, diagonal_v_offset, justification_offset,
)
let (rotation_h_offset, rotation_v_offset) = pdf_addtext_rotation_offsets(
position, rotation, text_width,
)
{
origin: point2(placed.x, placed.y),
base_rotation: placed.rotation,
text_rotation: pdf_addtext_rotation_radians(rotation),
rotation_offset: point2(rotation_h_offset, rotation_v_offset),
horizontal_offset: final_h_offset,
vertical_offset: final_v_offset,
justification_offset,
}
}
///|
fn pdf_addtext_bytes_match_at(
bytes : BytesView,
index : Int,
pattern : BytesView,
) -> Bool {
if pattern.length() == 0 || index + pattern.length() > bytes.length() {
false
} else {
for offset in 0.. Int {
let mut position = position
for byte in view {
output[position] = byte
position += 1
}
position
}
///|
fn pdf_addtext_replace_all_lazy(
text : String,
search : String,
replacement : () -> String,
) -> String {
let search_bytes = @utf8.encode(search)
if search_bytes.length() == 0 {
text
} else {
let text_bytes = @utf8.encode(text)
let replacements : Array[(Int, Int, PdfBytes)] = Array::new(
capacity=text_bytes.length() / search_bytes.length(),
)
let mut index = 0
let mut length = text_bytes.length()
while index < text_bytes.length() {
if pdf_addtext_bytes_match_at(text_bytes, index, search_bytes) {
let replacement_bytes = @utf8.encode(replacement())
replacements.push(
(index, index + search_bytes.length(), replacement_bytes),
)
length = length - search_bytes.length() + replacement_bytes.length()
index += search_bytes.length()
} else {
index += 1
}
}
guard replacements.length() > 0 else { return text }
let output = Array::make(length, b'\x00')
let mut source_index = 0
let mut position = 0
for entry in replacements {
position = pdf_addtext_copy_view(
output,
position,
text_bytes[source_index:entry.0],
)
position = pdf_addtext_copy_view(output, position, entry.2)
source_index = entry.1
}
let _ = pdf_addtext_copy_view(output, position, text_bytes[source_index:])
@utf8.decode_lossy(Bytes::from_array(output))
}
}
///|
/// 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 : 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)
}
pdf_strftime_with_time(time, output)
}
///|
/// Compatibility wrapper matching cpdfaddtext's `process_text`.
pub fn pdf_process_text(
time : PdfStrftimeTime,
text : String,
replacements : ArrayView[(String, () -> String)],
) -> String {
pdf_addtext_process_text_with_time(time, text, replacements)
}
///|
fn pdf_addtext_decode_lossy_view(bytes : BytesView) -> String {
let output = Array::make(bytes.length(), b'\x00')
for index, byte in bytes {
output[index] = byte
}
@utf8.decode_lossy(Bytes::from_array(output))
}
///|
/// 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::new(capacity=bytes.length())
let mut line_start = 0
let mut index = 0
while index < bytes.length() {
if index + 2 < bytes.length() &&
bytes[index].to_int() == 92 &&
bytes[index + 1].to_int() == 92 &&
bytes[index + 2].to_int() == 110 {
index += 3
} else if index + 1 < bytes.length() &&
bytes[index].to_int() == 92 &&
bytes[index + 1].to_int() == 110 {
lines.push(pdf_addtext_decode_lossy_view(bytes[line_start:index]))
index += 2
line_start = index
} else {
index += 1
}
}
lines.push(pdf_addtext_decode_lossy_view(bytes[line_start:]))
lines
}
///|
fn pdf_addtext_is_octal_digit(byte : Byte) -> Bool {
let value = byte.to_int()
value >= 48 && value <= 55
}
///|
fn pdf_addtext_octal_value(a : Byte, b : Byte, c : Byte) -> Int {
(a.to_int() - 48) * 64 + (b.to_int() - 48) * 8 + c.to_int() - 48
}
///|
fn pdf_addtext_unescaped_length(bytes : BytesView) -> Int raise PdfError {
let mut index = 0
let mut length = 0
while index < bytes.length() {
if bytes[index].to_int() == 92 {
if index + 3 < bytes.length() &&
pdf_addtext_is_octal_digit(bytes[index + 1]) &&
pdf_addtext_is_octal_digit(bytes[index + 2]) &&
pdf_addtext_is_octal_digit(bytes[index + 3]) {
let value = pdf_addtext_octal_value(
bytes[index + 1],
bytes[index + 2],
bytes[index + 3],
)
if value > 255 {
raise SoftError("bad add-text octal escape")
}
length += 1
index += 4
} else if index + 1 < bytes.length() && bytes[index + 1].to_int() == 92 {
length += 1
index += 2
} else if index + 1 < bytes.length() && bytes[index + 1].to_int() != 110 {
length += 1
index += 2
} else {
length += 1
index += 1
}
} else {
length += 1
index += 1
}
}
length
}
///|
/// 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 PdfError {
let bytes = @utf8.encode(text)
let output = Array::make(pdf_addtext_unescaped_length(bytes), b'\x00')
let mut index = 0
let mut position = 0
while index < bytes.length() {
if bytes[index].to_int() == 92 {
if index + 3 < bytes.length() &&
pdf_addtext_is_octal_digit(bytes[index + 1]) &&
pdf_addtext_is_octal_digit(bytes[index + 2]) &&
pdf_addtext_is_octal_digit(bytes[index + 3]) {
let value = pdf_addtext_octal_value(
bytes[index + 1],
bytes[index + 2],
bytes[index + 3],
)
if value > 255 {
raise SoftError("bad add-text octal escape")
}
output[position] = value.to_byte()
position += 1
index += 4
} else if index + 1 < bytes.length() && bytes[index + 1].to_int() == 92 {
output[position] = (92).to_byte()
position += 1
index += 2
} else if index + 1 < bytes.length() && bytes[index + 1].to_int() != 110 {
output[position] = bytes[index + 1]
position += 1
index += 2
} else {
output[position] = bytes[index]
position += 1
index += 1
}
} else {
output[position] = bytes[index]
position += 1
index += 1
}
}
@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 : PdfAddTextRotation,
) -> Array[String] raise 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
}
}
///|
fn pdf_addtext_roman_upper(number : Int) -> String {
@ascii.decode_lossy(pdf_page_label_roman_upper(number))
}
///|
fn pdf_addtext_roman_lower(number : Int) -> String {
@ascii.decode_lossy(
pdf_page_label_lowercase(pdf_page_label_roman_upper(number)),
)
}
///|
/// Format cpdf add-text's `%Bates` replacement for one page.
pub fn pdf_addtext_bates_number(
bates : Int,
page_number : Int,
bates_pad : Int?,
) -> String {
let number_text = (bates + page_number - 1).to_string()
match bates_pad {
None => number_text
Some(width) =>
if number_text.length() >= width {
number_text
} else {
let mut output = number_text
for _ in 0..<(width - number_text.length()) {
output = "0" + output
}
output
}
}
}
///|
fn pdf_addtext_bookmark_replacement(
values : PdfAddTextReplacementValues,
level : Int,
) -> String {
if level >= 0 && level < values.bookmarks.length() {
values.bookmarks[level]
} else {
""
}
}
///|
/// Build cpdf add-text replacement callbacks for page-local fields.
///
/// Page labels and bookmark strings are supplied by the caller; this helper
/// preserves cpdf's marker names and Bates/roman/page-number formatting.
pub fn pdf_addtext_replacement_pairs(
values : PdfAddTextReplacementValues,
) -> Array[(String, () -> String)] {
[
("%PageDiv2", fn() { ((values.page_number + 1) / 2).to_string() }),
("%Page", fn() { values.page_number.to_string() }),
("%Roman", fn() { pdf_addtext_roman_upper(values.page_number) }),
("%roman", fn() { pdf_addtext_roman_lower(values.page_number) }),
("%filename", fn() { values.filename }),
("%Label", fn() { values.page_label }),
("%EndPage", fn() { values.end_page.to_string() }),
("%EndLabel", fn() { values.end_label }),
("%Bookmark0", fn() { pdf_addtext_bookmark_replacement(values, 0) }),
("%Bookmark1", fn() { pdf_addtext_bookmark_replacement(values, 1) }),
("%Bookmark2", fn() { pdf_addtext_bookmark_replacement(values, 2) }),
("%Bookmark3", fn() { pdf_addtext_bookmark_replacement(values, 3) }),
("%Bookmark4", fn() { pdf_addtext_bookmark_replacement(values, 4) }),
(
"%Bates",
fn() {
pdf_addtext_bates_number(
values.bates,
values.page_number,
values.bates_pad,
)
},
),
]
}
///|
fn pdf_addtext_pdfdocstring_to_string(
bytes : BytesView,
) -> String raise PdfError {
@utf8.decode(pdf_utf8_of_pdfdocstring(bytes)) catch {
_ => raise InvalidUTF8
}
}
///|
fn pdf_addtext_page_label_prefix(label : PdfPageLabel) -> String raise PdfError {
match label.label_prefix {
Some(prefix) => pdf_addtext_pdfdocstring_to_string(prefix)
None => ""
}
}
///|
fn pdf_addtext_page_label_number(label : PdfPageLabel) -> String {
@ascii.decode_lossy(
pdf_page_label_number_bytes(label.start_value, label.label_style),
)
}
///|
/// Return cpdf add-text's visible page label for a 1-based page number.
///
/// Documents without page labels fall back to decimal page numbers.
pub fn PdfDocument::addtext_page_label(
self : PdfDocument,
page_number : Int,
) -> String raise PdfError {
let labels = pdf_page_label_complete(self.read_page_labels())
match pdf_page_label_of_page_number(page_number, labels) {
Some(label) =>
pdf_addtext_page_label_prefix(label) +
pdf_addtext_page_label_number(label)
None => page_number.to_string()
}
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_page_label`.
pub fn pdf_addtext_page_label(
document : PdfDocument,
page_number : Int,
) -> String raise PdfError {
document.addtext_page_label(page_number)
}
///|
fn pdf_addtext_bookmark_text(bookmark : PdfBookmark) -> String raise PdfError {
pdf_addtext_pdfdocstring_to_string(bookmark.text)
}
///|
/// Return cpdf add-text's current bookmark text at `level` for `page_number`.
///
/// The search is constrained to the current bookmark branch so section
/// bookmarks from a previous chapter do not leak into a later chapter.
pub fn PdfDocument::addtext_bookmark_for_page(
self : PdfDocument,
bookmarks : ArrayView[PdfBookmark],
level : Int,
page_number : Int,
) -> String raise PdfError {
let before : Array[PdfBookmark] = Array::new(capacity=bookmarks.length())
for bookmark in bookmarks {
if self.page_number_of_target(bookmark.target) <= page_number {
before.push(bookmark)
} else {
break
}
}
let branch : Array[PdfBookmark] = Array::new(capacity=before.length())
let mut before_index = before.length()
while before_index > 0 {
before_index -= 1
let bookmark = before[before_index]
if bookmark.level >= level {
branch.push(bookmark)
} else {
break
}
}
let pickfrom : Array[PdfBookmark] = Array::new(capacity=branch.length())
let mut branch_index = branch.length()
while branch_index > 0 {
branch_index -= 1
let bookmark = branch[branch_index]
if bookmark.level == level {
pickfrom.push(bookmark)
}
}
let mut fallback : PdfBookmark? = None
for bookmark in pickfrom {
if self.page_number_of_target(bookmark.target) == page_number {
return pdf_addtext_bookmark_text(bookmark)
}
fallback = Some(bookmark)
}
match fallback {
Some(bookmark) => pdf_addtext_bookmark_text(bookmark)
None => ""
}
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_bookmark_for_page`.
pub fn pdf_addtext_bookmark_for_page(
document : PdfDocument,
bookmarks : ArrayView[PdfBookmark],
level : Int,
page_number : Int,
) -> String raise PdfError {
document.addtext_bookmark_for_page(bookmarks, level, page_number)
}
///|
/// Build cpdf add-text replacement values from document-local state.
pub fn PdfDocument::addtext_replacement_values(
self : PdfDocument,
bookmarks : ArrayView[PdfBookmark],
page_number : Int,
end_page : Int,
filename : String,
bates : Int,
bates_pad : Int?,
) -> PdfAddTextReplacementValues raise PdfError {
let bookmark_values : Array[String] = Array::new(capacity=5)
for level in 0..<5 {
bookmark_values.push(
self.addtext_bookmark_for_page(bookmarks, level, page_number),
)
}
{
page_number,
end_page,
filename,
page_label: self.addtext_page_label(page_number),
end_label: self.addtext_page_label(end_page),
bates,
bates_pad,
bookmarks: bookmark_values,
}
}
///|
/// Compatibility wrapper for `PdfDocument::addtext_replacement_values`.
pub fn pdf_addtext_replacement_values(
document : PdfDocument,
bookmarks : ArrayView[PdfBookmark],
page_number : Int,
end_page : Int,
filename : String,
bates : Int,
bates_pad : Int?,
) -> PdfAddTextReplacementValues raise PdfError {
document.addtext_replacement_values(
bookmarks, page_number, end_page, filename, bates, bates_pad,
)
}
///|
/// Source-spelled compatibility wrapper matching cpdfaddtext's `replace_pairs`.
///
/// The OCaml API receives precomputed page-reference lookup state and the page
/// record; this wrapper derives the same page-local values from `document`.
pub fn pdf_replace_pairs(
bookmarks : ArrayView[PdfBookmark],
document : PdfDocument,
end_page : Int,
filename : String,
bates : Int,
bates_pad : Int?,
page_number : Int,
) -> Array[(String, () -> String)] raise PdfError {
let values = document.addtext_replacement_values(
bookmarks, page_number, end_page, filename, bates, bates_pad,
)
pdf_addtext_replacement_pairs(values)
}
///|
fn pdf_addtext_afm_header_value(data : PdfAfmData, key : String) -> String? {
let key_bytes = @ascii.encode(key)
for entry in data.headers {
if pdf_afm_view_equals(entry.0, key_bytes) {
return Some(@ascii.decode_lossy(entry.1))
}
}
None
}
///|
fn pdf_addtext_parse_double(text : String) -> Double? {
let parsed : Result[Double, Error] = try? @strconv.from_str(text)
match parsed {
Ok(value) => Some(value)
Err(_) => None
}
}
///|
fn pdf_addtext_standard_cap_height(fontname : String) -> Double? {
match pdf_standard_font_of_name("/" + fontname) {
Some(font) => {
let data = font.afm_data()
match pdf_addtext_afm_header_value(data, "CapHeight") {
Some(value) =>
match pdf_addtext_parse_double(value) {
Some(parsed) => Some(parsed)
None => Some(0.0)
}
None => Some(0.0)
}
}
None => None
}
}
///|
/// Return the cap height used by cpdf add-text URL link rectangles.
///
/// Explicit simple-font descriptors win. Otherwise `fontname` is resolved as a
/// Standard 14 font name without a leading slash, matching cpdf's fallback.
pub fn pdf_addtext_cap_height(font : PdfFont?, fontname : String) -> Double? {
match font {
Some(PdfFontSimple({ fontdescriptor: Some(descriptor), .. })) =>
Some(descriptor.capheight)
_ => pdf_addtext_standard_cap_height(fontname)
}
}
///|
fn pdf_addtext_find_byte(bytes : BytesView, start : Int, value : Int) -> Int? {
let mut index = start
while index < bytes.length() {
if bytes[index].to_int() == value {
return Some(index)
}
index += 1
}
None
}
///|
fn pdf_addtext_require_byte(
bytes : BytesView,
start : Int,
value : Int,
) -> Int raise PdfError {
match pdf_addtext_find_byte(bytes, start, value) {
Some(index) => index
None => raise SoftError("bad URL syntax in text")
}
}
///|
fn pdf_addtext_is_url_marker(bytes : BytesView, index : Int) -> Bool {
index + 4 < bytes.length() &&
bytes[index].to_int() == 37 &&
bytes[index + 1].to_int() == 85 &&
bytes[index + 2].to_int() == 82 &&
bytes[index + 3].to_int() == 76 &&
bytes[index + 4].to_int() == 91
}
///|
fn pdf_addtext_utf8_string(bytes : BytesView) -> String raise PdfError {
@utf8.decode(bytes) catch {
_ => raise InvalidUTF8
}
}
///|
fn pdf_addtext_url_visible_length(bytes : BytesView) -> Int raise PdfError {
let mut index = 0
let mut length = 0
while index < bytes.length() {
if pdf_addtext_is_url_marker(bytes, index) {
let text_start = index + 5
let pipe = pdf_addtext_require_byte(bytes, text_start, 124)
let close = pdf_addtext_require_byte(bytes, pipe + 1, 93)
length += pipe - text_start
index = close + 1
} else {
length += 1
index += 1
}
}
length
}
///|
/// Strip cpdf add-text `%URL[text|url]` markup from one line.
///
/// The returned URL spans use byte offsets in the stripped visible text, as in
/// cpdf's `get_urls_line` helper.
pub fn pdf_addtext_get_urls_line(
line : String,
) -> PdfAddTextUrlLine raise PdfError {
let bytes = @utf8.encode(line)
let output = Array::make(pdf_addtext_url_visible_length(bytes), b'\x00')
let urls : Array[PdfAddTextUrl] = Array::new(capacity=bytes.length() / 5)
let mut index = 0
let mut position = 0
while index < bytes.length() {
if pdf_addtext_is_url_marker(bytes, index) {
let text_start = index + 5
let pipe = pdf_addtext_require_byte(bytes, text_start, 124)
let close = pdf_addtext_require_byte(bytes, pipe + 1, 93)
let start = position
for byte in bytes[text_start:pipe] {
output[position] = byte
position += 1
}
let url = pdf_addtext_utf8_string(bytes[pipe + 1:close])
let end = position
urls.push({ url, start, end })
index = close + 1
} else {
output[position] = bytes[index]
position += 1
index += 1
}
}
{ text: pdf_addtext_utf8_string(Bytes::from_array(output)), urls }
}
///|
/// Expand add-text lines and strip URL markup before line measurement.
///
/// This mirrors cpdf's `expand_lines` behavior after callers have supplied the
/// document-specific replacement callbacks.
pub fn pdf_addtext_expand_lines_with_time(
time : PdfStrftimeTime,
lines : ArrayView[String],
replacements : ArrayView[(String, () -> String)],
) -> Array[String] raise PdfError {
let output : Array[String] = Array::new(capacity=lines.length())
for line in lines {
let processed = pdf_addtext_process_text_with_time(time, line, replacements)
output.push(pdf_addtext_get_urls_line(processed).text)
}
output
}
///|
fn pdf_addtext_codepoints_contains(
codepoints : ArrayView[Int],
codepoint : Int,
) -> Bool {
for existing in codepoints {
if existing == codepoint {
return true
}
}
false
}
///|
/// Return the unique Unicode codepoints used by expanded add-text lines.
///
/// This mirrors cpdf's pre-embedding `used` scan after replacement expansion
/// and URL stripping. The returned order is first occurrence for deterministic
/// callers and tests.
pub fn pdf_addtext_used_codepoints(
lines : ArrayView[String],
) -> Array[Int] raise PdfError {
let used : Array[Int] = Array::new(capacity=lines.length())
for line in lines {
for codepoint in pdf_codepoints_of_utf8(@utf8.encode(line)) {
if !pdf_addtext_codepoints_contains(used, codepoint) {
used.push(codepoint)
}
}
}
used
}