diff --git a/Cargo.lock b/Cargo.lock index 8782576..41d8217 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,12 +633,12 @@ dependencies = [ "allsorts", "image", "js-sys", - "log", "lopdf", "owned_ttf_parser", "pdf-writer", "svg2pdf", "time", + "ttf-parser 0.25.0", "usvg", ] @@ -1034,6 +1034,12 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8686b91785aff82828ed725225925b33b4fde44c4bb15876e5f7c832724c420a" +[[package]] +name = "ttf-parser" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5902c5d130972a0000f60860bfbf46f7ca3db5391eddfedd1b8728bd9dc96c0e" + [[package]] name = "typenum" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index 257c228..fc78761 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/fschutt/printpdf" homepage = "https://github.com/fschutt/printpdf" license = "MIT" readme = "README.md" -description = "Rust library for writing PDF files" +description = "Rust library for reading and writing PDF files" keywords = ["pdf", "gui", "graphics", "wkhtmltopdf"] categories = ["gui"] exclude = ["./assets/*", "./doc/*", "./examples/*"] @@ -17,55 +17,30 @@ autoexamples = false edition = "2021" [dependencies] -# minimum dependencies -lopdf = { version = "0.33.0", default-features = false, features = [ - "pom_parser", -] } -owned_ttf_parser = { version = "0.24.0", default-features = false, features = [ - "std", -] } +lopdf = { version = "0.33.0", default-features = false, features = ["pom_parser"] } +owned_ttf_parser = { version = "0.24.0", default-features = false, features = ["std"] } time = { version = "0.3.25", default-features = false, features = ["std"] } -# optional: logging -log = { version = "0.4.8", optional = true } -# image reading (png / jpeg) -image = { version = "0.25", optional = true, default-features = false, features = [ - "gif", - "jpeg", - "png", - "pnm", - "tiff", - "bmp", -] } -# svg support (svg -> pdf xobject) +allsorts = { version = "0.15", default-features = false, features = ["flate2_rust"] } +pdf-writer = { version = "0.10" } +# optional deps +image = { version = "0.25", optional = true, default-features = false, features = ["gif", "jpeg", "png", "pnm", "tiff", "bmp"] } svg2pdf = { version = "0.11", optional = true } -pdf-writer = { version = "0.10", optional = true } usvg = { version = "0.42", optional = true } -allsorts = { version = "0.15", optional = true, default-features = false, features = [ - "flate2_rust", -] } +ttf-parser = "0.25.0" [features] default = ["js-sys"] -# do not compress PDF streams, useful for debugging -less-optimization = [] -# enables logging -logging = ["log"] -# enables image support with some basic formats -embedded_images = ["image"] -# enables extra image formats -ico = ["image/ico", "embedded_images"] -tga = ["image/tga", "embedded_images"] -hdr = ["image/hdr", "embedded_images"] -rayon = ["image/rayon", "embedded_images"] -dds = ["image/dds", "embedded_images"] -webp = ["image/webp", "embedded_images"] -# enables svg -svg = ["svg2pdf", "usvg", "pdf-writer"] -font_subsetting = ["dep:allsorts"] -# enables annotations -annotations = ["pdf-writer"] -# enables js-sys features on wasm -js-sys = ["dep:js-sys"] +less-optimization = [] # do not compress PDF streams, useful for debugging +images = ["image"] # enables image support with some basic formats +ico = ["image/ico", "images"] # enables extra image format .ICO +tga = ["image/tga", "images"] # enables extra image format .TGA +hdr = ["image/hdr", "images"] # enables extra image format .HDR +dds = ["image/dds", "images"] # enables extra image format .DDS +webp = ["image/webp", "images"] # enables extra image format .WEBP +rayon = ["image/rayon", "images"] # enables multithreading for decoding images +svg = ["svg2pdf", "usvg"] # enables SVG embedding +js-sys = ["dep:js-sys"] # enables js-sys features on wasm +rendering = [] # enables experimental rendering module (renders PDF pages to image::DynamicImage) [package.metadata.docs.rs] all-features = true @@ -73,6 +48,18 @@ all-features = true [target.'cfg(all(target_arch="wasm32",target_os="unknown"))'.dependencies] js-sys = { version = "0.3.40", optional = true } +[[example]] +name = "image" +required-features = ["images"] + +[[example]] +name = "image_alpha" +required-features = ["images"] + +[[example]] +name = "svg" +required-features = ["svg"] + [[example]] name = "bookmark" required-features = [] @@ -85,14 +72,6 @@ required-features = [] name = "font" required-features = [] -[[example]] -name = "image" -required-features = ["embedded_images"] - -[[example]] -name = "image_alpha" -required-features = ["embedded_images"] - [[example]] name = "no_icc" required-features = [] @@ -105,17 +84,13 @@ required-features = [] name = "shape" required-features = [] -[[example]] -name = "svg" -required-features = ["svg"] - [[example]] name = "annotations" required-features = [] [[example]] name = "hyperlink" -required-features = ["annotations"] +required-features = [] [[example]] name = "rect" diff --git a/src/annotation.rs b/src/annotation.rs new file mode 100644 index 0000000..c7d3761 --- /dev/null +++ b/src/annotation.rs @@ -0,0 +1,117 @@ +//! Bookmarks, page and link annotations + +use crate::graphics::Rect; + +#[derive(Debug, PartialEq, Clone)] +pub struct PageAnnotation { + /// Name of the bookmark annotation (i.e. "Chapter 5") + pub name: String, + /// Which page to jump to (i.e "page 10" = 10) + pub page: usize, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct LinkAnnotation { + pub rect: Rect, + pub border: BorderArray, + pub c: ColorArray, + pub a: Actions, + pub h: HighlightingMode, +} + +impl LinkAnnotation { + /// Creates a new LinkAnnotation + pub fn new( + rect: Rect, + border: Option, + c: Option, + a: Actions, + h: Option, + ) -> Self { + Self { + rect, + border: border.unwrap_or_default(), + c: c.unwrap_or_default(), + a, + h: h.unwrap_or_default(), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum BorderArray { + Solid([f32; 3]), + Dashed([f32; 3], DashPhase), +} + +impl Default for BorderArray { + fn default() -> Self { + BorderArray::Solid([0.0, 0.0, 1.0]) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct DashPhase { + pub dash_array: Vec, + pub phase: f32, +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum ColorArray { + Transparent, + Gray([f32; 1]), + RGB([f32; 3]), + CMYK([f32; 4]), +} + +impl Default for ColorArray { + fn default() -> Self { + ColorArray::RGB([0.0, 1.0, 1.0]) + } +} + +#[derive(Debug, PartialEq, Clone)] +#[non_exhaustive] +pub enum Destination { + /// Display `page` with coordinates `top` and `left` positioned at the upper-left corner of the + /// window and the contents of the page magnified by `zoom`. + /// + /// A value of `None` for any parameter indicates to leave the current value unchanged, and a + /// `zoom` value of 0 has the same meaning as `None`. + XYZ { + page: usize, + left: Option, + top: Option, + zoom: Option, + }, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Actions { + GoTo(Destination), + URI(String), +} + +impl Actions { + pub fn go_to(destination: Destination) -> Self { + Self::GoTo(destination) + } + + pub fn uri(uri: String) -> Self { + Self::URI(uri) + } +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum HighlightingMode { + None, + Invert, + Outline, + Push, +} + +impl Default for HighlightingMode { + fn default() -> Self { + HighlightingMode::Invert + } +} diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 0000000..4d7f195 --- /dev/null +++ b/src/color.rs @@ -0,0 +1,185 @@ +use crate::IccProfileId; + +/// Color space (enum for marking the number of bits a color has) +#[derive(Debug, Copy, PartialEq, Clone)] +pub enum ColorSpace { + Rgb, + Rgba, + Palette, + Cmyk, + Greyscale, + GreyscaleAlpha, +} + +#[cfg(feature = "images")] +impl From for ColorSpace { + fn from(color_type: image_crate::ColorType) -> Self { + use image_crate::ColorType::*; + match color_type { + L8 | L16 => ColorSpace::Greyscale, + La8 | La16 => ColorSpace::GreyscaleAlpha, + Rgb8 | Rgb16 => ColorSpace::Rgb, + Rgba8 | Rgba16 => ColorSpace::Rgba, + _ => ColorSpace::Greyscale, // unreachable + } + } +} + +impl From for &'static str { + fn from(val: ColorSpace) -> Self { + use self::ColorSpace::*; + match val { + Rgb => "DeviceRGB", + Cmyk => "DeviceCMYK", + Greyscale => "DeviceGray", + Palette => "Indexed", + Rgba | GreyscaleAlpha => "DeviceN", + } + } +} + +/// How many bits does a color have? +#[derive(Debug, Copy, PartialEq, Clone)] +pub enum ColorBits { + Bit1, + Bit8, + Bit16, +} + +#[cfg(feature = "images")] +impl From for ColorBits { + fn from(color_type: image_crate::ColorType) -> ColorBits { + use image_crate::ColorType::*; + use ColorBits::*; + + match color_type { + L8 | La8 | Rgb8 | Rgba8 => Bit8, + L16 | La16 | Rgb16 | Rgba16 => Bit16, + _ => Bit8, // unreachable + } + } +} + +impl From for i64 { + fn from(val: ColorBits) -> Self { + match val { + ColorBits::Bit1 => 1, + ColorBits::Bit8 => 8, + ColorBits::Bit16 => 16, + } + } +} + +/// Wrapper for Rgb, Cmyk and other color types +#[derive(Debug, Clone, PartialEq)] +pub enum Color { + Rgb(Rgb), + Cmyk(Cmyk), + Greyscale(Greyscale), + SpotColor(SpotColor), +} + +impl Color { + /// Consumes the color and converts into into a vector of numbers + pub fn into_vec(self) -> Vec { + match self { + Color::Rgb(rgb) => { + vec![rgb.r, rgb.g, rgb.b] + } + Color::Cmyk(cmyk) => { + vec![cmyk.c, cmyk.m, cmyk.y, cmyk.k] + } + Color::Greyscale(gs) => { + vec![gs.percent] + } + Color::SpotColor(spot) => { + vec![spot.c, spot.m, spot.y, spot.k] + } + } + } + + /// Returns if the color has an icc profile attached + pub fn get_icc_profile(&self) -> Option<&Option> { + match *self { + Color::Rgb(ref rgb) => Some(&rgb.icc_profile), + Color::Cmyk(ref cmyk) => Some(&cmyk.icc_profile), + Color::Greyscale(ref gs) => Some(&gs.icc_profile), + Color::SpotColor(_) => None, + } + } +} + +/// RGB color +#[derive(Debug, Clone, PartialEq)] +pub struct Rgb { + pub r: f32, + pub g: f32, + pub b: f32, + pub icc_profile: Option, +} + +impl Rgb { + pub fn new(r: f32, g: f32, b: f32, icc_profile: Option) -> Self { + Self { + r, + g, + b, + icc_profile, + } + } +} + +/// CMYK color +#[derive(Debug, Clone, PartialEq)] +pub struct Cmyk { + pub c: f32, + pub m: f32, + pub y: f32, + pub k: f32, + pub icc_profile: Option, +} + +impl Cmyk { + /// Creates a new CMYK color + pub fn new(c: f32, m: f32, y: f32, k: f32, icc_profile: Option) -> Self { + Self { + c, + m, + y, + k, + icc_profile, + } + } +} + +/// Greyscale color +#[derive(Debug, Clone, PartialEq)] +pub struct Greyscale { + pub percent: f32, + pub icc_profile: Option, +} + +impl Greyscale { + pub fn new(percent: f32, icc_profile: Option) -> Self { + Self { + percent, + icc_profile, + } + } +} + +/// Spot colors are like Cmyk, but without color space. They are essentially "named" colors +/// from specific vendors - currently they are the same as a CMYK color. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct SpotColor { + pub c: f32, + pub m: f32, + pub y: f32, + pub k: f32, +} + +impl SpotColor { + pub fn new(c: f32, m: f32, y: f32, k: f32) -> Self { + Self { c, m, y, k } + } +} diff --git a/src/conformance.rs b/src/conformance.rs new file mode 100644 index 0000000..a186b50 --- /dev/null +++ b/src/conformance.rs @@ -0,0 +1,258 @@ +//! Module regulating the comparison and feature sets / allowed plugins of a PDF document +//! +//! NOTE: All credit to Wikipedia: +//! +//! [PDF/X Versions](https://en.wikipedia.org/wiki/PDF/X) +//! +//! [PDF/A Versions](https://en.wikipedia.org/wiki/PDF/A) + +/// List of (relevant) PDF versions +/// Please note the difference between **PDF/A** (archiving), **PDF/UA** (universal acessibility), +/// **PDF/X** (printing), **PDF/E** (engineering / CAD), **PDF/VT** (large volume transactions with +/// repeated content) +#[derive(Debug, PartialEq, Eq, Clone)] +#[allow(non_camel_case_types)] +pub enum PdfConformance { + /// `PDF/A-1b` basic PDF, many features restricted + A1B_2005_PDF_1_4, + /// `PDF/A-1a` language specification, hierarchical document structure, + /// character mappings to unicode, descriptive text for images + A1A_2005_PDF_1_4, + /// `PDF/A-2:2011` - JPEG compression, transpareny, layering, OpenType fonts + A2_2011_PDF_1_7, + /// `PDF/A-2a:2011` + A2A_2011_PDF_1_7, + /// `PDF/A-2b:2011` + A2B_2011_PDF_1_7, + /// `PDF/A-2u:2011` - requires all text to be Unicode + A2U_2011_PDF_1_7, + /// `PDF/A-3` - like A2 but with embedded files (XML, CAD, etc.) + A3_2012_PDF_1_7, + /// `PDF/UA-1` extra functions for accessibility (blind, screenreaders, search, dynamic layout) + UA_2014_PDF_1_6, + /// `PDF/X-1a:2001` no ICC profiles + X1A_2001_PDF_1_3, + /// `PDF/X-3:2002` allows CMYK, spot, calibrated (managed) RGB, CIELAB, + ICC Profiles + X3_2002_PDF_1_3, + /// `PDF/X-1a:2003` Revision of `PDF/X-1a:2001` based on PDF 1.4 + X1A_2003_PDF_1_4, + /// `PDF/X-3:2003` Revision of `PDF/X-3:2002` based on PDF 1.4 + X3_2003_PDF_1_4, + /// `PDF/X-4:2010` Colour-managed, CMYK, gray, RGB or spot colour data are supported + /// as well as PDF transparency and optional content (layers) + X4_2010_PDF_1_4, + /// `PDF/X-4p:2010` Same as the above X-4:2010, but may reference an ICC profile from + /// an external file, and it's based on PDF 1.6 + X4P_2010_PDF_1_6, + /// `PDF/X-5g:2010` An extension of PDF/X-4 that enables the use of external graphical + /// content. This can be described as OPI-like (Open Prepress Interface) workflows. + /// Specifically this allows graphics to be referenced that are outside the PDF + X5G_2010_PDF_1_6, + /// `PDF/X-5pg` An extension of PDF/X-4p that enables the use of external graphical + /// content in conjunction with a reference to an external ICC Profile for the output intent. + X5PG_2010_PDF_1_6, + /// `PDF/X-5n` An extension of PDF/X-4p that allows the externally supplied ICC + /// Profile for the output intent to use a color space other than Greyscale, RGB and CMYK. + X5N_2010_PDF_1_6, + /// `PDF/E-1:2008` 3D Objects, geospatial, etc. + E1_2008_PDF_1_6, + /// `PDF/VT-1:2010` Basically a way to make a incomplete PDF as a template and the RIP program + /// is set up in a way that it can easily inject data into the PDF, for high-throughput PDFs + /// (like postcards, stamps), that require customization before printing + VT_2010_PDF_1_4, + /// Custom PDF conformance, to allow / disallow options. This allows for making very small + /// documents, for example + Custom(CustomPdfConformance), +} + +// default: save on file size +impl Default for PdfConformance { + fn default() -> Self { + Self::Custom(CustomPdfConformance::default()) + } +} + +/// Allows building custom conformance profiles. This is useful if you want very small documents for example and +/// you don't __need__ conformance with any PDF standard, you just want a PDF file. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct CustomPdfConformance { + /// Identifier for this conformance + /// + /// Default: __""__ + pub identifier: String, + /// Does this standard allow 3d content? + /// + /// Default: __false__ + pub allows_3d_content: bool, + /// Does this standard allow video content? + /// + /// Default: __false__ + pub allows_video_content: bool, + /// Does this standard allow audio content + /// + /// Default: __false__ + pub allows_audio_content: bool, + /// Does this standard allow enbedded JS? + /// + /// Default: __false__ + pub allows_embedded_javascript: bool, + /// Does this standard allow enbedding JPEG files? + /// + /// Default: __true__ + pub allows_jpeg_content: bool, + /// Does this standard require XMP metadata to be set? + /// + /// Default: __true__ + pub requires_xmp_metadata: bool, + /// Does this standard allow the default PDF fonts (Helvetica, etc.) + /// + /// _(please don't enable this if you do any work that has to be printed accurately)_ + /// + /// Default: __false__ + pub allows_default_fonts: bool, + /// Does this standard require an ICC profile to be embedded for color management? + /// + /// Default: __true__ + pub requires_icc_profile: bool, + /// Does this standard allow PDF layers? + /// + /// Default: __true__ + pub allows_pdf_layers: bool, +} + +impl Default for CustomPdfConformance { + fn default() -> Self { + CustomPdfConformance { + identifier: "".into(), + allows_3d_content: false, + allows_video_content: false, + allows_audio_content: false, + allows_embedded_javascript: false, + allows_jpeg_content: true, + requires_xmp_metadata: false, + allows_default_fonts: false, + requires_icc_profile: false, + allows_pdf_layers: true, + } + } +} + +impl PdfConformance { + /// Get the identifier string for PDF + pub fn get_identifier_string(&self) -> String { + // todo: these identifiers might not be correct in all cases + let identifier = match *self { + PdfConformance::A1B_2005_PDF_1_4 => "PDF/A-1b:2005", + PdfConformance::A1A_2005_PDF_1_4 => "PDF/A-1a:2005", + PdfConformance::A2_2011_PDF_1_7 => "PDF/A-2:2011", + PdfConformance::A2A_2011_PDF_1_7 => "PDF/A-2a:2011", + PdfConformance::A2B_2011_PDF_1_7 => "PDF/A-2b:2011", + PdfConformance::A2U_2011_PDF_1_7 => "PDF/A-2u:2011", + PdfConformance::A3_2012_PDF_1_7 => "PDF/A-3:2012", + PdfConformance::UA_2014_PDF_1_6 => "PDF/UA", + PdfConformance::X1A_2001_PDF_1_3 => "PDF/X-1a:2001", + PdfConformance::X3_2002_PDF_1_3 => "PDF/X-3:2002", + PdfConformance::X1A_2003_PDF_1_4 => "PDF/X-1a:2003", + PdfConformance::X3_2003_PDF_1_4 => "PDF/X-3:2003", + PdfConformance::X4_2010_PDF_1_4 => "PDF/X-4", + PdfConformance::X4P_2010_PDF_1_6 => "PDF/X-4P", + PdfConformance::X5G_2010_PDF_1_6 => "PDF/X-5G", + PdfConformance::X5PG_2010_PDF_1_6 => "PDF/X-5PG", + PdfConformance::X5N_2010_PDF_1_6 => "PDF/X-5N", + PdfConformance::E1_2008_PDF_1_6 => "PDF/E-1", + PdfConformance::VT_2010_PDF_1_4 => "PDF/VT", + PdfConformance::Custom(ref c) => &c.identifier, + }; + + identifier.to_string() + } + + /// __STUB__: Detects if the PDF has 3D content, but the + /// conformance to the given PDF standard does not allow it. + pub fn is_3d_content_allowed(&self) -> bool { + match *self { + PdfConformance::E1_2008_PDF_1_6 => true, + PdfConformance::Custom(ref c) => c.allows_3d_content, + _ => false, + } + } + + /// Does this conformance level allow video + pub fn is_video_content_allowed(&self) -> bool { + // todo + match *self { + PdfConformance::Custom(ref c) => c.allows_video_content, + _ => false, + } + } + + /// __STUB__: Detects if the PDF has audio content, but the + /// conformance to the given PDF standard does not allow it. + pub fn is_audio_content_allowed(&self) -> bool { + // todo + match *self { + PdfConformance::Custom(ref c) => c.allows_audio_content, + _ => false, + } + } + + /// __STUB__: Detects if the PDF has 3D content, but the + /// conformance to the given PDF standard does not allow it. + pub fn is_javascript_content_allowed(&self) -> bool { + // todo + match *self { + PdfConformance::Custom(ref c) => c.allows_embedded_javascript, + _ => false, + } + } + + /// __STUB__: Detects if the PDF has JPEG images, but the + /// conformance to the given PDF standard does not allow it + pub fn is_jpeg_content_allowed(&self) -> bool { + // todo + match *self { + PdfConformance::Custom(ref c) => c.allows_jpeg_content, + _ => true, + } + } + + /// Detects if the PDF must have XMP metadata + /// if it has to conform to the given PDF Standard + pub fn must_have_xmp_metadata(&self) -> bool { + match *self { + PdfConformance::X1A_2001_PDF_1_3 => true, + PdfConformance::X3_2002_PDF_1_3 => true, + PdfConformance::X1A_2003_PDF_1_4 => true, + PdfConformance::X3_2003_PDF_1_4 => true, + PdfConformance::X4_2010_PDF_1_4 => true, + PdfConformance::X4P_2010_PDF_1_6 => true, + PdfConformance::X5G_2010_PDF_1_6 => true, + PdfConformance::X5PG_2010_PDF_1_6 => true, + PdfConformance::Custom(ref c) => c.requires_xmp_metadata, + _ => false, + } + } + + /// Check if the conformance level must have an ICC Profile + pub fn must_have_icc_profile(&self) -> bool { + // todo + match *self { + PdfConformance::X1A_2001_PDF_1_3 => false, + PdfConformance::Custom(ref c) => c.requires_icc_profile, + _ => true, + } + } + + /// __STUB__: Detects if the PDF has layering (optional content groups), + /// but the conformance to the given PDF standard does not allow it. + pub fn is_layering_allowed(&self) -> bool { + match self { + PdfConformance::X1A_2001_PDF_1_3 => false, + PdfConformance::X3_2002_PDF_1_3 => false, + PdfConformance::X1A_2003_PDF_1_4 => false, + PdfConformance::X3_2003_PDF_1_4 => false, + PdfConformance::Custom(c) => c.allows_pdf_layers, + _ => true, + } + } +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..73a9511 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,91 @@ + +/// ## General graphics state + +/// Set line width +pub const OP_PATH_STATE_SET_LINE_WIDTH: &str = "w"; +/// Set line join +pub const OP_PATH_STATE_SET_LINE_JOIN: &str = "J"; +/// Set line cap +pub const OP_PATH_STATE_SET_LINE_CAP: &str = "j"; +/// Set miter limit +pub const OP_PATH_STATE_SET_MITER_LIMIT: &str = "M"; +/// Set line dash pattern +pub const OP_PATH_STATE_SET_LINE_DASH: &str = "d"; +/// Set rendering intent +pub const OP_PATH_STATE_SET_RENDERING_INTENT: &str = "ri"; +/// Set flatness tolerance +pub const OP_PATH_STATE_SET_FLATNESS_TOLERANCE: &str = "i"; +/// (PDF 1.2) Set graphics state from parameter dictionary +pub const OP_PATH_STATE_SET_GS_FROM_PARAM_DICT: &str = "gs"; + +/// ## Color + +/// stroking color space (PDF 1.1) +pub const OP_COLOR_SET_STROKE_CS: &str = "CS"; +/// non-stroking color space (PDF 1.1) +pub const OP_COLOR_SET_FILL_CS: &str = "cs"; +/// set stroking color (PDF 1.1) +pub const OP_COLOR_SET_STROKE_COLOR: &str = "SC"; +/// set stroking color (PDF 1.2) with support for ICC, etc. +pub const OP_COLOR_SET_STROKE_COLOR_ICC: &str = "SCN"; +/// set fill color (PDF 1.1) +pub const OP_COLOR_SET_FILL_COLOR: &str = "sc"; +/// set fill color (PDF 1.2) with support for Icc, etc. +pub const OP_COLOR_SET_FILL_COLOR_ICC: &str = "scn"; + +/// Set the stroking color space to DeviceGray +pub const OP_COLOR_SET_STROKE_CS_DEVICEGRAY: &str = "G"; +/// Set the fill color space to DeviceGray +pub const OP_COLOR_SET_FILL_CS_DEVICEGRAY: &str = "g"; +/// Set the stroking color space to DeviceRGB +pub const OP_COLOR_SET_STROKE_CS_DEVICERGB: &str = "RG"; +/// Set the fill color space to DeviceRGB +pub const OP_COLOR_SET_FILL_CS_DEVICERGB: &str = "rg"; +/// Set the stroking color space to DeviceCMYK +pub const OP_COLOR_SET_STROKE_CS_DEVICECMYK: &str = "K"; +/// Set the fill color to DeviceCMYK +pub const OP_COLOR_SET_FILL_CS_DEVICECMYK: &str = "k"; + +/// Path construction + +/// Move to point +pub const OP_PATH_CONST_MOVE_TO: &str = "m"; +/// Straight line to the two following points +pub const OP_PATH_CONST_LINE_TO: &str = "l"; +/// Cubic bezier over four following points +pub const OP_PATH_CONST_4BEZIER: &str = "c"; +/// Cubic bezier with two points in v1 +pub const OP_PATH_CONST_3BEZIER_V1: &str = "v"; +/// Cubic bezier with two points in v2 +pub const OP_PATH_CONST_3BEZIER_V2: &str = "y"; +/// Add rectangle to the path (width / height): x y width height re +pub const OP_PATH_CONST_RECT: &str = "re"; +/// Close current sub-path (for appending custom patterns along line) +pub const OP_PATH_CONST_CLOSE_SUBPATH: &str = "h"; +/// Current path is a clip path, non-zero winding order (usually in like `h W S`) +pub const OP_PATH_CONST_CLIP_NZ: &str = "W"; +/// Current path is a clip path, non-zero winding order +pub const OP_PATH_CONST_CLIP_EO: &str = "W*"; + +/// Path painting + +/// Stroke path +pub const OP_PATH_PAINT_STROKE: &str = "S"; +/// Close and stroke path +pub const OP_PATH_PAINT_STROKE_CLOSE: &str = "s"; +/// Fill path using nonzero winding number rule +pub const OP_PATH_PAINT_FILL_NZ: &str = "f"; +/// Fill path using nonzero winding number rule (obsolete) +pub const OP_PATH_PAINT_FILL_NZ_OLD: &str = "F"; +/// Fill path using even-odd rule +pub const OP_PATH_PAINT_FILL_EO: &str = "f*"; +/// Fill and stroke path using nonzero winding number rule +pub const OP_PATH_PAINT_FILL_STROKE_NZ: &str = "B"; +/// Close, fill and stroke path using nonzero winding number rule +pub const OP_PATH_PAINT_FILL_STROKE_CLOSE_NZ: &str = "b"; +/// Fill and stroke path using even-odd rule +pub const OP_PATH_PAINT_FILL_STROKE_EO: &str = "B*"; +/// Close, fill and stroke path using even odd rule +pub const OP_PATH_PAINT_FILL_STROKE_CLOSE_EO: &str = "b*"; +/// End path without filling or stroking +pub const OP_PATH_PAINT_END: &str = "n"; \ No newline at end of file diff --git a/src/date.rs b/src/date.rs new file mode 100644 index 0000000..ee5124e --- /dev/null +++ b/src/date.rs @@ -0,0 +1,186 @@ +/// wasm32-unknown-unknown polyfill + +#[cfg(all(feature = "js-sys", target_arch = "wasm32", target_os = "unknown"))] +pub use self::js_sys_date::OffsetDateTime; + +#[cfg(not(feature = "js-sys"))] +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +pub use self::unix_epoch_stub_date::OffsetDateTime; + +#[cfg(not(any(target_arch = "wasm32", target_os = "unknown")))] +pub use time::{OffsetDateTime, UtcOffset}; + +#[cfg(all(feature = "js-sys", target_arch = "wasm32", target_os = "unknown"))] +mod js_sys_date { + use js_sys::Date; + use time::Month; + + #[derive(Debug, Clone)] + pub struct OffsetDateTime(Date); + + impl OffsetDateTime { + #[inline(always)] + pub fn now_utc() -> Self { + let date = Date::new_0(); + OffsetDateTime(date) + } + + #[inline(always)] + pub fn now() -> Self { + let date = Date::new_0(); + OffsetDateTime(date) + } + + #[inline(always)] + pub fn format(&self, format: impl ToString) -> String { + // TODO + "".into() + } + + #[inline(always)] + pub fn year(&self) -> i32 { + self.0.get_full_year() as i32 + } + + #[inline(always)] + pub fn month(&self) -> Month { + match self.0.get_month() { + 0 => Month::January, + 1 => Month::February, + 2 => Month::March, + 3 => Month::April, + 4 => Month::May, + 5 => Month::June, + 6 => Month::July, + 7 => Month::August, + 8 => Month::September, + 9 => Month::October, + 10 => Month::November, + 11 => Month::December, + _ => unreachable!(), + } + } + + #[inline(always)] + pub fn day(&self) -> u8 { + self.0.get_date() as u8 + } + + #[inline(always)] + pub fn hour(&self) -> u8 { + self.0.get_hours() as u8 + } + + #[inline(always)] + pub fn minute(&self) -> u8 { + self.0.get_minutes() as u8 + } + + #[inline(always)] + pub fn second(&self) -> u8 { + self.0.get_seconds() as u8 + } + + #[inline] + pub fn offset(&self) -> super::UtcOffset { + let offset = self.0.get_timezone_offset(); + let truncated_offset = offset as i32; + let hours = (truncated_offset % 60).try_into().unwrap(); + let minutes = (truncated_offset / 60).try_into().unwrap(); + let seconds = ((offset * 60.) % 60.) as i8; + super::UtcOffset { + hours, + minutes, + seconds, + } + } + } +} + +#[cfg(not(feature = "js-sys"))] +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +mod unix_epoch_stub_date { + use time::Month; + + #[derive(Debug, Clone)] + pub struct OffsetDateTime; + impl OffsetDateTime { + #[inline(always)] + pub fn now_utc() -> Self { + OffsetDateTime + } + + #[inline(always)] + pub fn now() -> Self { + OffsetDateTime + } + + #[inline(always)] + pub fn format(&self, format: impl ToString) -> String { + // TODO + "".into() + } + + #[inline(always)] + pub fn year(&self) -> i32 { + 1970 + } + + #[inline(always)] + pub fn month(&self) -> Month { + Month::January + } + + #[inline(always)] + pub fn day(&self) -> u8 { + 1 + } + + #[inline(always)] + pub fn hour(&self) -> u8 { + 0 + } + + #[inline(always)] + pub fn minute(&self) -> u8 { + 0 + } + + #[inline(always)] + pub fn second(&self) -> u8 { + 0 + } + + #[inline] + pub fn offset(&self) -> super::UtcOffset { + super::UtcOffset { + hours: 0, + minutes: 0, + seconds: 0, + } + } + } +} + +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct UtcOffset { + hours: i8, + minutes: i8, + seconds: i8, +} + +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +impl UtcOffset { + pub const fn whole_hours(self) -> i8 { + self.hours + } + + pub const fn is_negative(self) -> bool { + self.hours < 0 || self.minutes < 0 || self.seconds < 0 + } + + pub const fn minutes_past_hour(self) -> i8 { + self.minutes + } +} diff --git a/src/deserialize.rs b/src/deserialize.rs new file mode 100644 index 0000000..f086dec --- /dev/null +++ b/src/deserialize.rs @@ -0,0 +1,3 @@ +pub(crate) fn deserialize_pdf(pdf: &PdfDocument) -> Vec { + Vec::new() +} \ No newline at end of file diff --git a/src/font.rs b/src/font.rs new file mode 100644 index 0000000..17df297 --- /dev/null +++ b/src/font.rs @@ -0,0 +1,822 @@ +/// Builtin or external font +#[derive(Debug, Clone, PartialEq)] +pub enum Font { + /// Represents one of the 14 built-in fonts (Arial, Helvetica, etc.) + BuiltinFont(BuiltinFont), + /// Represents a font loaded from an external file + ExternalFont(Parse), +} + +/// Standard built-in PDF fonts +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum BuiltinFont { + TimesRoman, + TimesBold, + TimesItalic, + TimesBoldItalic, + Helvetica, + HelveticaBold, + HelveticaOblique, + HelveticaBoldOblique, + Courier, + CourierOblique, + CourierBold, + CourierBoldOblique, + Symbol, + ZapfDingbats, +} + +impl BuiltinFont { + pub fn get_id(val: BuiltinFont) -> &'static str { + use self::BuiltinFont::*; + match val { + TimesRoman => "Times-Roman", + TimesBold => "Times-Bold", + TimesItalic => "Times-Italic", + TimesBoldItalic => "Times-BoldItalic", + Helvetica => "Helvetica", + HelveticaBold => "Helvetica-Bold", + HelveticaOblique => "Helvetica-Oblique", + HelveticaBoldOblique => "Helvetica-BoldOblique", + Courier => "Courier", + CourierOblique => "Courier-Oblique", + CourierBold => "Courier-Bold", + CourierBoldOblique => "Courier-BoldOblique", + Symbol => "Symbol", + ZapfDingbats => "ZapfDingbats", + } + } +} + + +use time::error::Parse; +use core::fmt; +use std::collections::btree_map::BTreeMap; +use std::rc::Rc; +use std::vec::Vec; +use std::boxed::Box; +use allsorts::{ + binary::read::ReadScope, + font_data::FontData, + layout::{LayoutCache, GDEFTable, GPOS, GSUB}, + tables::{ + FontTableProvider, HheaTable, MaxpTable, HeadTable, + loca::LocaTable, + cmap::CmapSubtable, + glyf::{GlyfTable, Glyph, GlyfRecord}, + }, + tables::cmap::owned::CmapSubtable as OwnedCmapSubtable, +}; + +#[derive(Clone)] +pub struct ParsedFont { + pub font_metrics: FontMetrics, + pub num_glyphs: u16, + pub hhea_table: HheaTable, + pub hmtx_data: Box<[u8]>, + pub maxp_table: MaxpTable, + pub gsub_cache: LayoutCache, + pub gpos_cache: LayoutCache, + pub opt_gdef_table: Option>, + pub glyph_records_decoded: BTreeMap, + pub space_width: Option, + pub cmap_subtable: OwnedCmapSubtable, +} + +impl PartialEq for ParsedFont { + fn eq(&self, other: &Self) -> bool { + self.font_metrics == other.font_metrics && + self.num_glyphs == other.num_glyphs && + self.hhea_table == other.hhea_table && + self.hmtx_data == other.hmtx_data && + self.maxp_table == other.maxp_table && + self.space_width == other.space_width && + self.cmap_subtable == other.cmap_subtable + } +} + +impl fmt::Debug for ParsedFont { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ParsedFont") + .field("font_metrics", &self.font_metrics) + .field("num_glyphs", &self.num_glyphs) + .field("hhea_table", &self.hhea_table) + .field("hmtx_data", &self.hmtx_data) + .field("maxp_table", &self.maxp_table) + .field("glyph_records_decoded", &self.glyph_records_decoded) + .field("space_width", &self.space_width) + .field("cmap_subtable", &self.cmap_subtable) + .finish() + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[repr(C, u8)] +pub enum GlyphOutlineOperation { + MoveTo(OutlineMoveTo), + LineTo(OutlineLineTo), + QuadraticCurveTo(OutlineQuadTo), + CubicCurveTo(OutlineCubicTo), + ClosePath, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[repr(C)] +pub struct OutlineMoveTo { + pub x: f32, + pub y: f32, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[repr(C)] +pub struct OutlineLineTo { + pub x: f32, + pub y: f32, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[repr(C)] +pub struct OutlineQuadTo { + pub ctrl_1_x: f32, + pub ctrl_1_y: f32, + pub end_x: f32, + pub end_y: f32, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[repr(C)] +pub struct OutlineCubicTo { + pub ctrl_1_x: f32, + pub ctrl_1_y: f32, + pub ctrl_2_x: f32, + pub ctrl_2_y: f32, + pub end_x: f32, + pub end_y: f32, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +pub struct GlyphOutline { + pub operations: Vec, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +struct GlyphOutlineBuilder { + operations: Vec +} + +impl Default for GlyphOutlineBuilder { + fn default() -> Self { + GlyphOutlineBuilder { operations: Vec::new() } + } +} + +impl ttf_parser::OutlineBuilder for GlyphOutlineBuilder { + fn move_to(&mut self, x: f32, y: f32) { self.operations.push(GlyphOutlineOperation::MoveTo(OutlineMoveTo { x, y })); } + fn line_to(&mut self, x: f32, y: f32) { self.operations.push(GlyphOutlineOperation::LineTo(OutlineLineTo { x, y })); } + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { self.operations.push(GlyphOutlineOperation::QuadraticCurveTo(OutlineQuadTo { ctrl_1_x: x1, ctrl_1_y: y1, end_x: x, end_y: y })); } + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { self.operations.push(GlyphOutlineOperation::CubicCurveTo(OutlineCubicTo { ctrl_1_x: x1, ctrl_1_y: y1, ctrl_2_x: x2, ctrl_2_y: y2, end_x: x, end_y: y })); } + fn close(&mut self) { self.operations.push(GlyphOutlineOperation::ClosePath); } +} + +#[derive(Debug, Clone)] +#[repr(C)] +pub struct OwnedGlyphBoundingBox { + pub max_x: i16, + pub max_y: i16, + pub min_x: i16, + pub min_y: i16, +} + +#[derive(Debug, Clone)] +pub struct OwnedGlyph { + pub bounding_box: OwnedGlyphBoundingBox, + pub horz_advance: u16, + pub outline: Option, +} + +impl OwnedGlyph { + fn from_glyph_data<'a>(glyph: &Glyph<'a>, horz_advance: u16) -> Option { + let bbox = glyph.bounding_box()?; + Some(Self { + bounding_box: OwnedGlyphBoundingBox { + max_x: bbox.x_max, + max_y: bbox.y_max, + min_x: bbox.x_min, + min_y: bbox.y_min, + }, + horz_advance, + outline: None, + }) + } +} + +impl ParsedFont { + + pub fn from_bytes(font_bytes: &[u8], font_index: usize, parse_glyph_outlines: bool) -> Option { + + use allsorts::tag; + + let scope = ReadScope::new(font_bytes); + let font_file = scope.read::>().ok()?; + let provider = font_file.table_provider(font_index).ok()?; + + let head_data = provider.table_data(tag::HEAD).ok()??.into_owned(); + let head_table = ReadScope::new(&head_data).read::().ok()?; + + let maxp_data = provider.table_data(tag::MAXP).ok()??.into_owned(); + let maxp_table = ReadScope::new(&maxp_data).read::().ok()?; + + let loca_data = provider.table_data(tag::LOCA).ok()??.into_owned(); + let loca_table = ReadScope::new(&loca_data).read_dep::>((maxp_table.num_glyphs as usize, head_table.index_to_loc_format)).ok()?; + + let glyf_data = provider.table_data(tag::GLYF).ok()??.into_owned(); + let mut glyf_table = ReadScope::new(&glyf_data).read_dep::>(&loca_table).ok()?; + + let hmtx_data = provider.table_data(tag::HMTX).ok()??.into_owned().into_boxed_slice(); + + let hhea_data = provider.table_data(tag::HHEA).ok()??.into_owned(); + let hhea_table = ReadScope::new(&hhea_data).read::().ok()?; + + let font_metrics = FontMetrics::from_bytes(font_bytes, font_index); + + // not parsing glyph outlines can save lots of memory + let glyph_records_decoded = glyf_table.records_mut() + .into_iter() + .enumerate() + .filter_map(|(glyph_index, glyph_record)| { + if glyph_index > (u16::MAX as usize) { + return None; + } + glyph_record.parse().ok()?; + let glyph_index = glyph_index as u16; + let horz_advance = allsorts::glyph_info::advance( + &maxp_table, + &hhea_table, + &hmtx_data, + glyph_index + ).unwrap_or_default(); + + match glyph_record { + GlyfRecord::Present { .. } => None, + GlyfRecord::Parsed(g) => OwnedGlyph::from_glyph_data(g, horz_advance) + .map(|s| (glyph_index, s)), + } + }).collect::>(); + + let glyph_records_decoded = glyph_records_decoded.into_iter().collect(); + + let mut font_data_impl = allsorts::font::Font::new(provider).ok()?; + + // required for font layout: gsub_cache, gpos_cache and gdef_table + let gsub_cache = font_data_impl.gsub_cache().ok()??; + let gpos_cache = font_data_impl.gpos_cache().ok()??; + let opt_gdef_table = font_data_impl.gdef_table().ok().and_then(|o| o); + let num_glyphs = font_data_impl.num_glyphs(); + + let cmap_subtable = ReadScope::new(font_data_impl.cmap_subtable_data()).read::>().ok()?.to_owned()?; + + let mut font = ParsedFont { + font_metrics, + num_glyphs, + hhea_table, + hmtx_data, + maxp_table, + gsub_cache, + gpos_cache, + opt_gdef_table, + cmap_subtable, + glyph_records_decoded, + space_width: None, + }; + + let space_width = font.get_space_width_internal(); + font.space_width = space_width; + + Some(font) + } + + fn get_space_width_internal(&mut self) -> Option { + let glyph_index = self.lookup_glyph_index(' ' as u32)?; + allsorts::glyph_info::advance(&self.maxp_table, &self.hhea_table, &self.hmtx_data, glyph_index).ok().map(|s| s as usize) + } + + /// Returns the width of the space " " character (unscaled units) + #[inline] + pub const fn get_space_width(&self) -> Option { + self.space_width + } + + /// Get the horizontal advance of a glyph index (unscaled units) + pub fn get_horizontal_advance(&self, glyph_index: u16) -> u16 { + self.glyph_records_decoded.get(&glyph_index).map(|gi| gi.horz_advance).unwrap_or_default() + } + + // get the x and y size of a glyph (unscaled units) + pub fn get_glyph_size(&self, glyph_index: u16) -> Option<(i32, i32)> { + let g = self.glyph_records_decoded.get(&glyph_index)?; + let glyph_width = g.bounding_box.max_x as i32 - g.bounding_box.min_x as i32; // width + let glyph_height = g.bounding_box.max_y as i32 - g.bounding_box.min_y as i32; // height + Some((glyph_width, glyph_height)) + } + + pub fn lookup_glyph_index(&self, c: u32) -> Option { + match self.cmap_subtable.map_glyph(c) { + Ok(Some(c)) => Some(c), + _ => None, + } + } +} + +type GlyphId = u32; +type UnicodeCodePoint = u32; +type CmapBlock = Vec<(GlyphId, UnicodeCodePoint)>; + +/// Generates a CMAP (character map) from valid cmap blocks +fn generate_cid_to_unicode_map(face_name: String, all_cmap_blocks: Vec) -> String { + let mut cid_to_unicode_map = + format!(include_str!("../assets/gid_to_unicode_beg.txt"), face_name); + + for cmap_block in all_cmap_blocks + .into_iter() + .filter(|block| !block.is_empty() || block.len() < 100) + { + cid_to_unicode_map.push_str(format!("{} beginbfchar\r\n", cmap_block.len()).as_str()); + for (glyph_id, unicode) in cmap_block { + cid_to_unicode_map.push_str(format!("<{glyph_id:04x}> <{unicode:04x}>\n").as_str()); + } + cid_to_unicode_map.push_str("endbfchar\r\n"); + } + + cid_to_unicode_map.push_str(include_str!("../assets/gid_to_unicode_end.txt")); + cid_to_unicode_map +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(C)] +pub struct FontMetrics { + // head table + pub units_per_em: u16, + pub font_flags: u16, + pub x_min: i16, + pub y_min: i16, + pub x_max: i16, + pub y_max: i16, + + // hhea table + pub ascender: i16, + pub descender: i16, + pub line_gap: i16, + pub advance_width_max: u16, + pub min_left_side_bearing: i16, + pub min_right_side_bearing: i16, + pub x_max_extent: i16, + pub caret_slope_rise: i16, + pub caret_slope_run: i16, + pub caret_offset: i16, + pub num_h_metrics: u16, + + // os/2 table + pub x_avg_char_width: i16, + pub us_weight_class: u16, + pub us_width_class: u16, + pub fs_type: u16, + pub y_subscript_x_size: i16, + pub y_subscript_y_size: i16, + pub y_subscript_x_offset: i16, + pub y_subscript_y_offset: i16, + pub y_superscript_x_size: i16, + pub y_superscript_y_size: i16, + pub y_superscript_x_offset: i16, + pub y_superscript_y_offset: i16, + pub y_strikeout_size: i16, + pub y_strikeout_position: i16, + pub s_family_class: i16, + pub panose: [u8; 10], + pub ul_unicode_range1: u32, + pub ul_unicode_range2: u32, + pub ul_unicode_range3: u32, + pub ul_unicode_range4: u32, + pub ach_vend_id: u32, + pub fs_selection: u16, + pub us_first_char_index: u16, + pub us_last_char_index: u16, + + // os/2 version 0 table + pub s_typo_ascender: Option, + pub s_typo_descender: Option, + pub s_typo_line_gap: Option, + pub us_win_ascent: Option, + pub us_win_descent: Option, + + // os/2 version 1 table + pub ul_code_page_range1: Option, + pub ul_code_page_range2: Option, + + // os/2 version 2 table + pub sx_height: Option, + pub s_cap_height: Option, + pub us_default_char: Option, + pub us_break_char: Option, + pub us_max_context: Option, + + // os/2 version 3 table + pub us_lower_optical_point_size: Option, + pub us_upper_optical_point_size: Option, +} + +impl Default for FontMetrics { + fn default() -> Self { + FontMetrics::zero() + } +} + +impl FontMetrics { + + /// Only for testing, zero-sized font, will always return 0 for every metric (`units_per_em = 1000`) + pub const fn zero() -> Self { + FontMetrics { + units_per_em: 1000, + font_flags: 0, + x_min: 0, + y_min: 0, + x_max: 0, + y_max: 0, + ascender: 0, + descender: 0, + line_gap: 0, + advance_width_max: 0, + min_left_side_bearing: 0, + min_right_side_bearing: 0, + x_max_extent: 0, + caret_slope_rise: 0, + caret_slope_run: 0, + caret_offset: 0, + num_h_metrics: 0, + x_avg_char_width: 0, + us_weight_class: 0, + us_width_class: 0, + fs_type: 0, + y_subscript_x_size: 0, + y_subscript_y_size: 0, + y_subscript_x_offset: 0, + y_subscript_y_offset: 0, + y_superscript_x_size: 0, + y_superscript_y_size: 0, + y_superscript_x_offset: 0, + y_superscript_y_offset: 0, + y_strikeout_size: 0, + y_strikeout_position: 0, + s_family_class: 0, + panose: [0; 10], + ul_unicode_range1: 0, + ul_unicode_range2: 0, + ul_unicode_range3: 0, + ul_unicode_range4: 0, + ach_vend_id: 0, + fs_selection: 0, + us_first_char_index: 0, + us_last_char_index: 0, + s_typo_ascender: None, + s_typo_descender: None, + s_typo_line_gap: None, + us_win_ascent: None, + us_win_descent: None, + ul_code_page_range1: None, + ul_code_page_range2: None, + sx_height: None, + s_cap_height: None, + us_default_char: None, + us_break_char: None, + us_max_context: None, + us_lower_optical_point_size: None, + us_upper_optical_point_size: None, + } + } + + /// Parses `FontMetrics` from a font + pub fn from_bytes(font_bytes: &[u8], font_index: usize) -> Self { + + #[derive(Default)] + struct Os2Info { + x_avg_char_width: i16, + us_weight_class: u16, + us_width_class: u16, + fs_type: u16, + y_subscript_x_size: i16, + y_subscript_y_size: i16, + y_subscript_x_offset: i16, + y_subscript_y_offset: i16, + y_superscript_x_size: i16, + y_superscript_y_size: i16, + y_superscript_x_offset: i16, + y_superscript_y_offset: i16, + y_strikeout_size: i16, + y_strikeout_position: i16, + s_family_class: i16, + panose: [u8; 10], + ul_unicode_range1: u32, + ul_unicode_range2: u32, + ul_unicode_range3: u32, + ul_unicode_range4: u32, + ach_vend_id: u32, + fs_selection: u16, + us_first_char_index: u16, + us_last_char_index: u16, + s_typo_ascender: Option, + s_typo_descender: Option, + s_typo_line_gap: Option, + us_win_ascent: Option, + us_win_descent: Option, + ul_code_page_range1: Option, + ul_code_page_range2: Option, + sx_height: Option, + s_cap_height: Option, + us_default_char: Option, + us_break_char: Option, + us_max_context: Option, + us_lower_optical_point_size: Option, + us_upper_optical_point_size: Option, + } + + let scope = ReadScope::new(font_bytes); + let font_file = match scope.read::>() { + Ok(o) => o, + Err(_) => return FontMetrics::default(), + }; + let provider = match font_file.table_provider(font_index) { + Ok(o) => o, + Err(_) => return FontMetrics::default(), + }; + let font = match allsorts::font::Font::new(provider).ok() { + Some(s) => s, + _ => return FontMetrics::default(), + }; + + // read the HHEA table to get the metrics for horizontal layout + let hhea_table = &font.hhea_table; + let head_table = match font.head_table().ok() { + Some(Some(s)) => s, + _ => return FontMetrics::default(), + }; + + let os2_table = match font.os2_table().ok() { + Some(Some(s)) => { + Os2Info { + x_avg_char_width: s.x_avg_char_width, + us_weight_class: s.us_weight_class, + us_width_class: s.us_width_class, + fs_type: s.fs_type, + y_subscript_x_size: s.y_subscript_x_size, + y_subscript_y_size: s.y_subscript_y_size, + y_subscript_x_offset: s.y_subscript_x_offset, + y_subscript_y_offset: s.y_subscript_y_offset, + y_superscript_x_size: s.y_superscript_x_size, + y_superscript_y_size: s.y_superscript_y_size, + y_superscript_x_offset: s.y_superscript_x_offset, + y_superscript_y_offset: s.y_superscript_y_offset, + y_strikeout_size: s.y_strikeout_size, + y_strikeout_position: s.y_strikeout_position, + s_family_class: s.s_family_class, + panose: s.panose, + ul_unicode_range1: s.ul_unicode_range1, + ul_unicode_range2: s.ul_unicode_range2, + ul_unicode_range3: s.ul_unicode_range3, + ul_unicode_range4: s.ul_unicode_range4, + ach_vend_id: s.ach_vend_id, + fs_selection: s.fs_selection.bits(), + us_first_char_index: s.us_first_char_index, + us_last_char_index: s.us_last_char_index, + + s_typo_ascender: s.version0.as_ref().map(|q| q.s_typo_ascender), + s_typo_descender: s.version0.as_ref().map(|q| q.s_typo_descender), + s_typo_line_gap: s.version0.as_ref().map(|q| q.s_typo_line_gap), + us_win_ascent: s.version0.as_ref().map(|q| q.us_win_ascent), + us_win_descent: s.version0.as_ref().map(|q| q.us_win_descent), + + ul_code_page_range1: s.version1.as_ref().map(|q| q.ul_code_page_range1), + ul_code_page_range2: s.version1.as_ref().map(|q| q.ul_code_page_range2), + + sx_height: s.version2to4.as_ref().map(|q| q.sx_height), + s_cap_height: s.version2to4.as_ref().map(|q| q.s_cap_height), + us_default_char: s.version2to4.as_ref().map(|q| q.us_default_char), + us_break_char: s.version2to4.as_ref().map(|q| q.us_break_char), + us_max_context: s.version2to4.as_ref().map(|q| q.us_max_context), + + us_lower_optical_point_size: s.version5.as_ref().map(|q| q.us_lower_optical_point_size), + us_upper_optical_point_size: s.version5.as_ref().map(|q| q.us_upper_optical_point_size), + } + }, + _ => Os2Info::default(), + }; + + FontMetrics { + + // head table + units_per_em: if head_table.units_per_em == 0 { + 1000_u16 + } else { + head_table.units_per_em + }, + font_flags: head_table.flags, + x_min: head_table.x_min, + y_min: head_table.y_min, + x_max: head_table.x_max, + y_max: head_table.y_max, + + // hhea table + ascender: hhea_table.ascender, + descender: hhea_table.descender, + line_gap: hhea_table.line_gap, + advance_width_max: hhea_table.advance_width_max, + min_left_side_bearing: hhea_table.min_left_side_bearing, + min_right_side_bearing: hhea_table.min_right_side_bearing, + x_max_extent: hhea_table.x_max_extent, + caret_slope_rise: hhea_table.caret_slope_rise, + caret_slope_run: hhea_table.caret_slope_run, + caret_offset: hhea_table.caret_offset, + num_h_metrics: hhea_table.num_h_metrics, + + // os/2 table + + x_avg_char_width: os2_table.x_avg_char_width, + us_weight_class: os2_table.us_weight_class, + us_width_class: os2_table.us_width_class, + fs_type: os2_table.fs_type, + y_subscript_x_size: os2_table.y_subscript_x_size, + y_subscript_y_size: os2_table.y_subscript_y_size, + y_subscript_x_offset: os2_table.y_subscript_x_offset, + y_subscript_y_offset: os2_table.y_subscript_y_offset, + y_superscript_x_size: os2_table.y_superscript_x_size, + y_superscript_y_size: os2_table.y_superscript_y_size, + y_superscript_x_offset: os2_table.y_superscript_x_offset, + y_superscript_y_offset: os2_table.y_superscript_y_offset, + y_strikeout_size: os2_table.y_strikeout_size, + y_strikeout_position: os2_table.y_strikeout_position, + s_family_class: os2_table.s_family_class, + panose: os2_table.panose, + ul_unicode_range1: os2_table.ul_unicode_range1, + ul_unicode_range2: os2_table.ul_unicode_range2, + ul_unicode_range3: os2_table.ul_unicode_range3, + ul_unicode_range4: os2_table.ul_unicode_range4, + ach_vend_id: os2_table.ach_vend_id, + fs_selection: os2_table.fs_selection, + us_first_char_index: os2_table.us_first_char_index, + us_last_char_index: os2_table.us_last_char_index, + s_typo_ascender: os2_table.s_typo_ascender.into(), + s_typo_descender: os2_table.s_typo_descender.into(), + s_typo_line_gap: os2_table.s_typo_line_gap.into(), + us_win_ascent: os2_table.us_win_ascent.into(), + us_win_descent: os2_table.us_win_descent.into(), + ul_code_page_range1: os2_table.ul_code_page_range1.into(), + ul_code_page_range2: os2_table.ul_code_page_range2.into(), + sx_height: os2_table.sx_height.into(), + s_cap_height: os2_table.s_cap_height.into(), + us_default_char: os2_table.us_default_char.into(), + us_break_char: os2_table.us_break_char.into(), + us_max_context: os2_table.us_max_context.into(), + us_lower_optical_point_size: os2_table.us_lower_optical_point_size.into(), + us_upper_optical_point_size: os2_table.us_upper_optical_point_size.into(), + } + } + + /// If set, use `OS/2.sTypoAscender - OS/2.sTypoDescender + OS/2.sTypoLineGap` to calculate the height + /// + /// See [`USE_TYPO_METRICS`](https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fss) + pub fn use_typo_metrics(&self) -> bool { + self.fs_selection & (1 << 7) != 0 + } + + pub fn get_ascender_unscaled(&self) -> i16 { + let use_typo = if !self.use_typo_metrics() { + None + } else { + self.s_typo_ascender.into() + }; + match use_typo { + Some(s) => s, + None => self.ascender, + } + } + + /// NOTE: descender is NEGATIVE + pub fn get_descender_unscaled(&self) -> i16 { + let use_typo = if !self.use_typo_metrics() { + None + } else { + self.s_typo_descender.into() + }; + match use_typo { + Some(s) => s, + None => self.descender, + } + } + + pub fn get_line_gap_unscaled(&self) -> i16 { + let use_typo = if !self.use_typo_metrics() { + None + } else { + self.s_typo_line_gap.into() + }; + match use_typo { + Some(s) => s, + None => self.line_gap, + } + } + + pub fn get_ascender(&self, target_font_size: f32) -> f32 { + self.get_ascender_unscaled() as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_descender(&self, target_font_size: f32) -> f32 { + self.get_descender_unscaled() as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_line_gap(&self, target_font_size: f32) -> f32 { + self.get_line_gap_unscaled() as f32 / self.units_per_em as f32 * target_font_size + } + + pub fn get_x_min(&self, target_font_size: f32) -> f32 { + self.x_min as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_y_min(&self, target_font_size: f32) -> f32 { + self.y_min as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_x_max(&self, target_font_size: f32) -> f32 { + self.x_max as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_y_max(&self, target_font_size: f32) -> f32 { + self.y_max as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_advance_width_max(&self, target_font_size: f32) -> f32 { + self.advance_width_max as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_min_left_side_bearing(&self, target_font_size: f32) -> f32 { + self.min_left_side_bearing as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_min_right_side_bearing(&self, target_font_size: f32) -> f32 { + self.min_right_side_bearing as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_x_max_extent(&self, target_font_size: f32) -> f32 { + self.x_max_extent as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_x_avg_char_width(&self, target_font_size: f32) -> f32 { + self.x_avg_char_width as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_y_subscript_x_size(&self, target_font_size: f32) -> f32 { + self.y_subscript_x_size as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_y_subscript_y_size(&self, target_font_size: f32) -> f32 { + self.y_subscript_y_size as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_y_subscript_x_offset(&self, target_font_size: f32) -> f32 { + self.y_subscript_x_offset as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_y_subscript_y_offset(&self, target_font_size: f32) -> f32 { + self.y_subscript_y_offset as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_y_superscript_x_size(&self, target_font_size: f32) -> f32 { + self.y_superscript_x_size as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_y_superscript_y_size(&self, target_font_size: f32) -> f32 { + self.y_superscript_y_size as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_y_superscript_x_offset(&self, target_font_size: f32) -> f32 { + self.y_superscript_x_offset as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_y_superscript_y_offset(&self, target_font_size: f32) -> f32 { + self.y_superscript_y_offset as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_y_strikeout_size(&self, target_font_size: f32) -> f32 { + self.y_strikeout_size as f32 / self.units_per_em as f32 * target_font_size + } + pub fn get_y_strikeout_position(&self, target_font_size: f32) -> f32 { + self.y_strikeout_position as f32 / self.units_per_em as f32 * target_font_size + } + + pub fn get_s_typo_ascender(&self, target_font_size: f32) -> Option { + self.s_typo_ascender + .map(|s| s as f32 / self.units_per_em as f32 * target_font_size) + } + pub fn get_s_typo_descender(&self, target_font_size: f32) -> Option { + self.s_typo_descender + .map(|s| s as f32 / self.units_per_em as f32 * target_font_size) + } + pub fn get_s_typo_line_gap(&self, target_font_size: f32) -> Option { + self.s_typo_line_gap + .map(|s| s as f32 / self.units_per_em as f32 * target_font_size) + } + pub fn get_us_win_ascent(&self, target_font_size: f32) -> Option { + self.us_win_ascent + .map(|s| s as f32 / self.units_per_em as f32 * target_font_size) + } + pub fn get_us_win_descent(&self, target_font_size: f32) -> Option { + self.us_win_descent + .map(|s| s as f32 / self.units_per_em as f32 * target_font_size) + } + pub fn get_sx_height(&self, target_font_size: f32) -> Option { + self.sx_height + .map(|s| s as f32 / self.units_per_em as f32 * target_font_size) + } + pub fn get_s_cap_height(&self, target_font_size: f32) -> Option { + self.s_cap_height + .map(|s| s as f32 / self.units_per_em as f32 * target_font_size) + } +} diff --git a/src/graphics.rs b/src/graphics.rs new file mode 100644 index 0000000..8ca1f99 --- /dev/null +++ b/src/graphics.rs @@ -0,0 +1,187 @@ +use crate::units::{Mm, Pt}; + +use crate::constants::{ + OP_PATH_CONST_CLIP_EO, + OP_PATH_CONST_CLIP_NZ, + OP_PATH_PAINT_FILL_EO, + OP_PATH_PAINT_FILL_NZ, + OP_PATH_PAINT_FILL_STROKE_CLOSE_EO, + OP_PATH_PAINT_FILL_STROKE_CLOSE_NZ, + OP_PATH_PAINT_FILL_STROKE_EO, + OP_PATH_PAINT_FILL_STROKE_NZ, +}; + +/// Rectangle struct (x, y, width, height) +#[derive(Debug, PartialEq, Clone)] +pub struct Rect { + pub x: Pt, + pub y: Pt, + pub width: Pt, + pub height: Pt, +} + +/// The rule to use in filling/clipping paint operations. +/// +/// This is meaningful in the following cases: +/// +/// - When a path uses one of the _fill_ paint operations, this will determine the rule used to +/// fill the paths. +/// - When a path uses a [clip] painting mode, this will determine the rule used to limit the +/// regions of the page affected by painting operators. +/// +/// Most of the time, `NonZero` is the appropriate option. +/// +/// [clip]: PaintMode::Clip +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum WindingOrder { + /// Make any filling or clipping paint operators follow the _even-odd rule_. + /// + /// This rule determines whether a point is inside a path by drawing a ray from that point in + /// any direction and simply counting the number of path segments that cross the ray, + /// regardless of direction. If this number is odd, the point is inside; if even, the point is + /// outside. This yields the same results as the nonzero winding number rule for paths with + /// simple shapes, but produces different results for more complex shapes. + EvenOdd, + + /// Make any filling or clipping paint operators follow the _nonzero rule_. + /// + /// This rule determines whether a given point is inside a path by conceptually drawing a ray + /// from that point to infinity in any direction and then examining the places where a segment + /// of the path crosses the ray. Starting with a count of 0, the rule adds 1 each time a path + /// segment crosses the ray from left to right and subtracts 1 each time a segment crosses from + /// right to left. After counting all the crossings, if the result is 0, the point is outside + /// the path; otherwise, it is inside. + #[default] + NonZero, +} + +impl WindingOrder { + /// Gets the operator for a clip paint operation. + #[must_use] + pub fn get_clip_op(&self) -> &'static str { + match self { + WindingOrder::NonZero => OP_PATH_CONST_CLIP_NZ, + WindingOrder::EvenOdd => OP_PATH_CONST_CLIP_EO, + } + } + + /// Gets the operator for a fill paint operation. + #[must_use] + pub fn get_fill_op(&self) -> &'static str { + match self { + WindingOrder::NonZero => OP_PATH_PAINT_FILL_NZ, + WindingOrder::EvenOdd => OP_PATH_PAINT_FILL_EO, + } + } + + /// Gets the operator for a close, fill and stroke painting operation. + #[must_use] + pub fn get_fill_stroke_close_op(&self) -> &'static str { + match self { + WindingOrder::NonZero => OP_PATH_PAINT_FILL_STROKE_CLOSE_NZ, + WindingOrder::EvenOdd => OP_PATH_PAINT_FILL_STROKE_CLOSE_EO, + } + } + + /// Gets the operator for a fill and stroke painting operation. + #[must_use] + pub fn get_fill_stroke_op(&self) -> &'static str { + match self { + WindingOrder::NonZero => OP_PATH_PAINT_FILL_STROKE_NZ, + WindingOrder::EvenOdd => OP_PATH_PAINT_FILL_STROKE_EO, + } + } +} + +/// The path-painting mode for a path. +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] +pub enum PaintMode { + /// Set the path in clipping mode instead of painting it. + /// + /// The path is not being drawing, but it will be used for clipping operations instead. The + /// rule for clipping are determined by the value [`WindingOrder`] associated to the path. + Clip, + + /// Fill the path. + #[default] + Fill, + + /// Paint a line along the path. + Stroke, + + /// Fill the path and paint a line along it. + FillStroke, +} + +#[derive(Debug, Copy, Clone)] +pub struct Point { + /// x position from the bottom left corner in pt + pub x: Pt, + /// y position from the bottom left corner in pt + pub y: Pt, +} + +impl Point { + /// Create a new point. + /// **WARNING: The reference point for a point is the bottom left corner, not the top left** + #[inline] + pub fn new(x: Mm, y: Mm) -> Self { + Self { + x: x.into(), + y: y.into(), + } + } +} + +impl PartialEq for Point { + // custom compare function because of floating point inaccuracy + fn eq(&self, other: &Point) -> bool { + if self.x.0.is_normal() + && other.x.0.is_normal() + && self.y.0.is_normal() + && other.y.0.is_normal() + { + // four floating point numbers have to match + let x_eq = self.x == other.x; + if !x_eq { + return false; + } + let y_eq = self.y == other.y; + if y_eq { + return true; + } + } + + false + } +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Line { + /// 2D Points for the line + pub points: Vec<(Point, bool)>, +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Polygon { + /// 2D Points for the line + pub rings: Vec, + /// What type of polygon is this? + pub mode: PaintMode, + /// Winding order to use for constructing this polygon + pub winding_order: WindingOrder, +} + +impl FromIterator<(Point, bool)> for Polygon { + fn from_iter>(iter: I) -> Self { + let mut points = Vec::new(); + for i in iter { + points.push(i); + } + Polygon { + rings: vec![Line { points }], + ..Default::default() + } + } +} + diff --git a/src/lib.rs b/src/lib.rs index e69de29..4a80932 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -0,0 +1,235 @@ +//! `printpdf` PDF library, second API iteration version + +use std::collections::BTreeMap; + +use annotation::{LinkAnnotation, PageAnnotation}; +use conformance::PdfConformance; +use font::ParsedFont; +use ops::PdfPage; +use time::OffsetDateTime; +use utils::{random_character_string_32, to_pdf_xmp_date}; +use xobject::XObject; + +/// Default ICC profile, necessary if `PdfMetadata::must_have_icc_profile()` return true +pub const ICC_PROFILE_ECI_V2: &[u8] = include_bytes!("../assets/CoatedFOGRA39.icc"); + +/// Link / bookmark annotation handling +pub mod annotation; +/// PDF standard handling +pub mod conformance; +/// Transformation and text matrices +pub mod matrix; +/// Units (Pt, Mm, Px, etc.) +pub mod units; +/// Date handling (stubs for platforms that don't support access to time clocks, such as wasm32-unknown) +pub mod date; +/// Font and codepoint handling +pub mod font; +/// Point / line / polygon handling +pub mod graphics; +/// Page operations +pub mod ops; +/// Color handling +pub mod color; +/// XObject handling +pub mod xobject; +/// Constants and library includes +pub(crate) mod constants; +/// Utility functions (random strings, numbers, timestamp formatting) +pub(crate) mod utils; + +/// Internal ID for page annotations +#[derive(Debug, PartialEq, Clone)] +pub struct PageAnnotId(pub String); + +/// Internal ID for link annotations +#[derive(Debug, PartialEq, Clone)] +pub struct LinkAnnotId(pub String); + +/// Internal ID for XObjects +#[derive(Debug, PartialEq, Clone)] +pub struct XObjectId(pub String); + +/// Internal ID for Fonts +#[derive(Debug, PartialEq, Clone)] +pub struct FontId(pub String); + +/// Internal ID for Layers +#[derive(Debug, PartialEq, Clone)] +pub struct LayerInternalId(pub String); + +/// Internal ID for extended graphic states +#[derive(Debug, PartialEq, Clone)] +pub struct ExtendedGraphicsStateId(pub String); + +/// Internal ID for ICC profiles +#[derive(Debug, PartialEq, Clone)] +pub struct IccProfileId(pub String); + +/// Parsed PDF document +#[derive(Debug, PartialEq, Clone)] +pub struct PdfDocument { + /// Metadata about the document (author, info, XMP metadata, etc.) + pub metadata: PdfMetadata, + /// Resources shared between pages, such as fonts, XObjects, images, forms, ICC profiles, etc. + pub resources: PdfResources, + /// Document-level bookmarks (used for the outline) + pub bookmarks: PageAnnotMap, + /// Page contents + pub pages: Vec, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct PdfResources { + /// Fonts found in the PDF file, indexed by the sha256 of their contents + pub fonts: PdfFontMap, + /// ICC profiles in this document, indexed by the sha256 of their contents + pub icc: IccProfileMap, + /// XObjects (forms, images, embedded PDF contents, etc.) + pub xobjects: XObjectMap, + /// Annotations for links between rects on pages + pub links: LinkAnnotMap, +} + +#[derive(Debug, PartialEq, Default, Clone)] +pub struct PdfFontMap { + pub map: BTreeMap, +} + +#[derive(Debug, PartialEq, Default, Clone)] +pub struct IccProfileMap { + pub map: BTreeMap, +} + +#[derive(Debug, PartialEq, Default, Clone)] +pub struct ParsedIccProfile { + +} + +#[derive(Debug, PartialEq, Default, Clone)] +pub struct XObjectMap { + pub map: BTreeMap, +} + +#[derive(Debug, PartialEq, Default, Clone)] +pub struct PageAnnotMap { + pub map: BTreeMap, +} + +#[derive(Debug, PartialEq, Default, Clone)] +pub struct LinkAnnotMap { + pub map: BTreeMap, +} + + +/// This is a wrapper in order to keep shared data between the documents XMP metadata and +/// the "Info" dictionary in sync +#[derive(Debug, PartialEq, Clone)] +pub struct PdfMetadata { + /// Document information + pub info: PdfDocumentInfo, + /// XMP Metadata. Is ignored on save if the PDF conformance does not allow XMP + pub xmp: Option, +} + +impl PdfMetadata { + /// Consumes the XmpMetadata and turns it into a PDF Object. + /// This is similar to the + pub(crate) fn xmp_metadata_string(self) -> String { + + // Shared between XmpMetadata and DocumentInfo + let trapping = if self.info.trapped { "True" } else { "False" }; + + // let xmp_instance_id = "2898d852-f86f-4479-955b-804d81046b19"; + let instance_id = random_character_string_32(); + let create_date = to_pdf_xmp_date(&self.info.creation_date); + let modification_date = to_pdf_xmp_date(&self.info.modification_date); + let metadata_date = to_pdf_xmp_date(&self.info.metadata_date); + + let pdf_x_version = self.info.conformance.get_identifier_string(); + let document_version = self.info.version.to_string(); + let document_id = self.info.identifier.to_string(); + + let rendition_class = match self.xmp.as_ref().and_then(|s| s.rendition_class.clone()) { + Some(class) => class, + None => "".to_string(), + }; + + format!( + include_str!("../assets/catalog_xmp_metadata.txt"), + create = create_date, + modify = modification_date, + mdate = metadata_date, + title = self.info.document_title, + id = document_id, + instance = instance_id, + class = rendition_class, + version = document_version, + pdfx = pdf_x_version, + trapping = trapping, + creator = self.info.creator, + subject = self.info.subject, + keywords = self.info.keywords.join(","), + identifier = self.info.identifier, + producer = self.info.producer + ) + } +} + +/// Initial struct for Xmp metatdata. This should be expanded later for XML handling, etc. +/// Right now it just fills out the necessary fields +#[derive(Debug, PartialEq, Clone)] +pub struct XmpMetadata { + /// Web-viewable or "default" or to be left empty. Usually "default". + pub rendition_class: Option, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct PdfDocumentInfo { + /// Is the document trapped? + pub trapped: bool, + /// PDF document version + pub version: u32, + /// Creation date of the document + pub creation_date: OffsetDateTime, + /// Modification date of the document + pub modification_date: OffsetDateTime, + /// Creation date of the metadata + pub metadata_date: OffsetDateTime, + /// PDF Standard + pub conformance: PdfConformance, + /// PDF document title + pub document_title: String, + /// PDF document author + pub author: String, + /// The creator of the document + pub creator: String, + /// The producer of the document + pub producer: String, + /// Keywords associated with the document + pub keywords: Vec, + /// The subject of the document + pub subject: String, + /// Identifier associated with the document + pub identifier: String, +} + +impl Default for PdfDocumentInfo { + fn default() -> Self { + Self { + trapped: false, + version: 1, + creation_date: OffsetDateTime::UNIX_EPOCH, + modification_date: OffsetDateTime::UNIX_EPOCH, + metadata_date: OffsetDateTime::UNIX_EPOCH, + conformance: PdfConformance::default(), + document_title: String::new(), + author: String::new(), + creator: String::new(), + producer: String::new(), + keywords: Vec::new(), + subject: String::new(), + identifier: String::new(), + } + } +} diff --git a/src/matrix.rs b/src/matrix.rs new file mode 100644 index 0000000..5577321 --- /dev/null +++ b/src/matrix.rs @@ -0,0 +1,316 @@ +//! Current transformation matrix, for transforming shapes (rotate, translate, scale) + +use crate::units::Pt; +use lopdf; +use lopdf::content::Operation; + +/// PDF "current transformation matrix". Once set, will operate on all following shapes, +/// until the `layer.restore_graphics_state()` is called. It is important to +/// call `layer.save_graphics_state()` earlier. +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum CurTransMat { + /// Translation matrix (in points from bottom left corner) + /// X and Y can have different values + Translate(Pt, Pt), + /// Rotation matrix (clockwise, in degrees) + Rotate(f32), + /// Combined rotate + translate matrix + TranslateRotate(Pt, Pt, f32), + /// Scale matrix (1.0 = 100% scale, no change) + /// X and Y can have different values + Scale(f32, f32), + /// Raw (PDF-internal) PDF matrix + Raw([f32; 6]), + /// Identity matrix + Identity, +} + +impl CurTransMat { + pub fn combine_matrix(a: [f32; 6], b: [f32; 6]) -> [f32; 6] { + let a = [ + [a[0], a[1], 0.0, 0.0], + [a[2], a[3], 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [a[4], a[5], 0.0, 1.0], + ]; + + let b = [ + [b[0], b[1], 0.0, 0.0], + [b[2], b[3], 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [b[4], b[5], 0.0, 1.0], + ]; + + let result = [ + [ + mul_add( + a[0][0], + b[0][0], + mul_add( + a[0][1], + b[1][0], + mul_add(a[0][2], b[2][0], a[0][3] * b[3][0]), + ), + ), + mul_add( + a[0][0], + b[0][1], + mul_add( + a[0][1], + b[1][1], + mul_add(a[0][2], b[2][1], a[0][3] * b[3][1]), + ), + ), + mul_add( + a[0][0], + b[0][2], + mul_add( + a[0][1], + b[1][2], + mul_add(a[0][2], b[2][2], a[0][3] * b[3][2]), + ), + ), + mul_add( + a[0][0], + b[0][3], + mul_add( + a[0][1], + b[1][3], + mul_add(a[0][2], b[2][3], a[0][3] * b[3][3]), + ), + ), + ], + [ + mul_add( + a[1][0], + b[0][0], + mul_add( + a[1][1], + b[1][0], + mul_add(a[1][2], b[2][0], a[1][3] * b[3][0]), + ), + ), + mul_add( + a[1][0], + b[0][1], + mul_add( + a[1][1], + b[1][1], + mul_add(a[1][2], b[2][1], a[1][3] * b[3][1]), + ), + ), + mul_add( + a[1][0], + b[0][2], + mul_add( + a[1][1], + b[1][2], + mul_add(a[1][2], b[2][2], a[1][3] * b[3][2]), + ), + ), + mul_add( + a[1][0], + b[0][3], + mul_add( + a[1][1], + b[1][3], + mul_add(a[1][2], b[2][3], a[1][3] * b[3][3]), + ), + ), + ], + [ + mul_add( + a[2][0], + b[0][0], + mul_add( + a[2][1], + b[1][0], + mul_add(a[2][2], b[2][0], a[2][3] * b[3][0]), + ), + ), + mul_add( + a[2][0], + b[0][1], + mul_add( + a[2][1], + b[1][1], + mul_add(a[2][2], b[2][1], a[2][3] * b[3][1]), + ), + ), + mul_add( + a[2][0], + b[0][2], + mul_add( + a[2][1], + b[1][2], + mul_add(a[2][2], b[2][2], a[2][3] * b[3][2]), + ), + ), + mul_add( + a[2][0], + b[0][3], + mul_add( + a[2][1], + b[1][3], + mul_add(a[2][2], b[2][3], a[2][3] * b[3][3]), + ), + ), + ], + [ + mul_add( + a[3][0], + b[0][0], + mul_add( + a[3][1], + b[1][0], + mul_add(a[3][2], b[2][0], a[3][3] * b[3][0]), + ), + ), + mul_add( + a[3][0], + b[0][1], + mul_add( + a[3][1], + b[1][1], + mul_add(a[3][2], b[2][1], a[3][3] * b[3][1]), + ), + ), + mul_add( + a[3][0], + b[0][2], + mul_add( + a[3][1], + b[1][2], + mul_add(a[3][2], b[2][2], a[3][3] * b[3][2]), + ), + ), + mul_add( + a[3][0], + b[0][3], + mul_add( + a[3][1], + b[1][3], + mul_add(a[3][2], b[2][3], a[3][3] * b[3][3]), + ), + ), + ], + ]; + + [ + result[0][0], + result[0][1], + result[1][0], + result[1][1], + result[3][0], + result[3][1], + ] + } +} + +/// Multiply add. Computes `(self * a) + b` with workaround for +/// arm-unknown-linux-gnueabi. +/// +/// `{f32, f64}::mul_add` is completly broken on arm-unknown-linux-gnueabi. +/// See issue https://github.com/rust-lang/rust/issues/46950. +#[inline(always)] +fn mul_add(a: f32, b: f32, c: f32) -> f32 { + if cfg!(all( + target_arch = "arm", + target_os = "linux", + target_env = "gnu" + )) { + // Workaround has two rounding errors and less accurate result, + // but for PDF it doesn't matter much. + (a * b) + c + } else { + a.mul_add(b, c) + } +} + +/// Text matrix. Text placement is a bit different, but uses the same +/// concepts as a CTM that's why it's merged here +/// +/// Note: `TextScale` does not exist. Use `layer.set_word_spacing()` +/// and `layer.set_character_spacing()` to specify the scaling between words +/// and characters. +#[derive(Debug, Copy, PartialEq, Clone)] +pub enum TextMatrix { + /// Text rotation matrix, used for rotating text + Rotate(f32), + /// Text translate matrix, used for indenting (transforming) text + /// (different to regular text placement) + Translate(Pt, Pt), + /// Combined translate + rotate matrix + TranslateRotate(Pt, Pt, f32), + /// Raw matrix (/tm operator) + Raw([f32; 6]), +} + +impl From for [f32; 6] { + fn from(val: TextMatrix) -> Self { + use self::TextMatrix::*; + match val { + Translate(x, y) => { + // 1 0 0 1 x y cm + [1.0, 0.0, 0.0, 1.0, x.0, y.0] + } + Rotate(rot) => { + let rad = (360.0 - rot).to_radians(); + [rad.cos(), -rad.sin(), rad.sin(), rad.cos(), 0.0, 0.0] /* cos sin -sin cos 0 0 cm */ + } + Raw(r) => r, + TranslateRotate(x, y, rot) => { + let rad = (360.0 - rot).to_radians(); + [rad.cos(), -rad.sin(), rad.sin(), rad.cos(), x.0, y.0] /* cos sin -sin cos x y cm */ + } + } + } +} + +impl From for [f32; 6] { + fn from(val: CurTransMat) -> Self { + use self::CurTransMat::*; + match val { + Translate(x, y) => { + // 1 0 0 1 x y cm + [1.0, 0.0, 0.0, 1.0, x.0, y.0] + } + TranslateRotate(x, y, rot) => { + let rad = (360.0 - rot).to_radians(); + [rad.cos(), -rad.sin(), rad.sin(), rad.cos(), x.0, y.0] /* cos sin -sin cos x y cm */ + } + Rotate(rot) => { + // cos sin -sin cos 0 0 cm + let rad = (360.0 - rot).to_radians(); + [rad.cos(), -rad.sin(), rad.sin(), rad.cos(), 0.0, 0.0] + } + Raw(r) => r, + Scale(x, y) => { + // x 0 0 y 0 0 cm + [x, 0.0, 0.0, y, 0.0, 0.0] + } + Identity => [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], + } + } +} + +#[test] +fn test_ctm_translate() { + use self::*; + + // test that the translation matrix look like what PDF expects + let ctm_trans = CurTransMat::Translate(Pt(150.0), Pt(50.0)); + let ctm_trans_arr: [f32; 6] = ctm_trans.into(); + assert_eq!([1.0_f32, 0.0, 0.0, 1.0, 150.0, 50.0], ctm_trans_arr); + + let ctm_scale = CurTransMat::Scale(2.0, 4.0); + let ctm_scale_arr: [f32; 6] = ctm_scale.into(); + assert_eq!([2.0_f32, 0.0, 0.0, 4.0, 0.0, 0.0], ctm_scale_arr); + + let ctm_rot = CurTransMat::Rotate(30.0); + let ctm_rot_arr: [f32; 6] = ctm_rot.into(); + assert_eq!( + [0.8660253, 0.5000002, -0.5000002, 0.8660253, 0.0, 0.0], + ctm_rot_arr + ); +} diff --git a/src/ops.rs b/src/ops.rs new file mode 100644 index 0000000..019ea46 --- /dev/null +++ b/src/ops.rs @@ -0,0 +1,80 @@ +use crate::{color::Color, graphics::{Line, Point, Polygon, Rect}, matrix::{CurTransMat, TextMatrix}, units::Pt, ExtendedGraphicsStateId, FontId, LayerInternalId, LinkAnnotId, PageAnnotId, XObjectId}; + +#[derive(Debug, PartialEq, Clone)] +pub struct PdfPage { + pub media_box: Rect, + pub trim_box: Rect, + pub crop_box: Rect, + pub ops: Vec, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum LayerIntent { + View, + Design, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum LayerSubtype { + Artwork, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Layer { + pub name: String, + pub creator: String, + pub intent: LayerIntent, + pub usage: LayerSubtype, +} + +/// Operations that can occur in a PDF page +#[derive(Debug, Clone)] +pub enum Op { + BeginLayer { layer_id: LayerInternalId }, + EndLayer { layer_id: LayerInternalId }, + + SaveGraphicsState, + RestoreGraphicsState, + UseGraphicsState { id: ExtendedGraphicsStateId }, + + BeginTextSection, + EndTextSection, + + WriteText { text: String, font: FontId }, + SetFont { font: FontId }, + SetTextCursor { pos: Point }, + SetFillColor { col: Color }, + SetOutlineColor { col: Color }, + + DrawLine { line: Line }, + DrawPolygon { polygon: Polygon }, + SetTransformationMatrix { matrix: CurTransMat }, + SetTextMatrix { matrix: TextMatrix }, + + LinkAnnotation { link_ref: LinkAnnotId }, + InstantiateXObject { xobj_id: XObjectId, transformations: Vec }, + Unknown { key: String, value: lopdf::content::Operation }, +} + +impl PartialEq for Op { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::BeginLayer { layer_id: l_layer_id }, Self::BeginLayer { layer_id: r_layer_id }) => l_layer_id == r_layer_id, + (Self::EndLayer { layer_id: l_layer_id }, Self::EndLayer { layer_id: r_layer_id }) => l_layer_id == r_layer_id, + (Self::UseGraphicsState { id: l_id }, Self::UseGraphicsState { id: r_id }) => l_id == r_id, + (Self::WriteText { text: l_text, font: l_font }, Self::WriteText { text: r_text, font: r_font }) => l_text == r_text && l_font == r_font, + (Self::SetFont { font: l_font }, Self::SetFont { font: r_font }) => l_font == r_font, + (Self::SetTextCursor { pos: l_pos }, Self::SetTextCursor { pos: r_pos }) => l_pos == r_pos, + (Self::SetFillColor { col: l_col }, Self::SetFillColor { col: r_col }) => l_col == r_col, + (Self::SetOutlineColor { col: l_col }, Self::SetOutlineColor { col: r_col }) => l_col == r_col, + (Self::DrawLine { line: l_line }, Self::DrawLine { line: r_line }) => l_line == r_line, + (Self::DrawPolygon { polygon: l_polygon }, Self::DrawPolygon { polygon: r_polygon }) => l_polygon == r_polygon, + (Self::SetTransformationMatrix { matrix: l_matrix }, Self::SetTransformationMatrix { matrix: r_matrix }) => l_matrix == r_matrix, + (Self::SetTextMatrix { matrix: l_matrix }, Self::SetTextMatrix { matrix: r_matrix }) => l_matrix == r_matrix, + (Self::LinkAnnotation { link_ref: l_link_ref }, Self::LinkAnnotation { link_ref: r_link_ref }) => l_link_ref == r_link_ref, + (Self::InstantiateXObject { xobj_id: l_xobj_id, transformations: l_transformations }, Self::InstantiateXObject { xobj_id: r_xobj_id, transformations: r_transformations }) => l_xobj_id == r_xobj_id && l_transformations == r_transformations, + (Self::Unknown { key: l_key, value: l_value }, Self::Unknown { key: r_key, value: r_value }) => l_key == r_key && l_value.operator == r_value.operator && l_value.operands == l_value.operands, + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} \ No newline at end of file diff --git a/src/serialize.rs b/src/serialize.rs new file mode 100644 index 0000000..65246fd --- /dev/null +++ b/src/serialize.rs @@ -0,0 +1,3 @@ +pub fn parse(bytes: &[u8]) -> PdfDocument { + PdfDocument::default() +} \ No newline at end of file diff --git a/src/units.rs b/src/units.rs new file mode 100644 index 0000000..40e580b --- /dev/null +++ b/src/units.rs @@ -0,0 +1,277 @@ +//! Scaling types for reducing errors between conversions between point (pt) and millimeter (mm) + +use std::cmp::Ordering; +use std::num::FpCategory; + +macro_rules! impl_partialeq { + ($t:ty) => { + impl PartialEq for $t { + // custom compare function because of floating point inaccuracy + fn eq(&self, other: &$t) -> bool { + if (self.0.classify() == FpCategory::Zero + || self.0.classify() == FpCategory::Normal) + && (other.0.classify() == FpCategory::Zero + || other.0.classify() == FpCategory::Normal) + { + // four floating point numbers have to match + (self.0 * 1000.0).round() == (other.0 * 1000.0).round() + } else { + false + } + } + } + }; +} + +macro_rules! impl_ord { + ($t:ty) => { + impl Ord for $t { + // custom compare function to offer ordering + fn cmp(&self, other: &$t) -> Ordering { + if self.0 < other.0 { + Ordering::Less + } else if self.0 > other.0 { + Ordering::Greater + } else { + Ordering::Equal + } + } + } + }; +} + +/// Scale in millimeter +#[derive(Debug, Default, Copy, Clone, PartialOrd)] +pub struct Mm(pub f32); + +impl Mm { + pub fn into_pt(&self) -> Pt { + let pt: Pt = (*self).into(); + pt + } +} + +impl From for Mm { + fn from(value: Pt) -> Mm { + Mm(value.0 * 0.352_778_f32) + } +} + +impl Eq for Mm {} + +impl_partialeq!(Mm); +impl_ord!(Mm); + +/// Scale in point +#[derive(Debug, Default, Copy, Clone, PartialOrd)] +pub struct Pt(pub f32); + +impl From for Pt { + fn from(value: Mm) -> Pt { + Pt(value.0 * 2.834_646_f32) + } +} + +impl From for ::lopdf::Object { + fn from(value: Pt) -> Self { + Self::Real(value.0) + } +} + +impl Eq for Pt {} + +impl_partialeq!(Pt); +impl_ord!(Pt); + +/// Scale in pixels +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub struct Px(pub usize); + +impl Px { + pub fn into_pt(self, dpi: f32) -> Pt { + Mm(self.0 as f32 * (25.4 / dpi)).into() + } +} + +use std::ops::{Add, Div, Mul, Sub}; +use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign}; + +macro_rules! impl_add_self { + ($type:ident) => { + impl Add for $type { + type Output = Self; + fn add(self, other: Self) -> Self { + Self { + 0: self.0 + other.0, + } + } + } + }; +} + +macro_rules! impl_add_assign_self { + ($type:ident) => { + impl AddAssign for $type { + fn add_assign(&mut self, other: Self) { + self.0 += other.0; + } + } + }; +} + +macro_rules! impl_sub_assign_self { + ($type:ident) => { + impl SubAssign for $type { + fn sub_assign(&mut self, other: Self) { + self.0 -= other.0; + } + } + }; +} + +macro_rules! impl_sub_self { + ($type:ident) => { + impl Sub for $type { + type Output = Self; + fn sub(self, other: Self) -> Self { + Self { + 0: self.0 - other.0, + } + } + } + }; +} + +macro_rules! impl_mul_f32 { + ($type:ident) => { + impl Mul for $type { + type Output = Self; + fn mul(self, other: f32) -> Self { + Self { 0: self.0 * other } + } + } + }; +} + +macro_rules! impl_mul_assign_f32 { + ($type:ident) => { + impl MulAssign for $type { + fn mul_assign(&mut self, other: f32) { + self.0 *= other; + } + } + }; +} + +macro_rules! impl_div { + ($type:ident) => { + impl Div<$type> for $type { + type Output = f32; + fn div(self, other: $type) -> Self::Output { + self.0 / other.0 + } + } + impl Div for $type { + type Output = Self; + fn div(self, other: f32) -> Self::Output { + Self { 0: self.0 / other } + } + } + }; +} + +macro_rules! impl_div_assign_f32 { + ($type:ident) => { + impl DivAssign for $type { + fn div_assign(&mut self, other: f32) { + self.0 /= other; + } + } + }; +} + +impl_add_self!(Mm); +impl_add_self!(Pt); +impl_add_self!(Px); + +impl_add_assign_self!(Mm); +impl_add_assign_self!(Pt); +impl_add_assign_self!(Px); + +impl_sub_assign_self!(Mm); +impl_sub_assign_self!(Pt); +impl_sub_assign_self!(Px); + +impl_sub_self!(Mm); +impl_sub_self!(Pt); +impl_sub_self!(Px); + +impl_mul_f32!(Mm); +impl_mul_f32!(Pt); + +impl_mul_assign_f32!(Mm); +impl_mul_assign_f32!(Pt); + +impl_div!(Mm); +impl_div!(Pt); + +impl_div_assign_f32!(Mm); +impl_div_assign_f32!(Pt); + +#[test] +fn point_to_mm_conversion() { + let pt1: Mm = Pt(1.0).into(); + let pt2: Mm = Pt(15.0).into(); + assert_eq!(pt1, Mm(0.352778)); + assert_eq!(pt2, Mm(5.29167)); +} + +#[test] +fn mm_to_point_conversion() { + let mm1: Pt = Mm(1.0).into(); + let mm2: Pt = Mm(23.0).into(); + assert_eq!(mm1, Pt(2.83464745483286)); + assert_eq!(mm2, Pt(65.1969)); +} + +#[test] +fn mm_eq_zero_check() { + let mm1: Mm = Mm(0.0).into(); + let mm2: Mm = Mm(0.0).into(); + assert_eq!(mm1, mm2); + assert_eq!(mm1, Mm(0.0)); + assert_eq!(mm2, Mm(0.0)); +} + +#[test] +fn max_mm() { + let mm_vector = vec![Mm(0.0), Mm(1.0), Mm(2.0)]; + assert_eq!(mm_vector.iter().max().unwrap(), &Mm(2.0)); +} + +#[test] +fn min_mm() { + let mm_vector = vec![Mm(0.0), Mm(1.0), Mm(2.0)]; + assert_eq!(mm_vector.iter().min().unwrap(), &Mm(0.0)); +} + +#[test] +fn pt_eq_zero_check() { + let pt1: Pt = Pt(0.0).into(); + let pt2: Pt = Pt(0.0).into(); + assert_eq!(pt1, pt2); + assert_eq!(pt1, Pt(0.0)); + assert_eq!(pt2, Pt(0.0)); +} + +#[test] +fn max_pt() { + let pt_vector = vec![Pt(0.0), Pt(1.0), Pt(2.0)]; + assert_eq!(pt_vector.iter().max().unwrap(), &Pt(2.0)); +} + +#[test] +fn min_pt() { + let pt_vector = vec![Pt(0.0), Pt(1.0), Pt(2.0)]; + assert_eq!(pt_vector.iter().min().unwrap(), &Pt(0.0)); +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..37c8a57 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,141 @@ + +use std::sync::atomic::{AtomicUsize, Ordering}; +use crate::date::OffsetDateTime; + +/// Since the random number generator doesn't have to be cryptographically secure +/// it doesn't make sense to import the entire rand library, so this is just a +/// xorshift pseudo-random function +static RAND_SEED: AtomicUsize = AtomicUsize::new(2100); + +/// Xorshift-based random number generator. Impure function +pub(crate) fn random_number() -> usize { + let mut x = RAND_SEED.fetch_add(21, Ordering::SeqCst); + #[cfg(target_pointer_width = "64")] + { + x ^= x << 21; + x ^= x >> 35; + x ^= x << 4; + x + } + + #[cfg(target_pointer_width = "32")] + { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + x + } +} + +/// Returns a string with 32 random characters +pub(crate) fn random_character_string_32() -> String { + const MAX_CHARS: usize = 32; + let mut final_string = String::with_capacity(MAX_CHARS); + let mut char_pos = 0; + + 'outer: while char_pos < MAX_CHARS { + let rand = format!("{}", crate::utils::random_number()); + for ch in rand.chars() { + if char_pos < MAX_CHARS { + final_string.push(u8_to_char(ch.to_digit(10).unwrap() as u8)); + char_pos += 1; + } else { + break 'outer; + } + } + } + + final_string +} + +// D:20170505150224+02'00' +pub(crate) fn to_pdf_time_stamp_metadata(date: &OffsetDateTime) -> String { + let offset = date.offset(); + let offset_sign = if offset.is_negative() { '-' } else { '+' }; + format!( + "D:{:04}{:02}{:02}{:02}{:02}{:02}{offset_sign}{:02}'{:02}'", + date.year(), + u8::from(date.month()), + date.day(), + date.hour(), + date.minute(), + date.second(), + offset.whole_hours().abs(), + offset.minutes_past_hour().abs(), + ) +} + +// D:2018-09-19T10:05:05+00'00' +pub(crate) fn to_pdf_xmp_date(date: &OffsetDateTime) -> String { + // Since the time is in UTC, we know that the time zone + // difference to UTC is 0 min, 0 sec, hence the 00'00 + format!( + "D:{:04}-{:02}-{:02}T{:02}:{:02}:{:02}+00'00'", + date.year(), + date.month(), + date.day(), + date.hour(), + date.minute(), + date.second(), + ) +} + +/// `0 => A`, `1 => B`, and so on +#[inline(always)] +fn u8_to_char(input: u8) -> char { + (b'A' + input) as char +} + +#[cfg(any(debug_assertions, feature = "less-optimization"))] +#[inline] +pub fn compress_stream(stream: lopdf::Stream) -> lopdf::Stream { + stream +} + +#[cfg(all(not(debug_assertions), not(feature = "less-optimization")))] +#[inline] +pub fn compress_stream(mut stream: lopdf::Stream) -> lopdf::Stream { + let _ = stream.compress(); + stream +} + +#[cfg(feature = "images")] +fn preprocess_image_with_alpha( + color_type: ColorType, + image_data: Vec, + dim: (u32, u32), +) -> (ImageXObject, Option) { + let (color_type, image_data, smask_data) = match color_type { + ColorType::Rgba8 => { + let (rgb, alpha) = rgba_to_rgb(image_data); + (ColorType::Rgb8, rgb, Some(alpha)) + } + _ => (color_type, image_data, None), + }; + let color_bits = ColorBits::from(color_type); + let color_space = ColorSpace::from(color_type); + + let img = ImageXObject { + width: Px(dim.0 as usize), + height: Px(dim.1 as usize), + color_space, + bits_per_component: color_bits, + image_data, + interpolate: true, + image_filter: None, + clipping_bbox: None, + smask: None, + }; + let img_mask = smask_data.map(|smask| ImageXObject { + width: img.width, + height: img.height, + color_space: ColorSpace::Greyscale, + bits_per_component: ColorBits::Bit8, + interpolate: false, + image_data: smask, + image_filter: None, + clipping_bbox: None, + smask: None, + }); + (img, img_mask) +} diff --git a/src/xobject.rs b/src/xobject.rs new file mode 100644 index 0000000..ab6ff1b --- /dev/null +++ b/src/xobject.rs @@ -0,0 +1,212 @@ +use crate::{color::{ColorBits, ColorSpace}, matrix::CurTransMat, units::Px, OffsetDateTime}; + +/* Parent: Resources dictionary of the page */ +/// External object that gets reference outside the PDF content stream +/// Gets constructed similar to the `ExtGState`, then inserted into the `/XObject` dictionary +/// on the page. You can instantiate `XObjects` with the `/Do` operator. The `layer.add_xobject()` +/// (or better yet, the `layer.add_image()`, `layer.add_form()`) methods will do this for you. +#[derive(Debug, PartialEq, Clone)] +pub enum XObject { + /* /Subtype /Image */ + /// Image XObject, for images + Image(ImageXObject), + /* /Subtype /Form */ + /// Form XObject, for PDF forms + Form(Box), + /* /Subtype /PS */ + /// Embedded PostScript XObject, for legacy applications + /// You can embed PostScript in a PDF, it is not recommended + PostScript(PostScriptXObject), + /// XObject embedded from an external stream + /// + /// This is mainly used to add XObjects to the resources that the library + /// doesn't support natively (such as gradients, patterns, etc). + /// + /// The only thing this does is to ensure that this stream is set on + /// the /Resources dictionary of the page. The `XObjectRef` returned + /// by `add_xobject()` is the unique name that can be used to invoke + /// the `/Do` operator (by the `use_xobject`) + External(lopdf::Stream), +} + + +#[derive(Debug, PartialEq, Clone)] +pub struct ImageXObject { + /// Width of the image (original width, not scaled width) + pub width: Px, + /// Height of the image (original height, not scaled height) + pub height: Px, + /// Color space (Greyscale, RGB, CMYK) + pub color_space: ColorSpace, + /// Bits per color component (1, 2, 4, 8, 16) - 1 for black/white, 8 Greyscale / RGB, etc. + /// If using a JPXDecode filter (for JPEG images), this can be inferred from the image data + pub bits_per_component: ColorBits, + /// Should the image be interpolated when scaled? + pub interpolate: bool, + /// The actual data from the image + pub image_data: Vec, + /// Decompression filter for `image_data`, if `None` assumes uncompressed raw pixels in the expected color format. + pub image_filter: Option, + // SoftMask for transparency, if `None` assumes no transparency. See page 444 of the adope pdf 1.4 reference + pub smask: Option, + /* /BBox << dictionary >> */ + /* todo: find out if this is really required */ + /// Required bounds to clip the image, in unit space + /// Default value: Identity matrix (`[1 0 0 1 0 0]`) - used when value is `None` + pub clipping_bbox: Option, +} + +/// Describes the format the image bytes are compressed with. +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum ImageFilter { + /// ??? + Ascii85, + /// Lempel Ziv Welch compression, i.e. zip + Lzw, + /// Discrete Cosinus Transform, JPEG Baseline. + DCT, + /// JPEG2000 aka JPX wavelet based compression. + JPX, +} + + +/// __THIS IS NOT A PDF FORM!__ A form `XObject` can be nearly everything. +/// PDF allows you to reuse content for the graphics stream in a `FormXObject`. +/// A `FormXObject` is basically a layer-like content stream and can contain anything +/// as long as it's a valid strem. A `FormXObject` is intended to be used for reapeated +/// content on one page. +#[derive(Debug, PartialEq, Clone)] +pub struct FormXObject { + /* /Type /XObject */ + /* /Subtype /Form */ + + /* /FormType Integer */ + /// Form type (currently only Type1) + pub form_type: FormType, + /// The actual content of this FormXObject + pub bytes: Vec, + /* /Matrix [Integer , 6] */ + /// Optional matrix, maps the form into user space + pub matrix: Option, + /* /Resources << dictionary >> */ + /// (Optional but strongly recommended; PDF 1.2) A dictionary specifying + /// any resources (such as fonts and images) required by the form XObject + /// (see Section 3.7, “Content Streams and Resources”). + /// + /// In PDF 1.1 and earlier, all named resources used in the form XObject must be + /// included in the resource dictionary of each page object on which the form + /// XObject appears, regardless of whether they also appear in the resource + /// dictionary of the form XObject. It can be useful to specify these resources + /// in the form XObject’s resource dictionary as well, to determine which resources + /// are used inside the form XObject. If a resource is included in both dictionaries, + /// it should have the same name in both locations. + /// /// In PDF 1.2 and later versions, form XObjects can be independent of the content + /// streams in which they appear, and this is strongly recommended although not + /// required. In an independent form XObject, the resource dictionary of the form + /// XObject is required and contains all named resources used by the form XObject. + /// These resources are not promoted to the outer content stream’s resource + /// dictionary, although that stream’s resource dictionary refers to the form XObject. + pub resources: Option, + /* /Group << dictionary >> */ + /// (Optional; PDF 1.4) A group attributes dictionary indicating that the contents of the + /// form XObject are to be treated as a group and specifying the attributes of that group + /// (see Section 4.9.2, “Group XObjects”). + /// + /// Note: If a Ref entry (see below) is present, the group attributes also apply to the + /// external page imported by that entry, which allows such an imported page to be treated + /// as a group without further modification. + pub group: Option, + /* /Ref << dictionary >> */ + /// (Optional; PDF 1.4) A reference dictionary identifying a page to be imported from another + /// PDF file, and for which the form XObject serves as a proxy (see Section 4.9.3, “Reference XObjects”). + pub ref_dict: Option, + /* /Metadata [stream] */ + /// (Optional; PDF 1.4) A metadata stream containing metadata for the form XObject + /// (see Section 10.2.2, “Metadata Streams”). + pub metadata: Option, + /* /PieceInfo << dictionary >> */ + /// (Optional; PDF 1.3) A page-piece dictionary associated with the form XObject + /// (see Section 10.4, “Page-Piece Dictionaries”). + pub piece_info: Option, + /* /LastModified (date) */ + /// (Required if PieceInfo is present; optional otherwise; PDF 1.3) The date and time + /// (see Section 3.8.3, “Dates”) when the form XObject’s contents were most recently + /// modified. If a page-piece dictionary (PieceInfo) is present, the modification date + /// is used to ascertain which of the application data dictionaries it contains correspond + /// to the current content of the form (see Section 10.4, “Page-Piece Dictionaries”). + pub last_modified: Option, + /* /StructParent integer */ + /// (Required if the form XObject is a structural content item; PDF 1.3) The integer key of + /// the form XObject’s entry in the structural parent tree (see “Finding Structure Elements + /// from Content Items” on page 868). + pub struct_parent: Option, + /* /StructParents integer */ + /// __(Required if the form XObject contains marked-content sequences that are structural content + /// items; PDF 1.3)__ The integer key of the form XObject’s entry in the structural parent tree + /// (see “Finding Structure Elements from Content Items” on page 868). + /// + /// __Note:__ At most one of the entries StructParent or StructParents may be present. A form + /// XObject can be either a content item in its entirety or a container for marked-content sequences + /// that are content items, but not both. + pub struct_parents: Option, + /* /OPI << dictionary >> */ + /// (Optional; PDF 1.2) An OPI version dictionary for the form XObject + /// (see Section 10.10.6, “Open Prepress Interface (OPI)”). + pub opi: Option, + /// (Optional; PDF 1.5) An optional content group or optional content membership dictionary + /// (see Section 4.10, “Optional Content”) specifying the optional content properties for + /// the form XObject. Before the form is processed, its visibility is determined based on + /// this entry. If it is determined to be invisible, the entire form is skipped, as if there + /// were no Do operator to invoke it. + pub oc: Option, + /* /Name /MyName */ + /// __(Required in PDF 1.0; optional otherwise)__ The name by which this form XObject is referenced + /// in the XObject subdictionary of the current resource dictionary + /// (see Section 3.7.2, “Resource Dictionaries”). + /// __Note:__ This entry is obsolescent and its use is no longer recommended. + /// (See implementation note 55 in Appendix H.) + pub name: Option, +} + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum FormType { + /// The only form type ever declared by Adobe + /* Integer(1) */ + Type1, +} + + +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct GroupXObject { + /* /Type /Group */ + /* /S /Transparency */ /* currently the only valid GroupXObject */ +} + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum GroupXObjectType { + /// Transparency group XObject + TransparencyGroup, +} + +/// PDF 1.4 and higher +/// Contains a PDF file to be embedded in the current PDF +#[derive(Debug, PartialEq, Clone)] +pub struct ReferenceXObject { + /// (Required) The file containing the target document. (?) + pub file: Vec, + /// Page number to embed + pub page: i64, + /// Optional, should be the document ID and version ID from the metadata + pub id: [i64; 2], +} + +/// TODO, very low priority +#[derive(Debug, PartialEq, Clone)] +pub struct PostScriptXObject { + /// __(Optional)__ A stream whose contents are to be used in + /// place of the PostScript XObject’s stream when the target + /// PostScript interpreter is known to support only LanguageLevel 1 + #[allow(dead_code)] + level1: Option>, +} +