use std::collections::BTreeMap;
use serde_derive::{Deserialize, Serialize};
pub mod annotation;
pub use annotation::*;
pub mod cmap;
pub use cmap::*;
pub mod text;
pub use text::*;
pub mod wasm;
pub use wasm::*;
pub mod conformance;
pub use conformance::*;
pub mod matrix;
pub use matrix::*;
pub mod units;
pub use units::*;
pub mod date;
pub use date::*;
pub mod font;
pub use font::*;
pub mod shape;
pub use shape::*;
pub mod graphics;
pub use graphics::*;
pub mod ops;
pub use ops::*;
pub mod color;
pub use color::*;
pub mod xobject;
pub use xobject::*;
pub mod image_types;
pub use image_types::*;
#[cfg(feature = "svg")]
pub mod svg;
#[cfg(feature = "svg")]
pub use svg::*;
#[cfg(feature = "images")]
pub mod image;
#[cfg(feature = "images")]
pub use image::*;
#[cfg(feature = "html")]
pub mod html;
#[cfg(feature = "html")]
pub use html::*;
#[cfg(feature = "text_layout")]
pub mod text_shaping {
pub use azul_layout::text3::cache::{FontManager, UnifiedLayout, ParsedFontTrait, LoadedFonts};
pub use azul_layout::text3::glyphs::{get_glyph_runs_pdf, PdfGlyphRun, PdfPositionedGlyph};
pub use azul_css::props::basic::ColorU;
pub use crate::shape::{layout_to_ops, layout_to_ops_with_offset};
#[cfg(feature = "html")]
pub use crate::html::bridge::render_unified_layout_public;
}
#[cfg(feature = "html")]
pub mod components;
#[cfg(feature = "html")]
pub use components::*;
pub mod utils;
use utils::*;
pub mod serialize;
pub use serialize::*;
pub mod deserialize;
pub use deserialize::*;
pub(crate) mod render;
pub use render::*;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct GeneratePdfOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub font_embedding: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_width: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_height: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub margin_top: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub margin_right: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub margin_bottom: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub margin_left: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_optimization: Option<ImageOptimizationOptions>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_page_numbers: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub header_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub footer_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skip_first_page: Option<bool>,
}
impl Default for GeneratePdfOptions {
fn default() -> Self {
Self {
font_embedding: Some(true),
page_width: Some(210.0),
page_height: Some(297.0),
margin_top: None,
margin_right: None,
margin_bottom: None,
margin_left: None,
image_optimization: None,
show_page_numbers: None,
header_text: None,
footer_text: None,
skip_first_page: None,
}
}
}
impl GeneratePdfOptions {
fn is_default(&self) -> bool {
*self == Self::default()
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(untagged)]
pub enum Base64OrRaw {
B64(String),
Raw(Vec<u8>),
}
impl Default for Base64OrRaw {
fn default() -> Self {
Base64OrRaw::Raw(Vec::new())
}
}
impl Base64OrRaw {
pub fn decode_bytes(&self) -> Result<Vec<u8>, String> {
use base64::Engine;
match self {
Base64OrRaw::B64(r) => base64::prelude::BASE64_STANDARD
.decode(get_base64_substr(r))
.map_err(|e| e.to_string()),
Base64OrRaw::Raw(r) => Ok(r.clone()),
}
}
}
fn get_base64_substr(input: &str) -> &str {
if input.starts_with("data:") {
if let Some(comma_index) = input.find(',') {
let metadata = &input[..comma_index];
if metadata.contains("base64") {
&input[comma_index + 1..]
} else {
input
}
} else {
input
}
} else {
input
}
}
#[derive(Debug, PartialEq, Clone, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PageAnnotId(pub String);
impl PageAnnotId {
pub fn new() -> Self {
Self(crate::utils::random_character_string_32())
}
}
#[derive(Debug, PartialEq, Clone, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct XObjectId(pub String);
impl XObjectId {
pub fn new() -> Self {
Self(crate::utils::random_character_string_32())
}
}
#[derive(Debug, PartialEq, Hash, Clone, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct FontId(pub String);
impl FontId {
pub fn new() -> Self {
Self(crate::utils::random_character_string_32())
}
}
#[derive(Debug, PartialEq, Clone, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct LayerInternalId(pub String);
impl LayerInternalId {
pub fn new() -> Self {
Self(crate::utils::random_character_string_32())
}
}
#[derive(Debug, PartialEq, Clone, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ExtendedGraphicsStateId(pub String);
impl ExtendedGraphicsStateId {
pub fn new() -> Self {
Self(crate::utils::random_character_string_32())
}
}
#[derive(Debug, PartialEq, Clone, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct IccProfileId(pub String);
impl IccProfileId {
pub fn new() -> Self {
Self(crate::utils::random_character_string_32())
}
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PdfDocument {
pub metadata: PdfMetadata,
pub resources: PdfResources,
pub bookmarks: PageAnnotMap,
pub pages: Vec<PdfPage>,
}
impl PdfDocument {
pub fn new(name: &str) -> Self {
Self {
metadata: PdfMetadata {
info: PdfDocumentInfo {
document_title: name.to_string(),
..Default::default()
},
xmp: None,
},
resources: PdfResources::default(),
bookmarks: PageAnnotMap::default(),
pages: Vec::new(),
}
}
pub fn parse(
bytes: &[u8],
opts: &PdfParseOptions,
warnings: &mut Vec<PdfWarnMsg>,
) -> Result<Self, String> {
self::deserialize::parse_pdf_from_bytes(bytes, opts, warnings)
}
pub async fn parse_async(
bytes: &[u8],
opts: &PdfParseOptions,
warnings: &mut Vec<PdfWarnMsg>,
) -> Result<Self, String> {
self::deserialize::parse_pdf_from_bytes_async(bytes, opts, warnings).await
}
pub fn add_graphics_state(&mut self, gs: ExtendedGraphicsState) -> ExtendedGraphicsStateId {
let id = ExtendedGraphicsStateId::new();
self.resources.extgstates.map.insert(id.clone(), gs);
id
}
pub fn add_layer(&mut self, layer: &Layer) -> LayerInternalId {
let id = LayerInternalId::new();
self.resources.layers.map.insert(id.clone(), layer.clone());
id
}
pub fn add_font(&mut self, font: &ParsedFont) -> FontId {
let id = FontId::new();
let pdf_font = crate::font::PdfFont::new(font.clone());
self.resources.fonts.map.insert(id.clone(), pdf_font);
id
}
pub fn extract_text(&self) -> Vec<Vec<String>> {
self.pages
.iter()
.map(|page| page.extract_text(&self.resources))
.collect()
}
pub fn add_image(&mut self, image: &RawImage) -> XObjectId {
let id = XObjectId::new();
self.resources
.xobjects
.map
.insert(id.clone(), XObject::Image(image.clone()));
id
}
pub fn add_xobject(&mut self, parsed_svg: &ExternalXObject) -> XObjectId {
let id = XObjectId::new();
self.resources
.xobjects
.map
.insert(id.clone(), XObject::External(parsed_svg.clone()));
id
}
pub fn add_bookmark(&mut self, name: &str, page: usize) -> PageAnnotId {
let id = PageAnnotId::new();
self.bookmarks.map.insert(
id.clone(),
PageAnnotation {
name: name.to_string(),
page,
},
);
id
}
#[cfg(feature = "html")]
pub fn from_html(
html: &str,
images: &BTreeMap<String, Base64OrRaw>,
fonts: &BTreeMap<String, Base64OrRaw>,
options: &GeneratePdfOptions,
warnings: &mut Vec<PdfWarnMsg>,
) -> Result<Self, String> {
Self::from_html_with_cache(html, images, fonts, options, warnings, None)
}
#[cfg(feature = "html")]
pub fn from_html_with_cache(
html: &str,
images: &BTreeMap<String, Base64OrRaw>,
fonts: &BTreeMap<String, Base64OrRaw>,
options: &GeneratePdfOptions,
warnings: &mut Vec<PdfWarnMsg>,
font_pool: Option<crate::html::SharedFontPool>,
) -> Result<Self, String> {
use crate::html::{XmlRenderOptions, PageMargins};
use base64::{engine::general_purpose::STANDARD, Engine as _};
let mut pdf = Self::new("PDF Document");
let mut xml_options = XmlRenderOptions::default();
xml_options.page_width = Mm(options.page_width.unwrap_or(210.0));
xml_options.page_height = Mm(options.page_height.unwrap_or(297.0));
xml_options.margins = PageMargins {
top: Mm(options.margin_top.unwrap_or(0.0)),
right: Mm(options.margin_right.unwrap_or(0.0)),
bottom: Mm(options.margin_bottom.unwrap_or(0.0)),
left: Mm(options.margin_left.unwrap_or(0.0)),
};
xml_options.show_page_numbers = options.show_page_numbers.unwrap_or(false);
xml_options.header_text = options.header_text.clone();
xml_options.footer_text = options.footer_text.clone();
xml_options.skip_first_page = options.skip_first_page.unwrap_or(false);
for (key, img) in images {
let bytes = match img {
Base64OrRaw::Raw(b) => b.clone(),
Base64OrRaw::B64(s) => STANDARD.decode(s).map_err(|e| format!("Base64 decode error: {}", e))?,
};
xml_options.images.insert(key.clone(), bytes);
}
for (key, font) in fonts {
let bytes = match font {
Base64OrRaw::Raw(b) => b.clone(),
Base64OrRaw::B64(s) => STANDARD.decode(s).map_err(|e| format!("Base64 decode error: {}", e))?,
};
xml_options.fonts.insert(key.clone(), bytes);
}
xml_options.font_pool = font_pool;
match crate::html::xml_to_pdf_pages(html, &xml_options) {
Ok((pages, font_data)) => {
for (font_hash, parsed_font) in font_data.iter() {
let font_id = FontId(format!("F{}", font_hash.font_hash));
let pdf_font = crate::font::PdfFont::new(parsed_font.clone());
pdf.resources.fonts.map.insert(font_id, pdf_font);
}
pdf.pages.extend(pages);
Ok(pdf)
}
Err(errs) => {
warnings.extend(errs);
Ok(pdf)
}
}
}
#[cfg(feature = "html")]
pub fn from_html_debug(
html: &str,
images: &BTreeMap<String, Base64OrRaw>,
fonts: &BTreeMap<String, Base64OrRaw>,
options: &GeneratePdfOptions,
warnings: &mut Vec<PdfWarnMsg>,
) -> Result<(Self, crate::html::PdfDebugInfo), String> {
Self::from_html_debug_with_cache(html, images, fonts, options, warnings, None)
}
#[cfg(feature = "html")]
pub fn from_html_debug_with_cache(
html: &str,
images: &BTreeMap<String, Base64OrRaw>,
fonts: &BTreeMap<String, Base64OrRaw>,
options: &GeneratePdfOptions,
warnings: &mut Vec<PdfWarnMsg>,
font_pool: Option<crate::html::SharedFontPool>,
) -> Result<(Self, crate::html::PdfDebugInfo), String> {
use crate::html::{XmlRenderOptions, PageMargins};
use base64::{engine::general_purpose::STANDARD, Engine as _};
let mut pdf = Self::new("PDF Document");
let mut xml_options = XmlRenderOptions::default();
xml_options.page_width = Mm(options.page_width.unwrap_or(210.0));
xml_options.page_height = Mm(options.page_height.unwrap_or(297.0));
xml_options.margins = PageMargins {
top: Mm(options.margin_top.unwrap_or(0.0)),
right: Mm(options.margin_right.unwrap_or(0.0)),
bottom: Mm(options.margin_bottom.unwrap_or(0.0)),
left: Mm(options.margin_left.unwrap_or(0.0)),
};
xml_options.show_page_numbers = options.show_page_numbers.unwrap_or(false);
xml_options.header_text = options.header_text.clone();
xml_options.footer_text = options.footer_text.clone();
xml_options.skip_first_page = options.skip_first_page.unwrap_or(false);
for (key, img) in images {
let bytes = match img {
Base64OrRaw::Raw(b) => b.clone(),
Base64OrRaw::B64(s) => STANDARD.decode(s).map_err(|e| format!("Base64 decode error: {}", e))?,
};
xml_options.images.insert(key.clone(), bytes);
}
for (key, font) in fonts {
let bytes = match font {
Base64OrRaw::Raw(b) => b.clone(),
Base64OrRaw::B64(s) => STANDARD.decode(s).map_err(|e| format!("Base64 decode error: {}", e))?,
};
xml_options.fonts.insert(key.clone(), bytes);
}
xml_options.font_pool = font_pool;
match crate::html::xml_to_pdf_pages_debug(html, &xml_options) {
Ok((pages, font_data, debug_info)) => {
for (font_hash, parsed_font) in font_data.iter() {
let font_id = FontId(format!("F{}", font_hash.font_hash));
let pdf_font = crate::font::PdfFont::new(parsed_font.clone());
pdf.resources.fonts.map.insert(font_id, pdf_font);
}
pdf.pages.extend(pages);
Ok((pdf, debug_info))
}
Err(errs) => {
warnings.extend(errs);
Ok((pdf, crate::html::PdfDebugInfo {
display_list_debug: Vec::new(),
pdf_ops_debug: Vec::new(),
}))
}
}
}
pub fn page_to_svg(
&self,
page: usize,
opts: &PdfToSvgOptions,
warnings: &mut Vec<PdfWarnMsg>,
) -> Option<String> {
Some(
self.pages
.get(page.saturating_sub(1))?
.to_svg(&self.resources, opts, warnings),
)
}
pub async fn page_to_svg_async(
&self,
page: usize,
opts: &PdfToSvgOptions,
warnings: &mut Vec<PdfWarnMsg>,
) -> Option<String> {
Some(
self.pages
.get(page.saturating_sub(1))?
.to_svg_async(&self.resources, opts, warnings)
.await,
)
}
pub fn with_pages(&mut self, pages: Vec<PdfPage>) -> &mut Self {
let mut pages = pages;
self.pages.append(&mut pages);
self
}
pub fn append_document(&mut self, other: PdfDocument) {
let page_offset = self.pages.len();
self.merge_resources(other.resources);
self.merge_bookmarks(other.bookmarks, page_offset);
self.pages.extend(other.pages);
}
pub fn insert_document(&mut self, insert_at: usize, other: PdfDocument) {
let insert_at = insert_at.min(self.pages.len());
let pages_to_insert = other.pages.len();
for (_, annot) in self.bookmarks.map.iter_mut() {
if annot.page >= insert_at {
annot.page += pages_to_insert;
}
}
self.merge_resources(other.resources);
self.merge_bookmarks(other.bookmarks, insert_at);
let mut new_pages = Vec::with_capacity(self.pages.len() + pages_to_insert);
new_pages.extend(self.pages.drain(..insert_at));
new_pages.extend(other.pages);
new_pages.extend(self.pages.drain(..));
self.pages = new_pages;
}
fn merge_resources(&mut self, other: PdfResources) {
for (id, font) in other.fonts.map {
self.resources.fonts.map.entry(id).or_insert(font);
}
for (id, xobj) in other.xobjects.map {
self.resources.xobjects.map.entry(id).or_insert(xobj);
}
for (id, gs) in other.extgstates.map {
self.resources.extgstates.map.entry(id).or_insert(gs);
}
for (id, layer) in other.layers.map {
self.resources.layers.map.entry(id).or_insert(layer);
}
}
fn merge_bookmarks(&mut self, other: PageAnnotMap, page_offset: usize) {
for (id, mut annot) in other.map {
annot.page += page_offset;
self.bookmarks.map.insert(id, annot);
}
}
pub fn page_count(&self) -> usize {
self.pages.len()
}
pub fn save_writer<W: std::io::Write>(
&self,
w: &mut W,
opts: &PdfSaveOptions,
warnings: &mut Vec<PdfWarnMsg>,
) {
self::serialize::serialize_pdf(self, opts, w, warnings);
}
pub fn save(&self, opts: &PdfSaveOptions, warnings: &mut Vec<PdfWarnMsg>) -> Vec<u8> {
self::serialize::serialize_pdf_into_bytes(self, opts, warnings)
}
pub fn to_lopdf_document(
&self,
opts: &PdfSaveOptions,
warnings: &mut Vec<PdfWarnMsg>,
) -> lopdf::Document {
self::serialize::to_lopdf_doc(self, opts, warnings)
}
pub async fn save_async(
&self,
opts: &PdfSaveOptions,
warnings: &mut Vec<PdfWarnMsg>,
) -> Vec<u8> {
self::serialize::serialize_pdf_into_bytes(self, opts, warnings)
}
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PdfResources {
#[serde(default)]
pub fonts: PdfFontMap,
#[serde(default)]
pub xobjects: XObjectMap,
#[serde(default)]
pub extgstates: ExtendedGraphicsStateMap,
#[serde(default)]
pub layers: PdfLayerMap,
}
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PdfLayerMap {
pub map: BTreeMap<LayerInternalId, Layer>,
}
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PdfFontMap {
pub map: BTreeMap<FontId, crate::font::PdfFont>,
}
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct XObjectMap {
pub map: BTreeMap<XObjectId, XObject>,
}
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PageAnnotMap {
pub map: BTreeMap<PageAnnotId, PageAnnotation>,
}
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ExtendedGraphicsStateMap {
pub map: BTreeMap<ExtendedGraphicsStateId, ExtendedGraphicsState>,
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PdfMetadata {
#[serde(default)]
pub info: PdfDocumentInfo,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub xmp: Option<XmpMetadata>,
}
impl PdfMetadata {
pub(crate) fn xmp_metadata_string(&self) -> String {
let trapping = if self.info.trapped { "True" } else { "False" };
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!("./res/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
)
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct XmpMetadata {
#[serde(default)]
pub rendition_class: Option<String>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PdfDocumentInfo {
pub trapped: bool,
pub version: u32,
pub creation_date: OffsetDateTime,
pub modification_date: OffsetDateTime,
pub metadata_date: OffsetDateTime,
pub conformance: PdfConformance,
pub document_title: String,
pub author: String,
pub creator: String,
pub producer: String,
pub keywords: Vec<String>,
pub subject: String,
pub identifier: String,
}
impl Default for PdfDocumentInfo {
fn default() -> Self {
Self {
trapped: false,
version: 1,
creation_date: OffsetDateTime::from_unix_timestamp(0).unwrap(),
modification_date: OffsetDateTime::from_unix_timestamp(0).unwrap(),
metadata_date: OffsetDateTime::from_unix_timestamp(0).unwrap(),
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(),
}
}
}