///|
fn pdf_annotation_name(text : String) -> PdfName {
pdf_name_of_bytes(@ascii.encode(text))
}
///|
let pdf_annotation_empty_name : PdfName = pdf_annotation_name("")
///|
let pdf_annotation_subtype_key_name : PdfName = pdf_annotation_name("/Subtype")
///|
fn pdf_annotation_subtype_key() -> PdfName {
pdf_annotation_subtype_key_name
}
///|
let pdf_annotation_contents_key_name : PdfName = pdf_annotation_name(
"/Contents",
)
///|
fn pdf_annotation_contents_key() -> PdfName {
pdf_annotation_contents_key_name
}
///|
let pdf_annotation_rect_key_name : PdfName = pdf_annotation_name("/Rect")
///|
fn pdf_annotation_rect_key() -> PdfName {
pdf_annotation_rect_key_name
}
///|
let pdf_annotation_border_key_name : PdfName = pdf_annotation_name("/Border")
///|
fn pdf_annotation_border_key() -> PdfName {
pdf_annotation_border_key_name
}
///|
let pdf_annotation_bs_key_name : PdfName = pdf_annotation_name("/BS")
///|
fn pdf_annotation_bs_key() -> PdfName {
pdf_annotation_bs_key_name
}
///|
let pdf_annotation_subj_key_name : PdfName = pdf_annotation_name("/Subj")
///|
fn pdf_annotation_subj_key() -> PdfName {
pdf_annotation_subj_key_name
}
///|
let pdf_annotation_colour_key_name : PdfName = pdf_annotation_name("/C")
///|
fn pdf_annotation_colour_key() -> PdfName {
pdf_annotation_colour_key_name
}
///|
let pdf_annotation_parent_key_name : PdfName = pdf_annotation_name("/Parent")
///|
fn pdf_annotation_parent_key() -> PdfName {
pdf_annotation_parent_key_name
}
///|
let pdf_annotation_annots_key_name : PdfName = pdf_annotation_name("/Annots")
///|
fn pdf_annotation_annots_key() -> PdfName {
pdf_annotation_annots_key_name
}
///|
let pdf_annotation_p_key_name : PdfName = pdf_annotation_name("/P")
///|
fn pdf_annotation_p_key() -> PdfName {
pdf_annotation_p_key_name
}
///|
let pdf_annotation_dest_key_name : PdfName = pdf_annotation_name("/Dest")
///|
fn pdf_annotation_dest_key() -> PdfName {
pdf_annotation_dest_key_name
}
///|
let pdf_annotation_a_key_name : PdfName = pdf_annotation_name("/A")
///|
fn pdf_annotation_a_key() -> PdfName {
pdf_annotation_a_key_name
}
///|
let pdf_annotation_d_key_name : PdfName = pdf_annotation_name("/D")
///|
fn pdf_annotation_d_key() -> PdfName {
pdf_annotation_d_key_name
}
///|
let pdf_annotation_width_key_name : PdfName = pdf_annotation_name("/W")
///|
fn pdf_annotation_width_key() -> PdfName {
pdf_annotation_width_key_name
}
///|
let pdf_annotation_style_key_name : PdfName = pdf_annotation_name("/S")
///|
fn pdf_annotation_style_key() -> PdfName {
pdf_annotation_style_key_name
}
///|
let pdf_annotation_dash_key_name : PdfName = pdf_annotation_name("/D")
///|
fn pdf_annotation_dash_key() -> PdfName {
pdf_annotation_dash_key_name
}
///|
let pdf_annotation_quadpoints_key_name : PdfName = pdf_annotation_name(
"/QuadPoints",
)
///|
fn pdf_annotation_quadpoints_key() -> PdfName {
pdf_annotation_quadpoints_key_name
}
///|
let pdf_annotation_line_key_name : PdfName = pdf_annotation_name("/L")
///|
fn pdf_annotation_line_key() -> PdfName {
pdf_annotation_line_key_name
}
///|
let pdf_annotation_beveled_name : PdfName = pdf_annotation_name("/B")
///|
let pdf_annotation_inset_name : PdfName = pdf_annotation_name("/I")
///|
let pdf_annotation_underline_style_name : PdfName = pdf_annotation_name("/U")
///|
let pdf_annotation_text_name : PdfName = pdf_annotation_name("/Text")
///|
let pdf_annotation_link_name : PdfName = pdf_annotation_name("/Link")
///|
let pdf_annotation_free_text_name : PdfName = pdf_annotation_name("/FreeText")
///|
let pdf_annotation_line_name : PdfName = pdf_annotation_name("/Line")
///|
let pdf_annotation_square_name : PdfName = pdf_annotation_name("/Square")
///|
let pdf_annotation_circle_name : PdfName = pdf_annotation_name("/Circle")
///|
let pdf_annotation_polygon_name : PdfName = pdf_annotation_name("/Polygon")
///|
let pdf_annotation_polyline_name : PdfName = pdf_annotation_name("/PolyLine")
///|
let pdf_annotation_highlight_name : PdfName = pdf_annotation_name("/Highlight")
///|
let pdf_annotation_underline_name : PdfName = pdf_annotation_name("/Underline")
///|
let pdf_annotation_squiggly_name : PdfName = pdf_annotation_name("/Squiggly")
///|
let pdf_annotation_strikeout_name : PdfName = pdf_annotation_name("/StrikeOut")
///|
let pdf_annotation_stamp_name : PdfName = pdf_annotation_name("/Stamp")
///|
let pdf_annotation_caret_name : PdfName = pdf_annotation_name("/Caret")
///|
let pdf_annotation_ink_name : PdfName = pdf_annotation_name("/Ink")
///|
let pdf_annotation_popup_name : PdfName = pdf_annotation_name("/Popup")
///|
let pdf_annotation_file_attachment_name : PdfName = pdf_annotation_name(
"/FileAttachment",
)
///|
let pdf_annotation_sound_name : PdfName = pdf_annotation_name("/Sound")
///|
let pdf_annotation_movie_name : PdfName = pdf_annotation_name("/Movie")
///|
let pdf_annotation_widget_name : PdfName = pdf_annotation_name("/Widget")
///|
let pdf_annotation_screen_name : PdfName = pdf_annotation_name("/Screen")
///|
let pdf_annotation_printer_mark_name : PdfName = pdf_annotation_name(
"/PrinterMark",
)
///|
let pdf_annotation_trap_net_name : PdfName = pdf_annotation_name("/TrapNet")
///|
let pdf_annotation_watermark_name : PdfName = pdf_annotation_name("/Watermark")
///|
let pdf_annotation_three_dee_name : PdfName = pdf_annotation_name("/3D")
///|
let pdf_annotation_unknown_name : PdfName = pdf_annotation_name("/Unknown")
///|
/// Border style used by an annotation border dictionary.
pub(all) enum PdfAnnotationStyle {
AnnotationNoStyle
AnnotationSolid
AnnotationDashed
AnnotationBeveled
AnnotationInset
AnnotationUnderlineStyle
} derive(Debug, Eq, ToJson)
///|
/// Parsed annotation border information.
///
/// `vertical_radius` and `horizontal_radius` come from the legacy `/Border`
/// array form. `style` and `dash_array` represent `/BS` dictionary entries when
/// present.
pub(all) struct PdfAnnotationBorder {
width : Double
vertical_radius : Double
horizontal_radius : Double
style : PdfAnnotationStyle
dash_array : Array[Int]
} derive(Debug, Eq, ToJson)
///|
/// RGB annotation colour from the `/C` entry.
///
/// Components are stored as integer channel values after parsing PDF numbers.
pub(all) struct PdfAnnotationColour {
red : Int
green : Int
blue : Int
} derive(Debug, Eq, ToJson)
///|
/// Supported annotation subtype.
///
/// Popup annotations embed their resolved parent annotation when available.
/// Unknown subtype names are preserved as `AnnotationUnknown`.
pub(all) enum PdfAnnotationSubtype {
AnnotationText
AnnotationLink
AnnotationFreeText
AnnotationLine
AnnotationSquare
AnnotationCircle
AnnotationPolygon
AnnotationPolyLine
AnnotationHighlight
AnnotationUnderline
AnnotationSquiggly
AnnotationStrikeOut
AnnotationStamp
AnnotationCaret
AnnotationInk
AnnotationPopup(PdfAnnotation)
AnnotationFileAttachment
AnnotationSound
AnnotationMovie
AnnotationWidget
AnnotationScreen
AnnotationPrinterMark
AnnotationTrapNet
AnnotationWatermark
AnnotationThreeDee
AnnotationUnknown(PdfName)
} derive(Debug, Eq, ToJson)
///|
/// Parsed PDF annotation dictionary.
///
/// `contents` and `subject` are raw PDF string bytes. `rectangle` is required
/// when reading from a PDF. `rest` preserves dictionary entries not modeled by
/// the typed fields so they can be written back.
pub(all) struct PdfAnnotation {
subtype : PdfAnnotationSubtype
contents : PdfBytes?
subject : PdfBytes?
rectangle : PdfRectangle
border : PdfAnnotationBorder
colour : PdfAnnotationColour?
rest : PdfObject
} derive(Debug, Eq, ToJson)
///|
fn pdf_annotation_default_border() -> PdfAnnotationBorder {
{
width: 1.0,
vertical_radius: 0.0,
horizontal_radius: 0.0,
style: AnnotationNoStyle,
dash_array: [],
}
}
///|
fn pdf_annotation_zero_rectangle() -> PdfRectangle {
{ min_x: 0.0, min_y: 0.0, max_x: 0.0, max_y: 0.0 }
}
///|
/// Build an annotation border, copying the supplied dash array.
pub fn pdf_annotation_make_border(
width : Double,
vertical_radius? : Double = 0.0,
horizontal_radius? : Double = 0.0,
style? : PdfAnnotationStyle = AnnotationNoStyle,
dash_array? : ArrayView[Int] = [],
) -> PdfAnnotationBorder {
{
width,
vertical_radius,
horizontal_radius,
style,
dash_array: [
for dash in dash_array => dash
],
}
}
///|
/// Build an annotation value with CamlPDF-compatible defaults.
///
/// Defaults are no contents, no subject, a zero rectangle, default border, no
/// colour, and no preserved extra dictionary entries.
pub fn pdf_annotation_make(
subtype : PdfAnnotationSubtype,
contents? : PdfBytes? = None,
border? : PdfAnnotationBorder = pdf_annotation_default_border(),
rectangle? : PdfRectangle = pdf_annotation_zero_rectangle(),
colour? : PdfAnnotationColour? = None,
subject? : PdfBytes? = None,
) -> PdfAnnotation {
{ subtype, contents, subject, rectangle, border, colour, rest: PdfNull }
}
///|
fn pdf_annotation_style_of_name(name : PdfName) -> PdfAnnotationStyle {
if name == pdf_annotation_style_key() {
AnnotationSolid
} else if name == pdf_annotation_dash_key() {
AnnotationDashed
} else if name == pdf_annotation_beveled_name {
AnnotationBeveled
} else if name == pdf_annotation_inset_name {
AnnotationInset
} else if name == pdf_annotation_underline_style_name {
AnnotationUnderlineStyle
} else {
AnnotationNoStyle
}
}
///|
fn pdf_annotation_subtype_name(subtype : PdfAnnotationSubtype) -> PdfName {
match subtype {
AnnotationText => pdf_annotation_text_name
AnnotationLink => pdf_annotation_link_name
AnnotationFreeText => pdf_annotation_free_text_name
AnnotationLine => pdf_annotation_line_name
AnnotationSquare => pdf_annotation_square_name
AnnotationCircle => pdf_annotation_circle_name
AnnotationPolygon => pdf_annotation_polygon_name
AnnotationPolyLine => pdf_annotation_polyline_name
AnnotationHighlight => pdf_annotation_highlight_name
AnnotationUnderline => pdf_annotation_underline_name
AnnotationSquiggly => pdf_annotation_squiggly_name
AnnotationStrikeOut => pdf_annotation_strikeout_name
AnnotationStamp => pdf_annotation_stamp_name
AnnotationCaret => pdf_annotation_caret_name
AnnotationInk => pdf_annotation_ink_name
AnnotationPopup(_) => pdf_annotation_popup_name
AnnotationFileAttachment => pdf_annotation_file_attachment_name
AnnotationSound => pdf_annotation_sound_name
AnnotationMovie => pdf_annotation_movie_name
AnnotationWidget => pdf_annotation_widget_name
AnnotationScreen => pdf_annotation_screen_name
AnnotationPrinterMark => pdf_annotation_printer_mark_name
AnnotationTrapNet => pdf_annotation_trap_net_name
AnnotationWatermark => pdf_annotation_watermark_name
AnnotationThreeDee => pdf_annotation_three_dee_name
AnnotationUnknown(_) => pdf_annotation_unknown_name
}
}
///|
fn PdfDocument::pdf_annotation_subtype(
self : PdfDocument,
annotation : PdfObject,
depth : Int,
) -> PdfAnnotationSubtype raise PdfError {
match self.lookup_direct(pdf_annotation_subtype_key(), annotation) {
Some(PdfNameObject(name)) if name == pdf_annotation_text_name =>
AnnotationText
Some(PdfNameObject(name)) if name == pdf_annotation_free_text_name =>
AnnotationFreeText
Some(PdfNameObject(name)) if name == pdf_annotation_popup_name =>
match
self.direct(annotation).lookup_immediate(pdf_annotation_parent_key()) {
Some(PdfIndirect(parent)) if depth < 64 =>
AnnotationPopup(
self.pdf_annotation_read_depth(PdfIndirect(parent), depth + 1),
)
_ => AnnotationUnknown(pdf_annotation_empty_name)
}
Some(PdfNameObject(name)) if name == pdf_annotation_stamp_name =>
AnnotationStamp
Some(PdfNameObject(name)) if name == pdf_annotation_link_name =>
AnnotationLink
Some(PdfNameObject(name)) if name == pdf_annotation_line_name =>
AnnotationLine
Some(PdfNameObject(name)) if name == pdf_annotation_square_name =>
AnnotationSquare
Some(PdfNameObject(name)) if name == pdf_annotation_circle_name =>
AnnotationCircle
Some(PdfNameObject(name)) if name == pdf_annotation_polygon_name =>
AnnotationPolygon
Some(PdfNameObject(name)) if name == pdf_annotation_polyline_name =>
AnnotationPolyLine
Some(PdfNameObject(name)) if name == pdf_annotation_highlight_name =>
AnnotationHighlight
Some(PdfNameObject(name)) if name == pdf_annotation_underline_name =>
AnnotationUnderline
Some(PdfNameObject(name)) if name == pdf_annotation_squiggly_name =>
AnnotationSquiggly
Some(PdfNameObject(name)) if name == pdf_annotation_strikeout_name =>
AnnotationStrikeOut
Some(PdfNameObject(name)) if name == pdf_annotation_caret_name =>
AnnotationCaret
Some(PdfNameObject(name)) if name == pdf_annotation_ink_name =>
AnnotationInk
Some(PdfNameObject(name)) if name == pdf_annotation_file_attachment_name =>
AnnotationFileAttachment
Some(PdfNameObject(name)) if name == pdf_annotation_sound_name =>
AnnotationSound
Some(PdfNameObject(name)) if name == pdf_annotation_movie_name =>
AnnotationMovie
Some(PdfNameObject(name)) if name == pdf_annotation_widget_name =>
AnnotationWidget
Some(PdfNameObject(name)) if name == pdf_annotation_screen_name =>
AnnotationScreen
Some(PdfNameObject(name)) if name == pdf_annotation_printer_mark_name =>
AnnotationPrinterMark
Some(PdfNameObject(name)) if name == pdf_annotation_trap_net_name =>
AnnotationTrapNet
Some(PdfNameObject(name)) if name == pdf_annotation_watermark_name =>
AnnotationWatermark
Some(PdfNameObject(name)) if name == pdf_annotation_three_dee_name =>
AnnotationThreeDee
Some(PdfNameObject(name)) => AnnotationUnknown(name)
_ => AnnotationUnknown(pdf_annotation_empty_name)
}
}
///|
fn PdfDocument::pdf_annotation_number_array(
self : PdfDocument,
object : PdfObject,
) -> Array[Int] raise PdfError {
match self.direct(object) {
PdfArray(items) => {
let output : Array[Int] = []
for item in items {
output.push(self.get_number(item).to_int())
}
output
}
_ => []
}
}
///|
fn PdfDocument::pdf_annotation_border(
self : PdfDocument,
annotation : PdfObject,
) -> PdfAnnotationBorder raise PdfError {
match self.lookup_direct(pdf_annotation_bs_key(), annotation) {
Some(border_style_dict) => {
let width = match
self.lookup_direct(pdf_annotation_width_key(), border_style_dict) {
Some(width) => self.get_number(width)
_ => 1.0
}
let style = match
self.lookup_direct(pdf_annotation_style_key(), border_style_dict) {
Some(PdfNameObject(name)) => pdf_annotation_style_of_name(name)
_ => AnnotationNoStyle
}
let dash_array = match
self.lookup_direct(pdf_annotation_dash_key(), border_style_dict) {
Some(dash) => self.pdf_annotation_number_array(dash)
_ => []
}
{ width, vertical_radius: 0.0, horizontal_radius: 0.0, style, dash_array }
}
None =>
match self.lookup_direct(pdf_annotation_border_key(), annotation) {
Some(PdfArray([horizontal, vertical, width])) =>
{
width: self.get_number(width),
vertical_radius: self.get_number(vertical),
horizontal_radius: self.get_number(horizontal),
style: AnnotationNoStyle,
dash_array: [],
}
Some(PdfArray([horizontal, vertical, width, dash_array])) =>
{
width: self.get_number(width),
vertical_radius: self.get_number(vertical),
horizontal_radius: self.get_number(horizontal),
style: AnnotationNoStyle,
dash_array: self.pdf_annotation_number_array(dash_array),
}
_ => pdf_annotation_default_border()
}
}
}
///|
fn PdfDocument::pdf_annotation_colour(
self : PdfDocument,
annotation : PdfObject,
) -> PdfAnnotationColour? raise PdfError {
match self.lookup_direct(pdf_annotation_colour_key(), annotation) {
Some(PdfArray([red, green, blue])) =>
Some({
red: self.get_number(red).to_int(),
green: self.get_number(green).to_int(),
blue: self.get_number(blue).to_int(),
})
_ => None
}
}
///|
fn pdf_annotation_is_record_key(key : PdfName) -> Bool {
key == pdf_annotation_subtype_key() ||
key == pdf_annotation_contents_key() ||
key == pdf_annotation_rect_key() ||
key == pdf_annotation_border_key() ||
key == pdf_annotation_subj_key() ||
key == pdf_annotation_bs_key() ||
key == pdf_annotation_colour_key()
}
///|
fn pdf_annotation_rest(annotation : PdfObject) -> PdfObject raise PdfError {
match annotation {
PdfDictionary(entries) => {
let output : Array[(PdfName, PdfObject)] = []
for entry in entries {
let (key, _) = entry
if !pdf_annotation_is_record_key(key) {
output.push(entry)
}
}
PdfDictionary(output)
}
_ => raise DictionaryExpected
}
}
///|
fn PdfDocument::pdf_annotation_read_depth(
self : PdfDocument,
annotation : PdfObject,
depth : Int,
) -> PdfAnnotation raise PdfError {
let direct_annotation = self.direct(annotation)
{
subtype: self.pdf_annotation_subtype(direct_annotation, depth),
contents: match
self.lookup_direct(pdf_annotation_contents_key(), direct_annotation) {
Some(PdfString(contents)) => Some(contents)
_ => None
},
subject: match
self.lookup_direct(pdf_annotation_subj_key(), direct_annotation) {
Some(PdfString(subject)) => Some(subject)
_ => None
},
rectangle: match
self.lookup_direct(pdf_annotation_rect_key(), direct_annotation) {
Some(rectangle) => self.parse_rectangle(rectangle)
None => raise RectangleExpected
},
border: self.pdf_annotation_border(direct_annotation),
colour: self.pdf_annotation_colour(direct_annotation),
rest: pdf_annotation_rest(direct_annotation),
}
}
///|
/// Read one annotation dictionary from this document.
///
/// The annotation object may be direct or indirect. `/Rect` is required. Popup
/// parents are resolved recursively up to a bounded depth to avoid cycles.
pub fn PdfDocument::read_annotation(
self : PdfDocument,
annotation : PdfObject,
) -> PdfAnnotation raise PdfError {
self.pdf_annotation_read_depth(annotation, 0)
}
///|
fn PdfDocument::pdf_annotation_popup_parent(
self : PdfDocument,
annotation : PdfObject,
) -> Int? {
match self.direct(annotation).lookup_immediate(pdf_annotation_parent_key()) {
Some(PdfIndirect(parent)) => Some(parent)
_ => None
}
}
///|
/// Read annotations from a page's `/Annots` array.
///
/// Missing or malformed `/Annots` entries are treated as no annotations. When a
/// popup annotation and its parent both appear in the page array, the parent is
/// skipped and represented through `AnnotationPopup`.
pub fn PdfDocument::annotations_of_page(
self : PdfDocument,
page : PdfPage,
) -> Array[PdfAnnotation] raise PdfError {
match self.lookup_direct(pdf_annotation_annots_key(), page.rest) {
Some(PdfArray(annotations)) => {
let popup_parents : Array[Int] = []
for annotation in annotations {
match self.pdf_annotation_popup_parent(annotation) {
Some(parent) => popup_parents.push(parent)
None => ()
}
}
let popup_parent_set = @number_set.pdf_number_set(popup_parents)
let output : Array[PdfAnnotation] = []
for annotation in annotations {
match annotation {
PdfIndirect(number) if popup_parent_set.contains(number) => ()
_ => output.push(self.read_annotation(annotation))
}
}
output
}
_ => []
}
}
///|
/// Compatibility wrapper for `PdfDocument::annotations_of_page`.
pub fn pdf_annotations_of_page(
document : PdfDocument,
page : PdfPage,
) -> Array[PdfAnnotation] raise PdfError {
document.annotations_of_page(page)
}
///|
fn PdfDocument::pdf_annotation_contents_text(
self : PdfDocument,
encoding : PdfMetadataEncoding,
annotation : PdfObject,
) -> PdfBytes raise PdfError {
match self.lookup_direct(pdf_annotation_contents_key(), annotation) {
Some(PdfString(contents)) => pdf_metadata_encode_output(encoding, contents)
_ => []
}
}
///|
fn pdf_annotation_write_listing_view(
output : Array[Byte],
position : Int,
data : BytesView,
) -> Int {
let mut current = position
for byte in data {
output[current] = byte
current += 1
}
current
}
///|
fn pdf_annotation_write_listing_ascii(
output : Array[Byte],
position : Int,
text : String,
) -> Int {
pdf_annotation_write_listing_view(output, position, @ascii.encode(text))
}
///|
fn pdf_annotation_write_listing_line(
output : Array[Byte],
position : Int,
page_number : Int,
contents : BytesView,
) -> Int {
let mut current = pdf_annotation_write_listing_ascii(
output, position, "Page ",
)
current = pdf_annotation_write_listing_ascii(
output,
current,
page_number.to_string(),
)
current = pdf_annotation_write_listing_ascii(output, current, ": ")
current = pdf_annotation_write_listing_view(output, current, contents)
output[current] = 10
current + 1
}
///|
fn pdf_annotation_listing_line_length(
page_number : Int,
contents : BytesView,
) -> Int {
@ascii.encode("Page ").length() +
@ascii.encode(page_number.to_string()).length() +
@ascii.encode(": ").length() +
contents.length() +
1
}
///|
/// Return old-style annotation contents as `(page_number, contents)` pairs.
///
/// Every annotation entry contributes one pair. Annotations without `/Contents`
/// contribute an empty byte string, matching cpdf's old list behavior.
pub fn PdfDocument::get_annotations(
self : PdfDocument,
encoding? : PdfMetadataEncoding = PdfMetadataUTF8,
) -> Array[(Int, PdfBytes)] raise PdfError {
let pages = self.pages_of_pagetree()
let output : Array[(Int, PdfBytes)] = []
for index, page in pages {
match self.lookup_direct(pdf_annotation_annots_key(), page.rest) {
Some(PdfArray(annotations)) =>
for annotation in annotations {
output.push(
(index + 1, self.pdf_annotation_contents_text(encoding, annotation)),
)
}
_ => ()
}
}
output
}
///|
/// Compatibility wrapper for `PdfDocument::get_annotations`.
pub fn pdf_get_annotations(
encoding : PdfMetadataEncoding,
document : PdfDocument,
) -> Array[(Int, PdfBytes)] raise PdfError {
document.get_annotations(encoding~)
}
///|
/// Render cpdf's old-style annotation listing for selected pages.
///
/// Unlike `get_annotations`, this suppresses empty `/Contents` entries and
/// returns the bytes cpdf would print instead of writing to stdout.
pub fn PdfDocument::list_annotations(
self : PdfDocument,
page_numbers : ArrayView[Int],
encoding? : PdfMetadataEncoding = PdfMetadataUTF8,
) -> PdfBytes raise PdfError {
let pages = self.pages_of_pagetree()
pdf_page_box_validate_page_numbers(
page_numbers,
pages.length(),
"list_annotations",
)
let selected_pages = @number_set.pdf_number_set(page_numbers)
let rows : Array[(Int, PdfBytes)] = []
let mut output_length = 0
for index, page in pages {
let page_number = index + 1
if selected_pages.contains(page_number) {
match self.lookup_direct(pdf_annotation_annots_key(), page.rest) {
Some(PdfArray(annotations)) =>
for annotation in annotations {
let contents = self.pdf_annotation_contents_text(
encoding, annotation,
)
if contents.length() > 0 {
output_length += pdf_annotation_listing_line_length(
page_number, contents,
)
rows.push((page_number, contents))
}
}
_ => ()
}
}
}
let output = Array::make(output_length, b'\x00')
let mut position = 0
for row in rows {
position = pdf_annotation_write_listing_line(output, position, row.0, row.1)
}
Bytes::from_array(output)
}
///|
/// Compatibility wrapper for `PdfDocument::list_annotations`.
pub fn pdf_list_annotations(
page_numbers : ArrayView[Int],
encoding : PdfMetadataEncoding,
document : PdfDocument,
) -> PdfBytes raise PdfError {
document.list_annotations(page_numbers, encoding~)
}
///|
fn pdf_annotation_json_int_array_contains(
values : ArrayView[Int],
value : Int,
) -> Bool {
for item in values {
if item == value {
break true
}
} nobreak {
false
}
}
///|
fn pdf_annotation_json_rewrite_destination(
page_numbers : PdfPageNumberLookup,
destination : PdfObject,
) -> PdfObject {
match destination {
PdfArray(items) =>
if items.length() == 0 {
destination
} else {
match items[0] {
PdfIndirect(number) => {
let rewritten : Array[PdfObject] = [
PdfInteger(page_numbers.page_number(number)),
]
for index in 1.. destination
}
}
_ => destination
}
}
///|
fn PdfDocument::pdf_annotation_json_rewrite_destinations(
self : PdfDocument,
page_numbers : PdfPageNumberLookup,
annotation : PdfObject,
) -> PdfObject raise PdfError {
let mut rewritten = annotation
match self.indirect_number(pdf_annotation_p_key(), rewritten) {
Some(number) =>
rewritten = rewritten.add_dict_entry(
pdf_annotation_p_key(),
PdfInteger(page_numbers.page_number(number)),
)
None => ()
}
match self.lookup_direct(pdf_annotation_dest_key(), rewritten) {
Some(destination) =>
rewritten = rewritten.add_dict_entry(
pdf_annotation_dest_key(),
pdf_annotation_json_rewrite_destination(page_numbers, destination),
)
None =>
match self.lookup_direct(pdf_annotation_a_key(), rewritten) {
Some(action) =>
match self.lookup_direct(pdf_annotation_d_key(), action) {
Some(destination) =>
rewritten = rewritten.add_dict_entry(
pdf_annotation_a_key(),
action.add_dict_entry(
pdf_annotation_d_key(),
pdf_annotation_json_rewrite_destination(
page_numbers, destination,
),
),
)
None => ()
}
None => ()
}
}
rewritten
}
///|
fn pdf_annotation_json_extra_root(
object : PdfObject,
) -> PdfObject raise PdfError {
match object.dictionary_view() {
Some(_) =>
object
.remove_dict_entry(pdf_annotation_popup_name)
.remove_dict_entry(pdf_annotation_parent_key())
None => object
}
}
///|
fn PdfDocument::pdf_annotation_json_extra_numbers(
self : PdfDocument,
roots : ArrayView[PdfObject],
annotation_numbers : ArrayView[Int],
) -> Array[Int] raise PdfError {
let found : @hashmap.HashMap[Int, Bool] = HashMap([])
for root in roots {
let searchable = pdf_annotation_json_extra_root(root)
for number in self.referenced_object_numbers(searchable) {
if !pdf_annotation_json_int_array_contains(annotation_numbers, number) {
found[number] = true
}
}
}
let numbers = found.keys().to_array()
numbers.sort()
numbers
}
///|
fn PdfDocument::pdf_annotation_json_page(
self : PdfDocument,
page_numbers : PdfPageNumberLookup,
page : PdfPage,
page_number : Int,
extra_roots : Array[PdfObject],
annotation_numbers : Array[Int],
) -> Array[Json] raise PdfError {
let output : Array[Json] = []
match self.lookup_direct(pdf_annotation_annots_key(), page.rest) {
Some(PdfArray(annotations)) =>
for annotation_ref in annotations {
match annotation_ref {
PdfIndirect(object_number) => {
let annotation = self.direct(annotation_ref)
let rewritten = self.pdf_annotation_json_rewrite_destinations(
page_numbers, annotation,
)
extra_roots.push(rewritten)
annotation_numbers.push(object_number)
output.push(
Json::array([
pdf_util_json_number(page_number),
pdf_util_json_number(object_number),
self.json_of_object(rewritten, utf8=true, clean_strings=true),
]),
)
}
_ => ()
}
}
_ => ()
}
output
}
///|
fn pdf_annotation_json_format_header() -> Json {
Json::array([
pdf_util_json_number(-1),
Json::object(
Map([
(
"/CPDFJSONannotformatversion",
Json::object(Map([("I", pdf_util_json_number(1))])),
),
]),
),
])
}
///|
/// Return cpdf's annotation JSON export for selected pages.
///
/// Only indirect page annotation entries are exported, matching cpdf's JSON
/// path. Page-object references in `/P`, `/Dest`, and action `/D` destinations
/// without `/Dest` are rewritten to 1-based page numbers. Referenced ancillary
/// objects are appended after annotation entries, excluding `/Popup` and
/// `/Parent` links.
pub fn PdfDocument::annotations_json(
self : PdfDocument,
page_numbers : ArrayView[Int],
) -> Json raise PdfError {
let pages = self.pages_of_pagetree()
pdf_page_box_validate_page_numbers(
page_numbers,
pages.length(),
"annotations_json",
)
let page_reference_numbers = self.page_reference_numbers()
let page_number_lookup = pdf_page_number_lookup(page_reference_numbers)
let selected_pages = @number_set.pdf_number_set(page_numbers)
let entries : Array[Json] = [pdf_annotation_json_format_header()]
let extra_roots : Array[PdfObject] = []
let annotation_numbers : Array[Int] = []
for index, page in pages {
let page_number = index + 1
if selected_pages.contains(page_number) {
entries.append(
self.pdf_annotation_json_page(
page_number_lookup, page, page_number, extra_roots, annotation_numbers,
),
)
}
}
for
number in self.pdf_annotation_json_extra_numbers(
extra_roots, annotation_numbers,
) {
entries.push(
Json::array([
pdf_util_json_number(number),
self.json_of_object(
self.lookup_object_or_null(number),
utf8=true,
clean_strings=true,
),
]),
)
}
Json::array(entries)
}
///|
/// Return cpdf's annotation JSON export as UTF-8 JSON bytes.
pub fn PdfDocument::annotations_json_blob(
self : PdfDocument,
page_numbers : ArrayView[Int],
) -> PdfBytes raise PdfError {
@utf8.encode(self.annotations_json(page_numbers).stringify())
}
///|
/// Compatibility wrapper for `PdfDocument::annotations_json_blob`.
pub fn pdf_get_annotations_json(
page_numbers : ArrayView[Int],
document : PdfDocument,
) -> PdfBytes raise PdfError {
document.annotations_json_blob(page_numbers)
}
///|
/// Compatibility wrapper matching cpdfannot's `get_annotations_json` order.
pub fn pdf_get_annotations_json_cpdf_order(
document : PdfDocument,
page_numbers : ArrayView[Int],
) -> PdfBytes raise PdfError {
document.annotations_json_blob(page_numbers)
}
///|
priv struct PdfAnnotationJsonImportRow {
page_number : Int
object_number : Int
object : PdfObject
}
///|
priv struct PdfAnnotationJsonImportData {
max_object_number : Int
extras : Array[(Int, PdfObject)]
annotations : Array[PdfAnnotationJsonImportRow]
}
///|
fn pdf_annotation_json_import_number(json : Json) -> Int raise PdfError {
match json {
Number(value, repr=_) => pdf_util_json_integral_number(value)
_ => raise SoftError("Bad annots entry")
}
}
///|
fn pdf_annotation_json_import_data(
json : Json,
) -> PdfAnnotationJsonImportData raise PdfError {
let entries = match json {
Array(values) => values
_ => raise SoftError("Bad Annotations JSON file")
}
let max_object_number : Ref[Int] = { val: 0 }
let extras : Array[(Int, PdfObject)] = []
let annotations : Array[PdfAnnotationJsonImportRow] = []
for entry in entries {
match entry {
Array([number_json, object_json]) => {
let number = pdf_annotation_json_import_number(number_json)
if number > 0 {
extras.push((number, pdf_object_of_json(object_json)))
if number > max_object_number.val {
max_object_number.val = number
}
}
}
Array([page_json, number_json, object_json]) => {
let page_number = pdf_annotation_json_import_number(page_json)
let object_number = pdf_annotation_json_import_number(number_json)
if object_number <= 0 {
raise SoftError("Bad annots entry")
}
annotations.push({
page_number,
object_number,
object: pdf_object_of_json(object_json),
})
if object_number > max_object_number.val {
max_object_number.val = object_number
}
}
_ => raise SoftError("Bad annots entry")
}
}
{ max_object_number: max_object_number.val, extras, annotations }
}
///|
fn PdfDocument::pdf_annotation_json_import_renumbered(
self : PdfDocument,
max_object_number : Int,
) -> PdfDocument raise PdfError {
if max_object_number <= 0 {
self.copy()
} else {
let changes : Array[(Int, Int)] = []
for index, number in self.object_numbers() {
changes.push((number, max_object_number + index + 1))
}
self.renumber_objects(changes)
}
}
///|
fn pdf_annotation_json_import_page_reference(
page_reference_numbers : ArrayView[Int],
page_number : Int,
) -> Int {
if page_number >= 1 && page_number <= page_reference_numbers.length() {
page_reference_numbers[page_number - 1]
} else {
page_number
}
}
///|
fn pdf_annotation_json_import_destination(
page_reference_numbers : ArrayView[Int],
destination : PdfObject,
) -> PdfObject {
match destination {
PdfArray(items) =>
if items.length() == 0 {
destination
} else {
match items[0] {
PdfInteger(page_number) => {
let rewritten : Array[PdfObject] = [
PdfIndirect(
pdf_annotation_json_import_page_reference(
page_reference_numbers, page_number,
),
),
]
for index in 1.. destination
}
}
_ => destination
}
}
///|
fn PdfDocument::pdf_annotation_json_import_destinations(
self : PdfDocument,
page_reference_numbers : ArrayView[Int],
annotation : PdfObject,
) -> PdfObject raise PdfError {
let mut rewritten = annotation
match self.lookup_direct(pdf_annotation_p_key(), rewritten) {
Some(PdfInteger(page_number)) =>
rewritten = rewritten.add_dict_entry(
pdf_annotation_p_key(),
PdfIndirect(
pdf_annotation_json_import_page_reference(
page_reference_numbers, page_number,
),
),
)
_ => ()
}
match self.lookup_direct(pdf_annotation_dest_key(), rewritten) {
Some(destination) =>
rewritten = rewritten.add_dict_entry(
pdf_annotation_dest_key(),
pdf_annotation_json_import_destination(
page_reference_numbers, destination,
),
)
None =>
match self.lookup_direct(pdf_annotation_a_key(), rewritten) {
Some(action) =>
match self.lookup_direct(pdf_annotation_d_key(), action) {
Some(destination) =>
rewritten = rewritten.add_dict_entry(
pdf_annotation_a_key(),
action.add_dict_entry(
pdf_annotation_d_key(),
pdf_annotation_json_import_destination(
page_reference_numbers, destination,
),
),
)
None => ()
}
None => ()
}
}
rewritten
}
///|
fn PdfDocument::pdf_annotation_json_import_rows_for_page(
self : PdfDocument,
page_reference_numbers : ArrayView[Int],
rows : ArrayView[PdfAnnotationJsonImportRow],
page_number : Int,
) -> Array[PdfObject] raise PdfError {
let imported : Array[PdfObject] = []
for row in rows {
if row.page_number == page_number {
self.add_object_given_number(
row.object_number,
self.pdf_annotation_json_import_destinations(
page_reference_numbers,
row.object,
),
)
imported.push(PdfIndirect(row.object_number))
}
}
imported
}
///|
fn PdfDocument::pdf_annotation_json_import_pages(
self : PdfDocument,
rows : ArrayView[PdfAnnotationJsonImportRow],
) -> PdfDocument raise PdfError {
if rows.length() == 0 {
self.copy()
} else {
let page_reference_numbers = self.page_reference_numbers()
let pages = self.pages_of_pagetree()
let rewritten : Array[PdfPage] = []
for index, page in pages {
let page_number = index + 1
let imported = self.pdf_annotation_json_import_rows_for_page(
page_reference_numbers, rows, page_number,
)
if imported.length() == 0 {
rewritten.push(page)
} else {
let existing = match
self.lookup_direct(pdf_annotation_annots_key(), page.rest) {
Some(PdfArray(annotations)) => annotations
_ => []
}
rewritten.push({
..page,
rest: page.rest.add_dict_entry(
pdf_annotation_annots_key(),
PdfArray([..existing, ..imported]),
),
})
}
}
self.change_pages(rewritten, change_references=true)
}
}
///|
/// Return a copy of the document with cpdf annotation JSON appended.
///
/// Existing objects are first renumbered above the imported annotation object
/// range, matching cpdf's collision-avoidance strategy. Header rows and other
/// nonpositive extra-object rows are ignored; positive extra objects are added
/// before page annotations are rebuilt.
pub fn PdfDocument::set_annotations_json(
self : PdfDocument,
json : Json,
) -> PdfDocument raise PdfError {
let data = pdf_annotation_json_import_data(json)
let document = self.pdf_annotation_json_import_renumbered(
data.max_object_number,
)
for extra in data.extras {
document.add_object_given_number(extra.0, extra.1)
}
document.pdf_annotation_json_import_pages(data.annotations)
}
///|
/// Compatibility wrapper for `PdfDocument::set_annotations_json`.
pub fn pdf_set_annotations_json(
json : Json,
document : PdfDocument,
) -> PdfDocument raise PdfError {
document.set_annotations_json(json)
}
///|
/// Compatibility wrapper matching cpdfannot's `set_annotations_json` order.
pub fn pdf_set_annotations_json_cpdf_order(
document : PdfDocument,
json : Json,
) -> PdfDocument raise PdfError {
document.set_annotations_json(json)
}
///|
/// Return a copy of this document with annotations copied from `source`.
pub fn PdfDocument::copy_annotations_from(
self : PdfDocument,
source : PdfDocument,
page_numbers : ArrayView[Int],
) -> PdfDocument raise PdfError {
self.set_annotations_json(source.annotations_json(page_numbers))
}
///|
/// Compatibility wrapper for `PdfDocument::copy_annotations_from`.
pub fn pdf_copy_annotations(
page_numbers : ArrayView[Int],
source : PdfDocument,
target : PdfDocument,
) -> PdfDocument raise PdfError {
target.copy_annotations_from(source, page_numbers)
}
///|
/// Return a copy of the document with `/Annots` removed from selected pages.
pub fn PdfDocument::remove_annotations(
self : PdfDocument,
page_numbers : ArrayView[Int],
) -> PdfDocument raise PdfError {
let pages = self.pages_of_pagetree()
pdf_page_box_validate_page_numbers(
page_numbers,
pages.length(),
"remove_annotations",
)
let selected_pages = @number_set.pdf_number_set(page_numbers)
let rewritten : Array[PdfPage] = Array::new(capacity=pages.length())
for index, page in pages {
let page_number = index + 1
if selected_pages.contains(page_number) {
rewritten.push({
..page,
rest: page.rest.remove_dict_entry(pdf_annotation_annots_key()),
})
} else {
rewritten.push(page)
}
}
self.change_pages(rewritten, change_references=true)
}
///|
/// Compatibility wrapper for `PdfDocument::remove_annotations`.
pub fn pdf_remove_annotations(
page_numbers : ArrayView[Int],
document : PdfDocument,
) -> PdfDocument raise PdfError {
document.remove_annotations(page_numbers)
}
///|
fn pdf_annotation_rectangle_object(rectangle : PdfRectangle) -> PdfObject {
PdfArray([
PdfReal(rectangle.min_x),
PdfReal(rectangle.min_y),
PdfReal(rectangle.max_x),
PdfReal(rectangle.max_y),
])
}
///|
fn pdf_annotation_border_object(border : PdfAnnotationBorder) -> PdfObject {
let base : Array[PdfObject] = [
PdfReal(border.horizontal_radius),
PdfReal(border.vertical_radius),
PdfReal(border.width),
]
if border.dash_array.length() > 0 {
base.push(PdfArray([ for dash in border.dash_array => PdfInteger(dash) ]))
}
PdfArray(base)
}
///|
fn pdf_annotation_rest_entries(
rest : PdfObject,
) -> Array[(PdfName, PdfObject)] raise PdfError {
match rest {
PdfNull => []
PdfDictionary(entries) => [ for entry in entries => entry ]
_ => raise DictionaryExpected
}
}
///|
fn pdf_object_of_annotation(
annotation : PdfAnnotation,
) -> PdfObject raise PdfError {
let entries : Array[(PdfName, PdfObject)] = [
(
pdf_annotation_subtype_key(),
PdfNameObject(pdf_annotation_subtype_name(annotation.subtype)),
),
(
pdf_annotation_contents_key(),
match annotation.contents {
Some(contents) => PdfString(contents)
None => PdfNull
},
),
(
pdf_annotation_rect_key(),
pdf_annotation_rectangle_object(annotation.rectangle),
),
(
pdf_annotation_border_key(),
pdf_annotation_border_object(annotation.border),
),
]
for entry in pdf_annotation_rest_entries(annotation.rest) {
entries.push(entry)
}
match annotation.colour {
Some(colour) =>
entries.push(
(
pdf_annotation_colour_key(),
PdfArray([
PdfInteger(colour.red),
PdfInteger(colour.green),
PdfInteger(colour.blue),
]),
),
)
None => ()
}
match annotation.subject {
Some(subject) =>
entries.push((pdf_annotation_subj_key(), PdfString(subject)))
None => ()
}
PdfDictionary(entries)
}
///|
/// Return a page value with an annotation prepended to `/Annots`.
///
/// The annotation is written as a direct dictionary. Existing annotation arrays
/// are preserved after the new entry; a non-array `/Annots` value raises.
pub fn PdfDocument::add_annotation(
self : PdfDocument,
page : PdfPage,
annotation : PdfAnnotation,
) -> PdfPage raise PdfError {
let object = pdf_object_of_annotation(annotation)
let annotations = match
self.lookup_direct(pdf_annotation_annots_key(), page.rest) {
Some(PdfArray(existing)) => [object, ..existing]
Some(_) => raise DictionaryExpected
None => [object]
}
{
..page,
rest: page.rest.add_dict_entry(
pdf_annotation_annots_key(),
PdfArray(annotations),
),
}
}
///|
/// Compatibility wrapper for `PdfDocument::add_annotation`.
pub fn pdf_add_annotation(
document : PdfDocument,
page : PdfPage,
annotation : PdfAnnotation,
) -> PdfPage raise PdfError {
document.add_annotation(page, annotation)
}
///|
fn PdfDocument::pdf_annotation_transform_one(
self : PdfDocument,
matrix : TransformMatrix,
annotation_number : Int,
) -> Unit raise PdfError {
let annotation = self.lookup_object_or_null(annotation_number)
let transformed = self.pdf_annotation_transform_object(matrix, annotation)
if transformed != annotation {
self.add_object_given_number(annotation_number, transformed)
}
}
///|
fn PdfDocument::pdf_annotation_transform_object(
self : PdfDocument,
matrix : TransformMatrix,
annotation : PdfObject,
) -> PdfObject raise PdfError {
let mut transformed = annotation
match self.lookup_direct(pdf_annotation_rect_key(), annotation) {
Some(rectangle) =>
transformed = transformed.add_dict_entry(
pdf_annotation_rect_key(),
self.transform_rect(matrix, rectangle),
)
None => ()
}
match self.lookup_direct(pdf_annotation_quadpoints_key(), annotation) {
Some(quadpoints) =>
transformed = transformed.add_dict_entry(
pdf_annotation_quadpoints_key(),
self.transform_quadpoints(matrix, quadpoints),
)
None => ()
}
match self.lookup_direct(pdf_annotation_line_key(), annotation) {
Some(line) =>
transformed = transformed.add_dict_entry(
pdf_annotation_line_key(),
self.transform_rect(matrix, line),
)
None => ()
}
transformed
}
///|
/// Transform annotation geometry referenced by a page dictionary.
///
/// Applies `matrix` to `/Rect`, `/QuadPoints`, and `/L` entries. Indirect
/// annotations are updated in this document; direct annotation dictionaries in
/// the page's `/Annots` array are updated in place.
pub fn PdfDocument::transform_annotations(
self : PdfDocument,
matrix : TransformMatrix,
rest : PdfObject,
) -> Unit raise PdfError {
match self.lookup_direct(pdf_annotation_annots_key(), rest) {
Some(PdfArray(annotations)) =>
for index in 0..
self.pdf_annotation_transform_one(matrix, number)
PdfDictionary(_) as annotation => {
let transformed = self.pdf_annotation_transform_object(
matrix, annotation,
)
if transformed != annotation {
annotations[index] = transformed
}
}
_ => ()
}
}
_ => ()
}
}
///|
/// Compatibility wrapper for `PdfDocument::transform_annotations`.
pub fn pdf_transform_annotations(
document : PdfDocument,
matrix : TransformMatrix,
rest : PdfObject,
) -> Unit raise PdfError {
document.transform_annotations(matrix, rest)
}