katana-document-viewer 0.1.4

KatanA document viewer artifact, render evaluation, and export foundation.
Documentation
use crate::export_surface::SurfaceLinkAnnotation;
use image::RgbaImage;

use super::super::PdfPageObjects;
use super::{PdfDestination, PdfDocumentHelpers};

impl PdfDocumentHelpers {
    pub(crate) fn rgb_bytes(image: &RgbaImage) -> Vec<u8> {
        let mut rgb =
            Vec::with_capacity((image.width() * image.height() * PDF_COLOR_CHANNEL_COUNT) as usize);
        for pixel in image.pixels() {
            rgb.extend_from_slice(&[pixel[0], pixel[1], pixel[2]]);
        }
        rgb
    }

    pub(crate) fn content_stream(image: &RgbaImage, image_object: usize) -> Vec<u8> {
        let width = image.width();
        let height = image.height();
        format!("q\n{width} 0 0 {height} 0 0 cm\n/Im{image_object} Do\nQ\n").into_bytes()
    }

    pub(crate) fn page_dictionary(
        image: &RgbaImage,
        page_object: usize,
        content_object: usize,
        image_object: usize,
        annotation_objects: &[usize],
    ) -> Vec<u8> {
        let width = image.width();
        let height = image.height();
        let annotations = if annotation_objects.is_empty() {
            String::new()
        } else {
            let refs = annotation_objects
                .iter()
                .map(|object| format!("{object} 0 R"))
                .collect::<Vec<_>>()
                .join(" ");
            format!(" /Annots [{refs}]")
        };
        Self::ascii_object(
            page_object,
            &format!(
                "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {width} {height}] /Resources << /XObject << /Im{image_object} {image_object} 0 R >> >> /Contents {content_object} 0 R{annotations} >>"
            ),
        )
    }

    pub(crate) fn image_dictionary(
        image: &RgbaImage,
        image_object: usize,
        image_stream: &[u8],
    ) -> Vec<u8> {
        let width = image.width();
        let height = image.height();
        let dictionary = format!(
            "<< /Type /XObject /Subtype /Image /Width {width} /Height {height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode"
        );
        Self::stream_object(image_object, &dictionary, image_stream)
    }

    pub(crate) fn link_annotation_object(
        number: usize,
        page_object: usize,
        annotation: &SurfaceLinkAnnotation,
        page_height: u32,
        destination: Option<PdfDestination>,
    ) -> Vec<u8> {
        let x1 = annotation.x;
        let x2 = annotation.x + annotation.width;
        let y1 = page_height.saturating_sub(annotation.y + annotation.height);
        let y2 = page_height.saturating_sub(annotation.y);
        let target = Self::pdf_link_target(annotation, destination);
        Self::ascii_object(
            number,
            &format!(
                "<< /Type /Annot /Subtype /Link /Rect [{x1} {y1} {x2} {y2}] /Border [0 0 0] /F 4 /H /I /P {page_object} 0 R {target} >>"
            ),
        )
    }

    pub(crate) fn pdf_link_target(
        annotation: &SurfaceLinkAnnotation,
        destination: Option<PdfDestination>,
    ) -> String {
        if annotation.target.starts_with('#') {
            let destination = destination.unwrap_or(PdfDestination {
                page_object: PDF_DEFAULT_PAGE_OBJECT,
                x: 0,
                y: 0,
            });
            return format!(
                "/Dest [{} 0 R /XYZ {} {} null]",
                destination.page_object, destination.x, destination.y
            );
        }
        format!(
            "/A << /Type /Action /S /URI /URI ({}) >>",
            Self::escape_pdf_string(&annotation.target)
        )
    }

    pub(crate) fn pdf_link_destination(
        annotation: &SurfaceLinkAnnotation,
        destinations: &[(String, PdfDestination)],
    ) -> Option<PdfDestination> {
        let target = annotation.target.strip_prefix('#')?;
        destinations
            .iter()
            .find_map(|(id, destination)| (id == target).then_some(*destination))
    }

    pub(crate) fn pdf_destinations(
        anchors: &[crate::export_surface::SurfaceLinkAnchor],
        page_objects: &[PdfPageObjects],
        pages: &[RgbaImage],
    ) -> Vec<(String, PdfDestination)> {
        anchors
            .iter()
            .filter_map(|anchor| {
                let page_object = page_objects.get(anchor.page_index)?.page;
                let page_height = pages.get(anchor.page_index)?.height();
                Some((
                    anchor.id.clone(),
                    PdfDestination {
                        page_object,
                        x: anchor.x,
                        y: page_height.saturating_sub(anchor.y),
                    },
                ))
            })
            .collect()
    }

    pub(crate) fn allocate_page_objects(
        page_annotations: &[Vec<&SurfaceLinkAnnotation>],
    ) -> Vec<PdfPageObjects> {
        let mut next_object = PDF_FIRST_PAGE_OBJECT;
        let mut pages = Vec::with_capacity(page_annotations.len());
        for annotations in page_annotations {
            let page = next_object;
            let content = next_object + 1;
            let image = next_object + 2;
            next_object += PDF_OBJECTS_PER_PAGE;
            let annotation_objects = (0..annotations.len())
                .map(|_| {
                    let object = next_object;
                    next_object += 1;
                    object
                })
                .collect();
            pages.push(PdfPageObjects {
                page,
                content,
                image,
                annotations: annotation_objects,
            });
        }
        pages
    }
}

const PDF_COLOR_CHANNEL_COUNT: u32 = 3;
const PDF_DEFAULT_PAGE_OBJECT: usize = 3;
const PDF_FIRST_PAGE_OBJECT: usize = 3;
const PDF_OBJECTS_PER_PAGE: usize = 3;