///|
pub(all) struct PdfCompositionEntry {
label : String
bytes : Int
percent : Double
} derive(Debug, Eq, ToJson)
///|
fn pdf_composition_name(text : String) -> PdfName {
pdf_name_of_bytes(@ascii.encode(text))
}
///|
let pdf_composition_struct_tree_root_name : PdfName = pdf_composition_name(
"/StructTreeRoot",
)
///|
fn pdf_composition_struct_tree_root() -> PdfName {
pdf_composition_struct_tree_root_name
}
///|
let pdf_composition_pg_name : PdfName = pdf_composition_name("/Pg")
///|
fn pdf_composition_pg() -> PdfName {
pdf_composition_pg_name
}
///|
let pdf_composition_piece_info_name : PdfName = pdf_composition_name(
"/PieceInfo",
)
///|
fn pdf_composition_piece_info() -> PdfName {
pdf_composition_piece_info_name
}
///|
let pdf_composition_subtype_name : PdfName = pdf_composition_name("/Subtype")
///|
fn pdf_composition_subtype() -> PdfName {
pdf_composition_subtype_name
}
///|
let pdf_composition_image_name : PdfName = pdf_composition_name("/Image")
///|
fn pdf_composition_image() -> PdfName {
pdf_composition_image_name
}
///|
let pdf_composition_form_name : PdfName = pdf_composition_name("/Form")
///|
fn pdf_composition_form() -> PdfName {
pdf_composition_form_name
}
///|
let pdf_composition_type_name : PdfName = pdf_composition_name("/Type")
///|
fn pdf_composition_type() -> PdfName {
pdf_composition_type_name
}
///|
let pdf_composition_font_name : PdfName = pdf_composition_name("/Font")
///|
fn pdf_composition_font() -> PdfName {
pdf_composition_font_name
}
///|
let pdf_composition_page_name : PdfName = pdf_composition_name("/Page")
///|
fn pdf_composition_page() -> PdfName {
pdf_composition_page_name
}
///|
let pdf_composition_contents_name : PdfName = pdf_composition_name("/Contents")
///|
fn pdf_composition_contents() -> PdfName {
pdf_composition_contents_name
}
///|
fn pdf_composition_mark_unseen(
marked : @hashmap.HashMap[Int, Bool],
output : Array[Int],
number : Int,
) -> Unit {
if !marked.contains(number) {
marked[number] = true
output.push(number)
}
}
///|
fn pdf_composition_mark_references(
document : PdfDocument,
marked : @hashmap.HashMap[Int, Bool],
output : Array[Int],
object : PdfObject,
no_follow_entries? : ArrayView[PdfName] = [],
) -> Unit {
for number in document.referenced_object_numbers(object, no_follow_entries~) {
pdf_composition_mark_unseen(marked, output, number)
}
}
///|
fn PdfDocument::pdf_composition_structure_info(
self : PdfDocument,
marked : @hashmap.HashMap[Int, Bool],
) -> Array[Int] raise PdfError {
let output : Array[Int] = []
match self.catalog().lookup_immediate(pdf_composition_struct_tree_root()) {
Some(struct_tree_root) =>
pdf_composition_mark_references(self, marked, output, struct_tree_root, no_follow_entries=[
pdf_composition_pg(),
])
None => ()
}
output
}
///|
fn PdfDocument::pdf_composition_piece_info(
self : PdfDocument,
marked : @hashmap.HashMap[Int, Bool],
) -> Array[Int] {
let entries = self.parsed_object_entries()
let output : Array[Int] = Array::new(capacity=entries.length())
for entry in entries {
match self.lookup_direct(pdf_composition_piece_info(), entry.2) {
Some(piece_info) =>
pdf_composition_mark_references(self, marked, output, piece_info)
None => ()
}
}
output
}
///|
fn PdfDocument::pdf_composition_images(
self : PdfDocument,
marked : @hashmap.HashMap[Int, Bool],
number : Int,
object : PdfObject,
) -> Array[Int] {
let output : Array[Int] = Array::new(capacity=1)
if !marked.contains(number) {
match self.lookup_direct(pdf_composition_subtype(), object) {
Some(PdfNameObject(name)) if name == pdf_composition_image() =>
pdf_composition_mark_unseen(marked, output, number)
_ => ()
}
}
output
}
///|
fn PdfDocument::pdf_composition_fonts(
self : PdfDocument,
marked : @hashmap.HashMap[Int, Bool],
number : Int,
object : PdfObject,
) -> Array[Int] {
let output : Array[Int] = Array::new(capacity=1)
if !marked.contains(number) {
match self.lookup_direct(pdf_composition_type(), object) {
Some(PdfNameObject(name)) if name == pdf_composition_font() =>
pdf_composition_mark_references(
self,
marked,
output,
PdfIndirect(number),
)
_ => ()
}
}
output
}
///|
fn pdf_composition_content_numbers(
contents : PdfObject,
output : Array[Int],
) -> Unit {
match contents {
PdfIndirect(number) => output.push(number)
PdfArray(values) =>
for value in values {
match value {
PdfIndirect(number) => output.push(number)
_ => ()
}
}
_ => ()
}
}
///|
fn PdfDocument::pdf_composition_content_streams(
self : PdfDocument,
marked : @hashmap.HashMap[Int, Bool],
number : Int,
object : PdfObject,
) -> Array[Int] {
let output : Array[Int] = Array::new(capacity=1)
if !marked.contains(number) {
match self.lookup_direct(pdf_composition_type(), object) {
Some(PdfNameObject(name)) if name == pdf_composition_page() =>
match object.lookup_immediate(pdf_composition_contents()) {
Some(contents) => {
let content_numbers : Array[Int] = []
pdf_composition_content_numbers(contents, content_numbers)
for content_number in content_numbers {
pdf_composition_mark_unseen(marked, output, content_number)
}
}
None => ()
}
_ =>
match self.lookup_direct(pdf_composition_subtype(), object) {
Some(PdfNameObject(name)) if name == pdf_composition_form() =>
pdf_composition_mark_unseen(marked, output, number)
_ => ()
}
}
}
output
}
///|
fn PdfDocument::pdf_composition_find_objects(
self : PdfDocument,
) -> (Array[Int], Array[Int], Array[Int], Array[Int], Array[Int]) raise PdfError {
let marked : @hashmap.HashMap[Int, Bool] = HashMap([])
let entries = self.parsed_object_entries()
let images : Array[Int] = Array::new(capacity=entries.length())
let fonts : Array[Int] = Array::new(capacity=entries.length())
let content_streams : Array[Int] = Array::new(capacity=entries.length())
for entry in entries {
if !marked.contains(entry.0) {
images.append(self.pdf_composition_images(marked, entry.0, entry.2))
content_streams.append(
self.pdf_composition_content_streams(marked, entry.0, entry.2),
)
fonts.append(self.pdf_composition_fonts(marked, entry.0, entry.2))
}
}
let structure_info = self.pdf_composition_structure_info(marked)
let piece_info = self.pdf_composition_piece_info(marked)
(images, fonts, content_streams, structure_info, piece_info)
}
///|
fn PdfDocument::pdf_composition_object_size(
self : PdfDocument,
number : Int,
) -> Int raise PdfError {
pdf_write_object(self.lookup_object_or_null(number)).length()
}
///|
fn pdf_composition_unique_numbers(numbers : ArrayView[Int]) -> Array[Int] {
let seen : @hashmap.HashMap[Int, Bool] = HashMap([])
let output : Array[Int] = Array::new(capacity=numbers.length())
for number in numbers {
if !seen.contains(number) {
seen[number] = true
output.push(number)
}
}
output
}
///|
fn PdfDocument::pdf_composition_compressed_size(
self : PdfDocument,
numbers : ArrayView[Int],
) -> Int raise PdfError {
let unique_numbers = pdf_composition_unique_numbers(numbers)
if unique_numbers.length() == 0 {
0
} else if self.objects.object_stream_ids.length() == 0 {
let mut total = 0
for number in unique_numbers {
total += self.pdf_composition_object_size(number)
}
total
} else {
let combined : Array[Byte] = []
let mut stream_total = 0
for number in unique_numbers {
let object = self.lookup_object_or_null(number)
match object {
PdfStreamObject(_) =>
stream_total += self.pdf_composition_object_size(number)
_ => {
let bytes = pdf_write_object(object)
for byte in bytes {
combined.push(byte)
}
}
}
}
stream_total + pdf_flate_encode(Bytes::from_array(combined)).length()
}
}
///|
fn PdfDocument::pdf_composition_xref_table_size(
self : PdfDocument,
) -> Int raise PdfError {
if self.objects.object_stream_ids.length() == 0 {
20 * self.object_count()
} else {
let numbers = self.objects.object_stream_ids.keys().to_array()
numbers.sort()
self.pdf_composition_compressed_size(numbers)
}
}
///|
fn pdf_composition_entry(
label : String,
bytes : Int,
file_size : Int,
) -> PdfCompositionEntry {
{ label, bytes, percent: bytes.to_double() / file_size.to_double() * 100.0 }
}
///|
/// Return cpdf-style composition buckets for a document.
///
/// The result order is stable: images, fonts, content streams, structure info,
/// attached files, xref table, piece info, then unclassified bytes.
pub fn PdfDocument::composition(
self : PdfDocument,
file_size : Int,
) -> Array[PdfCompositionEntry] raise PdfError {
if file_size <= 0 {
raise BadNumberArgument("file_size must be positive")
}
let (
image_numbers,
font_numbers,
content_numbers,
structure_numbers,
piece_info_numbers,
) = self.pdf_composition_find_objects()
let images = self.pdf_composition_compressed_size(image_numbers)
let fonts = self.pdf_composition_compressed_size(font_numbers)
let content_streams = self.pdf_composition_compressed_size(content_numbers)
let structure_info = self.pdf_composition_compressed_size(structure_numbers)
let attached_files = self.size_attached_files()
let xref_table = self.pdf_composition_xref_table_size()
let piece_info = self.pdf_composition_compressed_size(piece_info_numbers)
let classified = images +
fonts +
content_streams +
structure_info +
attached_files +
xref_table +
piece_info
[
pdf_composition_entry("Images", images, file_size),
pdf_composition_entry("Fonts", fonts, file_size),
pdf_composition_entry("Content streams", content_streams, file_size),
pdf_composition_entry("Structure Info", structure_info, file_size),
pdf_composition_entry("Attached Files", attached_files, file_size),
pdf_composition_entry("XRef Table", xref_table, file_size),
pdf_composition_entry("Piece Info", piece_info, file_size),
pdf_composition_entry("Unclassified", file_size - classified, file_size),
]
}
///|
/// Convert a composition entry to the cpdf JSON row shape:
/// `[label, byte_count, percent]`.
pub fn PdfCompositionEntry::to_json(self : PdfCompositionEntry) -> Json {
Json::array([
Json::string(self.label),
pdf_util_json_number(self.bytes),
Json::number(self.percent),
])
}
///|
/// Return the document composition in cpdf JSON row format.
pub fn PdfDocument::composition_json(
self : PdfDocument,
file_size : Int,
) -> Json raise PdfError {
let entries = self.composition(file_size)
let values : Array[Json] = Array::new(capacity=entries.length())
for entry in entries {
values.push(entry.to_json())
}
Json::array(values)
}
///|
/// Return the document composition serialized as UTF-8 JSON bytes.
pub fn PdfDocument::composition_json_blob(
self : PdfDocument,
file_size : Int,
) -> PdfBytes raise PdfError {
@utf8.encode(self.composition_json(file_size).stringify())
}
///|
fn pdf_composition_write_utf8(
output : Array[Byte],
position : Int,
text : String,
) -> Int {
let mut current = position
for byte in @utf8.encode(text) {
output[current] = byte
current += 1
}
current
}
///|
fn pdf_composition_percent_text(percent : Double) -> String {
let rounded = (percent * 100.0).round().to_int()
let negative = rounded < 0
let magnitude = if negative { -rounded } else { rounded }
let whole = magnitude / 100
let fraction = magnitude % 100
let fraction_text = if fraction < 10 {
"0" + fraction.to_string()
} else {
fraction.to_string()
}
(if negative { "-" } else { "" }) + whole.to_string() + "." + fraction_text
}
///|
fn pdf_composition_write_text_entry(
output : Array[Byte],
position : Int,
entry : PdfCompositionEntry,
) -> Int {
let mut current = pdf_composition_write_utf8(output, position, entry.label)
current = pdf_composition_write_utf8(output, current, ": ")
current = pdf_composition_write_utf8(output, current, entry.bytes.to_string())
current = pdf_composition_write_utf8(output, current, " bytes (")
current = pdf_composition_write_utf8(
output,
current,
pdf_composition_percent_text(entry.percent),
)
pdf_composition_write_utf8(output, current, "%)\n")
}
///|
fn pdf_composition_utf8_length(text : String) -> Int {
@utf8.encode(text).length()
}
///|
fn pdf_composition_text_entry_length(entry : PdfCompositionEntry) -> Int {
pdf_composition_utf8_length(entry.label) +
pdf_composition_utf8_length(": ") +
pdf_composition_utf8_length(entry.bytes.to_string()) +
pdf_composition_utf8_length(" bytes (") +
pdf_composition_utf8_length(pdf_composition_percent_text(entry.percent)) +
pdf_composition_utf8_length("%)\n")
}
///|
fn pdf_composition_text_length(entries : ArrayView[PdfCompositionEntry]) -> Int {
let mut length = 0
for entry in entries {
length += pdf_composition_text_entry_length(entry)
}
length
}
///|
/// Return the text that cpdf's `show_composition` prints when `json=false`.
pub fn PdfDocument::composition_text(
self : PdfDocument,
file_size : Int,
) -> PdfBytes raise PdfError {
let entries = self.composition(file_size)
let output = Array::make(pdf_composition_text_length(entries), b'\x00')
let mut position = 0
for entry in entries {
position = pdf_composition_write_text_entry(output, position, entry)
}
Bytes::from_array(output)
}
///|
fn pdf_composition_bytes_with_newline(bytes : BytesView) -> PdfBytes {
let output = Array::make(bytes.length() + 1, b'\n')
for i in 0.. Array[PdfCompositionEntry] raise PdfError {
document.composition(file_size)
}
///|
/// Compatibility wrapper for `PdfDocument::composition_json_blob`.
pub fn pdf_show_composition_json_blob(
file_size : Int,
document : PdfDocument,
) -> PdfBytes raise PdfError {
document.composition_json_blob(file_size)
}
///|
/// Return the bytes cpdf's `show_composition` would print.
pub fn pdf_show_composition(
file_size : Int,
json : Bool,
document : PdfDocument,
) -> PdfBytes raise PdfError {
if json {
pdf_composition_bytes_with_newline(
document.composition_json_blob(file_size),
)
} else {
document.composition_text(file_size)
}
}