use crate::editor::{DocumentEditor, EditableDocument, PdfPage};
use crate::error::{Error, Result};
use crate::writer::{DocumentBuilder, DocumentMetadata, PageSize};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GfmAlign {
Left,
Center,
Right,
}
#[derive(Debug)]
struct GfmTable {
headers: Vec<String>,
rows: Vec<Vec<String>>,
alignments: Vec<GfmAlign>,
}
impl GfmTable {
fn parse(lines: &[&str]) -> Option<Self> {
if lines.len() < 2 {
return None;
}
let headers = Self::parse_row(lines[0])?;
if headers.is_empty() {
return None;
}
let alignments = Self::parse_separator(lines[1], headers.len())?;
let mut rows = Vec::new();
for line in &lines[2..] {
if let Some(row) = Self::parse_row(line) {
let mut padded_row = row;
padded_row.resize(headers.len(), String::new());
rows.push(padded_row);
}
}
Some(Self {
headers,
rows,
alignments,
})
}
fn parse_row(line: &str) -> Option<Vec<String>> {
let trimmed = line.trim();
if !trimmed.contains('|') {
return None;
}
let content = trimmed.trim_start_matches('|').trim_end_matches('|');
let cells: Vec<String> = content.split('|').map(|s| s.trim().to_string()).collect();
if cells.is_empty() { None } else { Some(cells) }
}
fn parse_separator(line: &str, expected_cols: usize) -> Option<Vec<GfmAlign>> {
let trimmed = line.trim();
if !trimmed.contains('|') || !trimmed.contains('-') {
return None;
}
let content = trimmed.trim_start_matches('|').trim_end_matches('|');
let parts: Vec<&str> = content.split('|').map(|s| s.trim()).collect();
if parts.iter().any(|p| !Self::is_separator_cell(p)) {
return None;
}
let mut alignments: Vec<GfmAlign> = parts
.iter()
.map(|p| {
let has_left_colon = p.starts_with(':');
let has_right_colon = p.ends_with(':');
match (has_left_colon, has_right_colon) {
(true, true) => GfmAlign::Center,
(false, true) => GfmAlign::Right,
_ => GfmAlign::Left,
}
})
.collect();
alignments.resize(expected_cols, GfmAlign::Left);
Some(alignments)
}
fn is_separator_cell(s: &str) -> bool {
if s.is_empty() {
return false;
}
let stripped = s.trim_start_matches(':').trim_end_matches(':');
!stripped.is_empty() && stripped.chars().all(|c| c == '-')
}
fn column_widths(&self) -> Vec<usize> {
let mut widths: Vec<usize> = self.headers.iter().map(|h| h.len()).collect();
for row in &self.rows {
for (i, cell) in row.iter().enumerate() {
if i < widths.len() {
widths[i] = widths[i].max(cell.len());
}
}
}
widths.iter().map(|w| (*w).max(3)).collect()
}
fn render(&self) -> Vec<String> {
let widths = self.column_widths();
let mut lines = Vec::new();
lines.push(self.render_row(&self.headers, &widths, &self.alignments));
lines.push(self.render_separator(&widths, &self.alignments));
for row in &self.rows {
lines.push(self.render_row(row, &widths, &self.alignments));
}
lines
}
fn render_row(&self, cells: &[String], widths: &[usize], alignments: &[GfmAlign]) -> String {
let mut parts = Vec::new();
for (i, cell) in cells.iter().enumerate() {
let width = widths.get(i).copied().unwrap_or(3);
let align = alignments.get(i).copied().unwrap_or(GfmAlign::Left);
let formatted = match align {
GfmAlign::Left => format!("{:<width$}", cell, width = width),
GfmAlign::Center => format!("{:^width$}", cell, width = width),
GfmAlign::Right => format!("{:>width$}", cell, width = width),
};
parts.push(formatted);
}
format!("| {} |", parts.join(" | "))
}
fn render_separator(&self, widths: &[usize], alignments: &[GfmAlign]) -> String {
let mut parts = Vec::new();
for (i, width) in widths.iter().enumerate() {
let align = alignments.get(i).copied().unwrap_or(GfmAlign::Left);
let dashes = "-".repeat(*width);
let sep = match align {
GfmAlign::Left => format!(":{}", dashes),
GfmAlign::Center => format!(":{}:", &dashes[..dashes.len().saturating_sub(1)]),
GfmAlign::Right => format!("{}:", dashes),
};
parts.push(sep);
}
format!("|{}|", parts.join("|"))
}
}
fn is_table_line(line: &str) -> bool {
let trimmed = line.trim();
trimmed.starts_with('|') && trimmed.ends_with('|') && trimmed.len() > 2
}
struct InlineRun {
text: String,
bold: bool,
italic: bool,
code: bool,
}
fn contains_subslice(chars: &[char], from: usize, pat: &[char]) -> bool {
if pat.is_empty() || from >= chars.len() || chars.len() - from < pat.len() {
return false;
}
(from..=chars.len() - pat.len()).any(|k| chars[k..k + pat.len()] == *pat)
}
fn parse_inline_runs(s: &str) -> Vec<InlineRun> {
fn flush(runs: &mut Vec<InlineRun>, buf: &mut String, bold: bool, italic: bool, code: bool) {
if !buf.is_empty() {
runs.push(InlineRun {
text: std::mem::take(buf),
bold,
italic,
code,
});
}
}
let chars: Vec<char> = s.chars().collect();
let mut runs: Vec<InlineRun> = Vec::new();
let mut buf = String::new();
let (mut bold, mut italic, mut code) = (false, false, false);
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '`' {
if code {
flush(&mut runs, &mut buf, bold, italic, code);
code = false;
i += 1;
continue;
}
if contains_subslice(&chars, i + 1, &['`']) {
flush(&mut runs, &mut buf, bold, italic, code);
code = true;
i += 1;
continue;
}
} else if c == '*' && !code {
let dbl = i + 1 < chars.len() && chars[i + 1] == '*';
if dbl {
if bold || contains_subslice(&chars, i + 2, &['*', '*']) {
flush(&mut runs, &mut buf, bold, italic, code);
bold = !bold;
i += 2;
continue;
}
} else if italic || contains_subslice(&chars, i + 1, &['*']) {
flush(&mut runs, &mut buf, bold, italic, code);
italic = !italic;
i += 1;
continue;
}
}
buf.push(c);
i += 1;
}
flush(&mut runs, &mut buf, bold, italic, code);
runs
}
#[derive(Debug, Clone)]
pub struct PdfConfig {
pub title: Option<String>,
pub author: Option<String>,
pub subject: Option<String>,
pub keywords: Option<String>,
pub page_size: PageSize,
pub margin_left: f32,
pub margin_right: f32,
pub margin_top: f32,
pub margin_bottom: f32,
pub font_size: f32,
pub line_height: f32,
pub template: Option<crate::writer::PageTemplate>,
}
impl Default for PdfConfig {
fn default() -> Self {
Self {
title: None,
author: None,
subject: None,
keywords: None,
page_size: PageSize::Letter,
margin_left: 72.0,
margin_right: 72.0,
margin_top: 72.0,
margin_bottom: 72.0,
font_size: 12.0,
line_height: 1.5,
template: None,
}
}
}
pub struct Pdf {
bytes: Vec<u8>,
config: PdfConfig,
editor: Option<DocumentEditor>,
source_path: Option<PathBuf>,
}
impl Pdf {
pub fn new() -> Self {
Self {
bytes: Vec::new(),
config: PdfConfig::default(),
editor: None,
source_path: None,
}
}
pub fn from_markdown(content: &str) -> Result<Self> {
PdfBuilder::new().from_markdown(content)
}
pub fn from_markdown_with_fonts(content: &str, fonts: &[(String, Vec<u8>)]) -> Result<Self> {
PdfBuilder::new().from_markdown_with_fonts(content, fonts)
}
pub fn from_html(content: &str) -> Result<Self> {
PdfBuilder::new().from_html(content)
}
pub fn from_text(content: &str) -> Result<Self> {
PdfBuilder::new().from_text(content)
}
pub fn from_image(path: impl AsRef<Path>) -> Result<Self> {
PdfBuilder::new().from_image(path)
}
pub fn from_image_bytes(data: &[u8]) -> Result<Self> {
PdfBuilder::new().from_image_bytes(data)
}
pub fn from_images<P: AsRef<Path>>(paths: &[P]) -> Result<Self> {
PdfBuilder::new().from_images(paths)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let source_path = path.as_ref().to_path_buf();
let editor = DocumentEditor::open(&path)?;
Ok(Self {
bytes: Vec::new(),
config: PdfConfig::default(),
editor: Some(editor),
source_path: Some(source_path),
})
}
pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
let editor = DocumentEditor::from_bytes(data)?;
Ok(Self {
bytes: Vec::new(),
config: PdfConfig::default(),
editor: Some(editor),
source_path: None,
})
}
#[cfg(not(target_arch = "wasm32"))]
pub fn open_editor(path: impl AsRef<Path>) -> Result<DocumentEditor> {
DocumentEditor::open(path)
}
pub fn page_count(&mut self) -> Result<usize> {
if let Some(ref mut editor) = self.editor {
editor.page_count()
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn page(&mut self, index: usize) -> Result<PdfPage> {
if let Some(ref mut editor) = self.editor {
editor.get_page(index)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn save_page(&mut self, page: PdfPage) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.save_page(page)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn is_modified(&self) -> bool {
if let Some(ref editor) = self.editor {
editor.is_modified()
} else {
false
}
}
pub fn set_title(&mut self, title: impl Into<String>) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.set_title(title);
Ok(())
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn set_author(&mut self, author: impl Into<String>) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.set_author(author);
Ok(())
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn set_subject(&mut self, subject: impl Into<String>) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.set_subject(subject);
Ok(())
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn set_keywords(&mut self, keywords: impl Into<String>) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.set_keywords(keywords);
Ok(())
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn to_text(&mut self, page: usize) -> Result<String> {
if let Some(ref mut editor) = self.editor {
editor.source_mut().extract_text(page)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn extract_spans(&mut self, page: usize) -> Result<Vec<crate::layout::TextSpan>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor.source_mut().extract_spans(page)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_chars(&mut self, page: usize) -> Result<Vec<crate::layout::TextChar>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor.source_mut().extract_chars(page)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_words(&mut self, page: usize) -> Result<Vec<crate::layout::Word>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor.source_mut().extract_words(page)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_text_lines(&mut self, page: usize) -> Result<Vec<crate::layout::TextLine>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor.source_mut().extract_text_lines(page)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_rects(&mut self, page: usize) -> Result<Vec<crate::elements::PathContent>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor.source_mut().extract_rects(page)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_lines(&mut self, page: usize) -> Result<Vec<crate::elements::PathContent>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor.source_mut().extract_lines(page)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_tables(
&mut self,
page: usize,
) -> Result<Vec<crate::structure::table_extractor::Table>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor.source_mut().extract_tables(page)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_tables_with_config(
&mut self,
page: usize,
config: crate::structure::spatial_table_detector::TableDetectionConfig,
) -> Result<Vec<crate::structure::table_extractor::Table>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor.source_mut().extract_tables_with_config(page, config)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_text_in_rect(
&mut self,
page: usize,
region: crate::geometry::Rect,
mode: crate::layout::RectFilterMode,
) -> Result<String> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor.source_mut().extract_text_in_rect(page, region, mode)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_words_in_rect(
&mut self,
page: usize,
region: crate::geometry::Rect,
mode: crate::layout::RectFilterMode,
) -> Result<Vec<crate::layout::Word>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor
.source_mut()
.extract_words_in_rect(page, region, mode)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_text_lines_in_rect(
&mut self,
page: usize,
region: crate::geometry::Rect,
mode: crate::layout::RectFilterMode,
) -> Result<Vec<crate::layout::TextLine>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor
.source_mut()
.extract_text_lines_in_rect(page, region, mode)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_chars_in_rect(
&mut self,
page: usize,
region: crate::geometry::Rect,
mode: crate::layout::RectFilterMode,
) -> Result<Vec<crate::layout::TextChar>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor
.source_mut()
.extract_chars_in_rect(page, region, mode)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_images_in_rect(
&mut self,
page: usize,
region: crate::geometry::Rect,
) -> Result<Vec<crate::extractors::PdfImage>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor.source_mut().extract_images_in_rect(page, region)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_tables_in_rect(
&mut self,
page: usize,
region: crate::geometry::Rect,
) -> Result<Vec<crate::structure::table_extractor::Table>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor.source_mut().extract_tables_in_rect(page, region)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn extract_images(&mut self, page: usize) -> Result<Vec<crate::extractors::PdfImage>> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
editor.source_mut().extract_images(page)
} else {
Err(Error::InvalidOperation("No document loaded".to_string()))
}
}
pub fn within(
&mut self,
page_index: usize,
region: crate::geometry::Rect,
) -> PdfPageRegion<'_> {
PdfPageRegion {
pdf: self,
page_index,
region,
mode: crate::layout::RectFilterMode::Intersects,
}
}
fn ensure_editor(&mut self) -> Result<()> {
if self.editor.is_none() && !self.bytes.is_empty() {
let editor = DocumentEditor::from_bytes(self.bytes.clone())?;
self.editor = Some(editor);
}
Ok(())
}
pub fn render_page(
&mut self,
page_index: usize,
options: Option<&crate::rendering::RenderOptions>,
) -> Result<crate::rendering::RenderedImage> {
let default_opts;
let opts = match options {
Some(o) => o,
None => {
default_opts = crate::rendering::RenderOptions::default();
&default_opts
},
};
self.render_page_with_options(page_index, opts)
}
pub fn render_page_with_options(
&mut self,
page_index: usize,
options: &crate::rendering::RenderOptions,
) -> Result<crate::rendering::RenderedImage> {
self.ensure_editor()?;
if let Some(ref mut editor) = self.editor {
crate::rendering::render_page(editor.source(), page_index, options)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() or create one from content.".to_string(),
))
}
}
pub fn render_page_to_file(&mut self, page: usize, path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref();
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("png")
.to_lowercase();
let mut options = crate::rendering::RenderOptions::default();
if ext == "jpg" || ext == "jpeg" {
options.format = crate::rendering::ImageFormat::Jpeg;
}
let image = self.render_page_with_options(page, &options)?;
image.save(path)
}
pub fn render_page_to_file_with_dpi(
&mut self,
page: usize,
path: impl AsRef<Path>,
dpi: u32,
) -> Result<()> {
let path = path.as_ref();
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("png")
.to_lowercase();
let mut options = crate::rendering::RenderOptions::with_dpi(dpi);
if ext == "jpg" || ext == "jpeg" {
options.format = crate::rendering::ImageFormat::Jpeg;
}
let image = self.render_page_with_options(page, &options)?;
image.save(path)
}
pub fn search(&mut self, pattern: &str) -> Result<Vec<crate::search::SearchResult>> {
use crate::search::{SearchOptions, TextSearcher};
if let Some(ref mut editor) = self.editor {
TextSearcher::search(editor.source_mut(), pattern, &SearchOptions::default())
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn search_with_options(
&mut self,
pattern: &str,
options: crate::search::SearchOptions,
) -> Result<Vec<crate::search::SearchResult>> {
use crate::search::TextSearcher;
if let Some(ref mut editor) = self.editor {
TextSearcher::search(editor.source_mut(), pattern, &options)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn search_page(
&mut self,
page: usize,
pattern: &str,
) -> Result<Vec<crate::search::SearchResult>> {
use crate::search::{SearchOptions, TextSearcher};
if let Some(ref mut editor) = self.editor {
let regex = regex::RegexBuilder::new(pattern)
.build()
.map_err(|e| Error::InvalidPdf(format!("Invalid regex pattern: {}", e)))?;
TextSearcher::search_page(editor.source_mut(), page, ®ex, &SearchOptions::default())
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn highlight_matches(
&mut self,
results: &[crate::search::SearchResult],
color: [f32; 3],
) -> Result<()> {
use crate::annotation_types::TextMarkupType;
use crate::writer::TextMarkupAnnotation;
if self.editor.is_none() {
return Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
));
}
use std::collections::HashMap;
let mut by_page: HashMap<usize, Vec<&crate::search::SearchResult>> = HashMap::new();
for result in results {
by_page.entry(result.page).or_default().push(result);
}
for (page_num, page_results) in by_page {
let mut page = self.page(page_num)?;
for result in page_results {
let annotation =
TextMarkupAnnotation::from_rect(TextMarkupType::Highlight, result.bbox)
.with_color(color[0], color[1], color[2]);
page.add_annotation(annotation);
}
self.save_page(page)?;
}
Ok(())
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
pub fn into_bytes(mut self) -> Vec<u8> {
if self.editor.is_some()
&& self.bytes.is_empty()
&& let Ok(bytes) = self.to_bytes()
{
return bytes;
}
self.bytes
}
pub fn to_bytes(&mut self) -> Result<Vec<u8>> {
if let Some(ref mut editor) = self.editor {
editor.save_to_bytes()
} else if !self.bytes.is_empty() {
Ok(self.bytes.clone())
} else {
Err(Error::InvalidOperation("No document to serialize".to_string()))
}
}
pub fn save(&mut self, path: impl AsRef<Path>) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.save(path)
} else if !self.bytes.is_empty() {
fs::write(path.as_ref(), &self.bytes)?;
Ok(())
} else {
Err(Error::InvalidOperation("No document to save".to_string()))
}
}
pub fn save_as(&mut self, path: impl AsRef<Path>) -> Result<()> {
self.save(path)
}
pub fn save_to_bytes(&mut self) -> Result<Vec<u8>> {
if let Some(ref mut editor) = self.editor {
editor.save_to_bytes()
} else if !self.bytes.is_empty() {
Ok(self.bytes.clone())
} else {
Err(Error::InvalidOperation("No document to save".to_string()))
}
}
pub fn save_encrypted(
&mut self,
path: impl AsRef<Path>,
user_password: &str,
owner_password: &str,
) -> Result<()> {
use crate::editor::{EncryptionAlgorithm, EncryptionConfig, Permissions, SaveOptions};
let config = EncryptionConfig {
user_password: user_password.to_string(),
owner_password: owner_password.to_string(),
algorithm: EncryptionAlgorithm::Aes256,
permissions: Permissions::all(),
};
if let Some(ref mut editor) = self.editor {
editor.save_with_options(path, SaveOptions::with_encryption(config))
} else {
Err(Error::InvalidOperation(
"Encryption is only supported for opened PDFs. Use Pdf::open() first.".to_string(),
))
}
}
pub fn save_with_encryption(
&mut self,
path: impl AsRef<Path>,
config: crate::editor::EncryptionConfig,
) -> Result<()> {
use crate::editor::SaveOptions;
if let Some(ref mut editor) = self.editor {
editor.save_with_options(path, SaveOptions::with_encryption(config))
} else {
Err(Error::InvalidOperation(
"Encryption is only supported for opened PDFs. Use Pdf::open() first.".to_string(),
))
}
}
pub fn source_path(&self) -> Option<&Path> {
self.source_path.as_deref()
}
pub fn editor(&mut self) -> Option<&mut DocumentEditor> {
self.editor.as_mut()
}
pub fn config(&self) -> &PdfConfig {
&self.config
}
pub fn page_rotation(&mut self, page: usize) -> Result<i32> {
if let Some(ref mut editor) = self.editor {
editor.get_page_rotation(page)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn set_page_rotation(&mut self, page: usize, degrees: i32) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.set_page_rotation(page, degrees)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn rotate_page(&mut self, page: usize, degrees: i32) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.rotate_page_by(page, degrees)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn rotate_all_pages(&mut self, degrees: i32) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.rotate_all_pages(degrees)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn page_media_box(&mut self, page: usize) -> Result<[f32; 4]> {
if let Some(ref mut editor) = self.editor {
editor.get_page_media_box(page)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn set_page_media_box(&mut self, page: usize, rect: [f32; 4]) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.set_page_media_box(page, rect)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn page_crop_box(&mut self, page: usize) -> Result<Option<[f32; 4]>> {
if let Some(ref mut editor) = self.editor {
editor.get_page_crop_box(page)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn set_page_crop_box(&mut self, page: usize, rect: [f32; 4]) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.set_page_crop_box(page, rect)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn crop_margins(&mut self, left: f32, right: f32, top: f32, bottom: f32) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.crop_margins(left, right, top, bottom)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn erase_region(&mut self, page: usize, rect: [f32; 4]) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.erase_region(page, rect)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn erase_regions(&mut self, page: usize, rects: &[[f32; 4]]) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.erase_regions(page, rects)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn clear_erase_regions(&mut self, page: usize) {
if let Some(ref mut editor) = self.editor {
editor.clear_erase_regions(page);
}
}
pub fn flatten_page_annotations(&mut self, page: usize) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.flatten_page_annotations(page)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn flatten_all_annotations(&mut self) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.flatten_all_annotations()
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn is_page_marked_for_flatten(&self, page: usize) -> bool {
if let Some(ref editor) = self.editor {
editor.is_page_marked_for_flatten(page)
} else {
false
}
}
pub fn unmark_page_for_flatten(&mut self, page: usize) {
if let Some(ref mut editor) = self.editor {
editor.unmark_page_for_flatten(page);
}
}
pub fn flatten_forms_on_page(&mut self, page: usize) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.flatten_forms_on_page(page)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn flatten_forms(&mut self) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.flatten_forms()
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn is_page_marked_for_form_flatten(&self, page: usize) -> bool {
if let Some(ref editor) = self.editor {
editor.is_page_marked_for_form_flatten(page)
} else {
false
}
}
pub fn will_remove_acroform(&self) -> bool {
if let Some(ref editor) = self.editor {
editor.will_remove_acroform()
} else {
false
}
}
pub fn export_form_data_fdf(&mut self, output_path: impl AsRef<std::path::Path>) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.export_form_data_fdf(output_path)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn export_form_data_xfdf(
&mut self,
output_path: impl AsRef<std::path::Path>,
) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.export_form_data_xfdf(output_path)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn embed_file(&mut self, name: &str, data: Vec<u8>) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.embed_file(name, data)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn embed_file_with_options(&mut self, file: crate::writer::EmbeddedFile) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.embed_file_with_options(file)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn pending_embedded_files(&self) -> &[crate::writer::EmbeddedFile] {
if let Some(ref editor) = self.editor {
editor.pending_embedded_files()
} else {
&[]
}
}
pub fn clear_embedded_files(&mut self) {
if let Some(ref mut editor) = self.editor {
editor.clear_embedded_files();
}
}
pub fn apply_page_redactions(&mut self, page: usize) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.apply_page_redactions(page)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn apply_all_redactions(&mut self) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.apply_all_redactions()
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn is_page_marked_for_redaction(&self, page: usize) -> bool {
if let Some(ref editor) = self.editor {
editor.is_page_marked_for_redaction(page)
} else {
false
}
}
pub fn unmark_page_for_redaction(&mut self, page: usize) {
if let Some(ref mut editor) = self.editor {
editor.unmark_page_for_redaction(page);
}
}
pub fn page_images(&mut self, page: usize) -> Result<Vec<crate::editor::ImageInfo>> {
if let Some(ref mut editor) = self.editor {
editor.get_page_images(page)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn reposition_image(
&mut self,
page: usize,
image_name: &str,
x: f32,
y: f32,
) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.reposition_image(page, image_name, x, y)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn resize_image(
&mut self,
page: usize,
image_name: &str,
width: f32,
height: f32,
) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.resize_image(page, image_name, width, height)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn set_image_bounds(
&mut self,
page: usize,
image_name: &str,
x: f32,
y: f32,
width: f32,
height: f32,
) -> Result<()> {
if let Some(ref mut editor) = self.editor {
editor.set_image_bounds(page, image_name, x, y, width, height)
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn clear_image_modifications(&mut self, page: usize) {
if let Some(ref mut editor) = self.editor {
editor.clear_image_modifications(page);
}
}
pub fn has_image_modifications(&self, page: usize) -> bool {
if let Some(ref editor) = self.editor {
editor.has_image_modifications(page)
} else {
false
}
}
pub fn page_labels(&mut self) -> Result<Vec<crate::extractors::page_labels::PageLabelRange>> {
use crate::extractors::page_labels::PageLabelExtractor;
if let Some(ref mut editor) = self.editor {
PageLabelExtractor::extract(editor.source_mut())
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn page_label(&mut self, page: usize) -> Result<String> {
use crate::extractors::page_labels::PageLabelExtractor;
let ranges = self.page_labels()?;
Ok(PageLabelExtractor::get_label(&ranges, page))
}
pub fn all_page_labels(&mut self) -> Result<Vec<String>> {
use crate::extractors::page_labels::PageLabelExtractor;
let ranges = self.page_labels()?;
let page_count = self.page_count()?;
Ok(PageLabelExtractor::get_all_labels(&ranges, page_count))
}
pub fn xmp_metadata(&mut self) -> Result<Option<crate::extractors::xmp::XmpMetadata>> {
use crate::extractors::xmp::XmpExtractor;
if let Some(ref mut editor) = self.editor {
XmpExtractor::extract(editor.source_mut())
} else {
Err(Error::InvalidOperation(
"No document loaded. Use Pdf::open() to load a PDF.".to_string(),
))
}
}
pub fn has_xmp_metadata(&mut self) -> Result<bool> {
Ok(self.xmp_metadata()?.is_some())
}
pub fn xmp_title(&mut self) -> Result<Option<String>> {
if let Some(xmp) = self.xmp_metadata()? {
Ok(xmp.dc_title)
} else {
Ok(None)
}
}
pub fn xmp_creators(&mut self) -> Result<Vec<String>> {
if let Some(xmp) = self.xmp_metadata()? {
Ok(xmp.dc_creator)
} else {
Ok(Vec::new())
}
}
pub fn xmp_description(&mut self) -> Result<Option<String>> {
if let Some(xmp) = self.xmp_metadata()? {
Ok(xmp.dc_description)
} else {
Ok(None)
}
}
pub fn xmp_creator_tool(&mut self) -> Result<Option<String>> {
if let Some(xmp) = self.xmp_metadata()? {
Ok(xmp.xmp_creator_tool)
} else {
Ok(None)
}
}
pub fn xmp_create_date(&mut self) -> Result<Option<String>> {
if let Some(xmp) = self.xmp_metadata()? {
Ok(xmp.xmp_create_date)
} else {
Ok(None)
}
}
pub fn xmp_modify_date(&mut self) -> Result<Option<String>> {
if let Some(xmp) = self.xmp_metadata()? {
Ok(xmp.xmp_modify_date)
} else {
Ok(None)
}
}
pub fn xmp_producer(&mut self) -> Result<Option<String>> {
if let Some(xmp) = self.xmp_metadata()? {
Ok(xmp.pdf_producer)
} else {
Ok(None)
}
}
}
impl Default for Pdf {
fn default() -> Self {
Self::new()
}
}
pub struct PdfPageRegion<'a> {
pdf: &'a mut Pdf,
page_index: usize,
region: crate::geometry::Rect,
mode: crate::layout::RectFilterMode,
}
impl<'a> PdfPageRegion<'a> {
pub fn filter_mode(mut self, mode: crate::layout::RectFilterMode) -> Self {
self.mode = mode;
self
}
pub fn extract_text(&mut self) -> Result<String> {
self.pdf
.extract_text_in_rect(self.page_index, self.region, self.mode)
}
pub fn extract_words(&mut self) -> Result<Vec<crate::layout::Word>> {
self.pdf
.extract_words_in_rect(self.page_index, self.region, self.mode)
}
pub fn extract_text_lines(&mut self) -> Result<Vec<crate::layout::TextLine>> {
self.pdf
.extract_text_lines_in_rect(self.page_index, self.region, self.mode)
}
pub fn extract_chars(&mut self) -> Result<Vec<crate::layout::TextChar>> {
self.pdf
.extract_chars_in_rect(self.page_index, self.region, self.mode)
}
pub fn extract_rects(&mut self) -> Result<Vec<crate::elements::PathContent>> {
let rects = self.pdf.extract_rects(self.page_index)?;
Ok(rects
.into_iter()
.filter(|p| p.rendered_bbox().intersects(&self.region))
.collect())
}
pub fn extract_lines(&mut self) -> Result<Vec<crate::elements::PathContent>> {
let lines = self.pdf.extract_lines(self.page_index)?;
Ok(lines
.into_iter()
.filter(|p| p.rendered_bbox().intersects(&self.region))
.collect())
}
pub fn extract_images(&mut self) -> Result<Vec<crate::extractors::PdfImage>> {
self.pdf
.extract_images_in_rect(self.page_index, self.region)
}
pub fn extract_tables(&mut self) -> Result<Vec<crate::structure::table_extractor::Table>> {
self.pdf
.extract_tables_in_rect(self.page_index, self.region)
}
}
#[derive(Debug, Clone)]
pub struct PdfBuilder {
config: PdfConfig,
}
impl PdfBuilder {
pub fn new() -> Self {
Self {
config: PdfConfig::default(),
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.config.title = Some(title.into());
self
}
pub fn author(mut self, author: impl Into<String>) -> Self {
self.config.author = Some(author.into());
self
}
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.config.subject = Some(subject.into());
self
}
pub fn template(mut self, template: crate::writer::PageTemplate) -> Self {
self.config.template = Some(template);
self
}
pub fn keywords(mut self, keywords: impl Into<String>) -> Self {
self.config.keywords = Some(keywords.into());
self
}
pub fn page_size(mut self, size: PageSize) -> Self {
self.config.page_size = size;
self
}
pub fn margin(mut self, margin: f32) -> Self {
self.config.margin_left = margin;
self.config.margin_right = margin;
self.config.margin_top = margin;
self.config.margin_bottom = margin;
self
}
pub fn margins(mut self, left: f32, right: f32, top: f32, bottom: f32) -> Self {
self.config.margin_left = left;
self.config.margin_right = right;
self.config.margin_top = top;
self.config.margin_bottom = bottom;
self
}
pub fn font_size(mut self, size: f32) -> Self {
self.config.font_size = size;
self
}
pub fn line_height(mut self, height: f32) -> Self {
self.config.line_height = height;
self
}
pub fn from_markdown(self, content: &str) -> Result<Pdf> {
let bytes = self.render_markdown_with_fonts(content, &[])?;
Ok(Pdf {
bytes,
config: self.config,
editor: None,
source_path: None,
})
}
pub fn from_markdown_with_fonts(
self,
content: &str,
fonts: &[(String, Vec<u8>)],
) -> Result<Pdf> {
let bytes = self.render_markdown_with_fonts(content, fonts)?;
Ok(Pdf {
bytes,
config: self.config,
editor: None,
source_path: None,
})
}
pub fn from_html(self, content: &str) -> Result<Pdf> {
let bytes = self.render_html(content)?;
Ok(Pdf {
bytes,
config: self.config,
editor: None,
source_path: None,
})
}
pub fn from_text(self, content: &str) -> Result<Pdf> {
let bytes = self.render_text(content)?;
Ok(Pdf {
bytes,
config: self.config,
editor: None,
source_path: None,
})
}
pub fn from_image(self, path: impl AsRef<Path>) -> Result<Pdf> {
use crate::writer::ImageData;
let image = ImageData::from_file(path).map_err(|e| Error::Image(e.to_string()))?;
self.from_image_data(image)
}
pub fn from_image_bytes(self, data: &[u8]) -> Result<Pdf> {
use crate::writer::ImageData;
let image = ImageData::from_bytes(data).map_err(|e| Error::Image(e.to_string()))?;
self.from_image_data(image)
}
pub fn from_images<P: AsRef<Path>>(self, paths: &[P]) -> Result<Pdf> {
use crate::writer::ImageData;
if paths.is_empty() {
return Err(Error::InvalidPdf("No images provided".to_string()));
}
let images: Vec<ImageData> = paths
.iter()
.map(|p| ImageData::from_file(p).map_err(|e| Error::Image(e.to_string())))
.collect::<Result<Vec<_>>>()?;
self.from_image_data_multiple(images)
}
fn from_image_data(self, image: crate::writer::ImageData) -> Result<Pdf> {
let bytes = self.render_image(&image)?;
Ok(Pdf {
bytes,
config: self.config,
editor: None,
source_path: None,
})
}
pub fn from_image_data_multiple(self, images: Vec<crate::writer::ImageData>) -> Result<Pdf> {
let bytes = self.render_images(&images)?;
Ok(Pdf {
bytes,
config: self.config,
editor: None,
source_path: None,
})
}
fn render_image(&self, image: &crate::writer::ImageData) -> Result<Vec<u8>> {
self.render_images(std::slice::from_ref(image))
}
fn render_images(&self, images: &[crate::writer::ImageData]) -> Result<Vec<u8>> {
use crate::elements::{
ColorSpace as ElemColorSpace, ImageContent, ImageFormat as ElemImageFormat,
};
use crate::geometry::Rect;
use crate::writer::{PdfWriter, PdfWriterConfig};
let mut config = PdfWriterConfig::default();
config.title = self.config.title.clone();
config.author = self.config.author.clone();
config.subject = self.config.subject.clone();
config.creator = Some("xberg-native-pdf".to_string());
let mut writer = PdfWriter::with_config(config);
for image in images {
let (page_width, page_height, img_x, img_y, img_w, img_h) =
self.calculate_image_page_layout(image);
let mut image_content = ImageContent {
bbox: Rect::new(img_x, img_y, img_w, img_h),
format: match image.format {
crate::writer::ImageFormat::Jpeg => ElemImageFormat::Jpeg,
crate::writer::ImageFormat::Png => ElemImageFormat::Png,
crate::writer::ImageFormat::Raw => ElemImageFormat::Raw,
},
data: image.data.clone(),
width: image.width,
height: image.height,
bits_per_component: image.bits_per_component,
color_space: match image.color_space {
crate::writer::ColorSpace::DeviceGray => ElemColorSpace::Gray,
crate::writer::ColorSpace::DeviceRGB => ElemColorSpace::RGB,
crate::writer::ColorSpace::DeviceCMYK => ElemColorSpace::CMYK,
},
reading_order: None,
alt_text: None,
horizontal_dpi: None,
vertical_dpi: None,
soft_mask: image.soft_mask.clone(),
matrix: None,
is_artifact: false,
};
image_content.calculate_dpi();
let mut page = writer.add_page(page_width, page_height);
page.add_element(&crate::elements::ContentElement::Image(image_content));
page.finish();
}
writer.finish()
}
fn calculate_image_page_layout(
&self,
image: &crate::writer::ImageData,
) -> (f32, f32, f32, f32, f32, f32) {
let (page_width, page_height) = self.config.page_size.dimensions();
let avail_w = page_width - self.config.margin_left - self.config.margin_right;
let avail_h = page_height - self.config.margin_top - self.config.margin_bottom;
let (fit_w, fit_h) = image.fit_to_box(avail_w, avail_h);
let x = self.config.margin_left + (avail_w - fit_w) / 2.0;
let y = self.config.margin_bottom + (avail_h - fit_h) / 2.0;
(page_width, page_height, x, y, fit_w, fit_h)
}
fn render_markdown(&self, content: &str) -> Result<Vec<u8>> {
self.render_markdown_with_fonts(content, &[])
}
#[allow(clippy::manual_strip)]
fn render_markdown_with_fonts(
&self,
content: &str,
extra_fonts: &[(String, Vec<u8>)],
) -> Result<Vec<u8>> {
let mut builder = DocumentBuilder::new().compress_streams(true);
let mut metadata = DocumentMetadata::new();
if let Some(ref title) = self.config.title {
metadata = metadata.title(title);
}
if let Some(ref author) = self.config.author {
metadata = metadata.author(author);
}
if let Some(ref subject) = self.config.subject {
metadata = metadata.subject(subject);
}
builder = builder.metadata(metadata);
if let Some(ref template) = self.config.template {
builder = builder.template(template.clone());
}
let needs_unicode = crate::fonts::bundled::needs_unicode_font(content);
if needs_unicode {
if let Ok(font) = crate::writer::EmbeddedFont::from_data(
Some("DejaVuSans".to_string()),
crate::fonts::bundled::DEJAVU_SANS.to_vec(),
) {
builder = builder.register_embedded_font("DejaVuSans", font);
}
if let Ok(font) = crate::writer::EmbeddedFont::from_data(
Some("DejaVuSans-Bold".to_string()),
crate::fonts::bundled::DEJAVU_SANS_BOLD.to_vec(),
) {
builder = builder.register_embedded_font("DejaVuSans-Bold", font);
}
}
for (name, data) in extra_fonts {
if let Ok(font) =
crate::writer::EmbeddedFont::from_data(Some(name.clone()), data.clone())
{
builder = builder.register_embedded_font(name.clone(), font);
}
}
let (_page_width, page_height) = self.config.page_size.dimensions();
let start_y = page_height - self.config.margin_top;
let (body_font, bold_font, italic_font, bolditalic_font, mono_font): (
&'static str,
&'static str,
&'static str,
&'static str,
&'static str,
) = if needs_unicode {
("DejaVuSans", "DejaVuSans-Bold", "DejaVuSans", "DejaVuSans-Bold", "Courier")
} else {
(
"Helvetica",
"Helvetica-Bold",
"Helvetica-Oblique",
"Helvetica-BoldOblique",
"Courier",
)
};
let font_for = |r: &InlineRun, heading_bold: bool| -> &'static str {
let bold = r.bold || heading_bold;
if r.code {
mono_font
} else if bold && r.italic {
bolditalic_font
} else if bold {
bold_font
} else if r.italic {
italic_font
} else {
body_font
}
};
struct LaidRun {
x0: f32,
y: f32,
text: String,
font: &'static str,
size: f32,
first: bool,
}
let mut runs_out: Vec<LaidRun> = Vec::new();
let mut y = start_y;
let mut in_code = false;
let lines: Vec<&str> = content.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
if !in_code && is_table_line(line) && i + 1 < lines.len() {
let mut table_lines = vec![line];
let mut j = i + 1;
while j < lines.len() && is_table_line(lines[j]) {
table_lines.push(lines[j]);
j += 1;
}
if let Some(table) = GfmTable::parse(&table_lines) {
let table_font_size = self.config.font_size * 0.9;
let line_height = table_font_size * self.config.line_height;
y -= line_height * 0.5;
for table_line in table.render() {
y -= line_height;
if y < self.config.margin_bottom {
y = start_y - line_height;
}
runs_out.push(LaidRun {
x0: self.config.margin_left,
y,
text: table_line,
font: mono_font,
size: table_font_size,
first: true,
});
}
y -= line_height * 0.5;
i = j;
continue;
}
}
if line.starts_with("```") {
in_code = !in_code;
y -= self.config.font_size * self.config.line_height;
i += 1;
continue;
}
let (block_text, size, heading_bold, indent) = if in_code {
(line.to_string(), self.config.font_size * 0.9, false, 0.0)
} else if let Some(rest) = line.strip_prefix("#### ") {
(rest.to_string(), self.config.font_size * 1.1, true, 0.0)
} else if let Some(rest) = line.strip_prefix("### ") {
(rest.to_string(), self.config.font_size * 1.25, true, 0.0)
} else if let Some(rest) = line.strip_prefix("## ") {
(rest.to_string(), self.config.font_size * 1.5, true, 0.0)
} else if let Some(rest) = line.strip_prefix("# ") {
(rest.to_string(), self.config.font_size * 2.0, true, 0.0)
} else if let Some(rest) = line.strip_prefix("- ").or_else(|| line.strip_prefix("* ")) {
(format!("\u{2022} {rest}"), self.config.font_size, false, 0.0)
} else if let Some(rest) = line.strip_prefix("> ") {
(rest.to_string(), self.config.font_size, false, 12.0)
} else if line.trim().is_empty() {
y -= self.config.font_size * self.config.line_height;
i += 1;
continue;
} else {
(line.to_string(), self.config.font_size, false, 0.0)
};
let line_height = size * self.config.line_height;
y -= line_height;
if y < self.config.margin_bottom {
y = start_y - line_height;
}
let parsed = if in_code {
vec![InlineRun {
text: block_text,
bold: false,
italic: false,
code: true,
}]
} else {
parse_inline_runs(&block_text)
};
let x0 = self.config.margin_left + indent;
let mut first = true;
for r in &parsed {
if r.text.is_empty() {
continue;
}
runs_out.push(LaidRun {
x0,
y,
text: r.text.clone(),
font: font_for(r, heading_bold),
size,
first,
});
first = false;
}
i += 1;
}
{
let mut page = builder
.page(self.config.page_size)
.font(body_font, self.config.font_size);
let mut last_y = f32::MAX;
let mut pen_x = self.config.margin_left;
for run in runs_out {
if run.y > last_y {
page.done();
page = builder
.page(self.config.page_size)
.font(body_font, self.config.font_size);
}
if run.first {
pen_x = run.x0;
}
page = page.font(run.font, run.size);
let width = page.measure(&run.text);
page = page.at(pen_x, run.y).text(&run.text);
pen_x += width;
last_y = run.y;
}
page.done();
}
builder.build()
}
fn render_html(&self, content: &str) -> Result<Vec<u8>> {
let markdown = self.html_to_markdown(content);
self.render_markdown(&markdown)
}
fn html_to_markdown(&self, html: &str) -> String {
let mut result = html.to_string();
result = result.replace("<h1>", "# ").replace("</h1>", "\n");
result = result.replace("<h2>", "## ").replace("</h2>", "\n");
result = result.replace("<h3>", "### ").replace("</h3>", "\n");
result = result.replace("<h4>", "#### ").replace("</h4>", "\n");
result = result.replace("<h5>", "##### ").replace("</h5>", "\n");
result = result.replace("<h6>", "###### ").replace("</h6>", "\n");
result = result.replace("<p>", "").replace("</p>", "\n\n");
result = result
.replace("<br>", "\n")
.replace("<br/>", "\n")
.replace("<br />", "\n");
result = result.replace("<strong>", "**").replace("</strong>", "**");
result = result.replace("<b>", "**").replace("</b>", "**");
result = result.replace("<em>", "*").replace("</em>", "*");
result = result.replace("<i>", "*").replace("</i>", "*");
result = result.replace("<code>", "`").replace("</code>", "`");
result = result.replace("<pre>", "```\n").replace("</pre>", "\n```");
result = result
.replace("<blockquote>", "> ")
.replace("</blockquote>", "\n");
result = result.replace("<ul>", "").replace("</ul>", "");
result = result.replace("<ol>", "").replace("</ol>", "");
result = result.replace("<li>", "- ").replace("</li>", "\n");
let mut in_tag = false;
let mut cleaned = String::new();
for c in result.chars() {
if c == '<' {
in_tag = true;
} else if c == '>' {
in_tag = false;
} else if !in_tag {
cleaned.push(c);
}
}
let lines: Vec<&str> = cleaned.lines().collect();
lines.join("\n")
}
fn render_text(&self, content: &str) -> Result<Vec<u8>> {
let mut builder = DocumentBuilder::new();
let mut metadata = DocumentMetadata::new();
if let Some(ref title) = self.config.title {
metadata = metadata.title(title);
}
if let Some(ref author) = self.config.author {
metadata = metadata.author(author);
}
builder = builder.metadata(metadata);
if let Some(ref template) = self.config.template {
builder = builder.template(template.clone());
}
let (_page_width, page_height) = self.config.page_size.dimensions();
let start_y = page_height - self.config.margin_top;
let line_height = self.config.font_size * self.config.line_height;
let mut text_items: Vec<(f32, f32, String)> = Vec::new();
let mut y = start_y;
for line in content.lines() {
y -= line_height;
if y < self.config.margin_bottom {
y = start_y - line_height;
}
if !line.is_empty() {
text_items.push((self.config.margin_left, y, line.to_string()));
}
}
{
let mut page = builder.page(self.config.page_size);
let mut last_y = f32::MAX;
for (x, y, text) in text_items {
if y > last_y {
page.done();
page = builder.page(self.config.page_size);
}
page = page.at(x, y).text(&text);
last_y = y;
}
page.done();
}
builder.build()
}
}
impl Default for PdfBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn enc(s: &str) -> String {
parse_inline_runs(s)
.iter()
.map(|r| {
let tag = match (r.code, r.bold, r.italic) {
(true, _, _) => "C",
(false, true, true) => "J",
(false, true, false) => "B",
(false, false, true) => "I",
(false, false, false) => "P",
};
format!("{tag}:{}", r.text)
})
.collect::<Vec<_>>()
.join("|")
}
#[test]
fn test_parse_inline_runs_basic_and_edges() {
assert_eq!(enc("plain"), "P:plain");
assert_eq!(enc("**bold**"), "B:bold");
assert_eq!(enc("*it*"), "I:it");
assert_eq!(enc("`code`"), "C:code");
assert_eq!(enc("a **b** c"), "P:a |B:b|P: c");
assert_eq!(enc("call my_func_name(x)"), "P:call my_func_name(x)");
assert_eq!(enc("a * b"), "P:a * b");
assert_eq!(enc("a `b"), "P:a `b");
assert_eq!(enc("**oops"), "P:**oops");
assert_eq!(enc("**b *bi* b**"), "B:b |J:bi|B: b");
assert_eq!(enc("***x***"), "J:x");
assert_eq!(enc("`a*b*c`"), "C:a*b*c");
assert_eq!(enc(""), "");
assert_eq!(enc("****"), "");
}
#[test]
fn test_pdf_config_default() {
let config = PdfConfig::default();
assert_eq!(config.margin_left, 72.0);
assert_eq!(config.font_size, 12.0);
assert!(config.title.is_none());
}
#[test]
fn test_pdf_builder_chain() {
let builder = PdfBuilder::new()
.title("Test")
.author("Author")
.subject("Subject")
.keywords("test, pdf")
.page_size(PageSize::A4)
.margin(50.0)
.font_size(11.0)
.line_height(1.4);
assert_eq!(builder.config.title, Some("Test".to_string()));
assert_eq!(builder.config.author, Some("Author".to_string()));
assert_eq!(builder.config.margin_left, 50.0);
assert_eq!(builder.config.font_size, 11.0);
}
#[test]
fn test_pdf_builder_margins() {
let builder = PdfBuilder::new().margins(10.0, 20.0, 30.0, 40.0);
assert_eq!(builder.config.margin_left, 10.0);
assert_eq!(builder.config.margin_right, 20.0);
assert_eq!(builder.config.margin_top, 30.0);
assert_eq!(builder.config.margin_bottom, 40.0);
}
#[test]
fn test_pdf_from_text() {
let result = Pdf::from_text("Hello, World!");
assert!(result.is_ok());
let pdf = result.unwrap();
assert!(!pdf.as_bytes().is_empty());
}
#[test]
fn test_pdf_from_markdown() {
let result = Pdf::from_markdown("# Hello\n\nWorld");
assert!(result.is_ok());
let pdf = result.unwrap();
assert!(!pdf.as_bytes().is_empty());
}
#[test]
fn test_pdf_from_html() {
let result = Pdf::from_html("<h1>Hello</h1><p>World</p>");
assert!(result.is_ok());
let pdf = result.unwrap();
assert!(!pdf.as_bytes().is_empty());
}
#[test]
fn test_html_to_markdown() {
let builder = PdfBuilder::new();
let md = builder.html_to_markdown("<h1>Title</h1><p>Text</p>");
assert!(md.contains("# Title"));
assert!(md.contains("Text"));
}
#[test]
fn test_pdf_into_bytes() {
let pdf = Pdf::from_text("Test").unwrap();
let bytes = pdf.into_bytes();
assert!(!bytes.is_empty());
assert!(bytes.starts_with(b"%PDF"));
}
#[test]
fn test_gfm_table_parse_simple() {
let lines = vec![
"| Name | Age |",
"|------|-----|",
"| Alice | 30 |",
"| Bob | 25 |",
];
let table = super::GfmTable::parse(&lines);
assert!(table.is_some());
let table = table.unwrap();
assert_eq!(table.headers.len(), 2);
assert_eq!(table.headers[0], "Name");
assert_eq!(table.headers[1], "Age");
assert_eq!(table.rows.len(), 2);
assert_eq!(table.rows[0][0], "Alice");
assert_eq!(table.rows[0][1], "30");
}
#[test]
fn test_gfm_table_alignments() {
let lines = vec![
"| Left | Center | Right |",
"|:-----|:------:|------:|",
"| L | C | R |",
];
let table = super::GfmTable::parse(&lines).unwrap();
assert_eq!(table.alignments[0], super::GfmAlign::Left);
assert_eq!(table.alignments[1], super::GfmAlign::Center);
assert_eq!(table.alignments[2], super::GfmAlign::Right);
}
#[test]
fn test_gfm_table_render() {
let lines = vec!["| A | B |", "|---|---|", "| 1 | 2 |"];
let table = super::GfmTable::parse(&lines).unwrap();
let rendered = table.render();
assert_eq!(rendered.len(), 3);
assert!(rendered[0].contains("A"));
assert!(rendered[0].contains("B"));
assert!(rendered[1].contains("-"));
assert!(rendered[2].contains("1"));
assert!(rendered[2].contains("2"));
}
#[test]
fn test_gfm_table_invalid_separator() {
let lines = vec!["| Name | Age |", "| Alice | 30 |"];
let table = super::GfmTable::parse(&lines);
assert!(table.is_none());
}
#[test]
fn test_gfm_table_in_markdown() {
let markdown = r#"# Table Test
Here is a table:
| Item | Price |
|------|-------|
| Apple | $1 |
| Orange | $2 |
End of table.
"#;
let result = Pdf::from_markdown(markdown);
assert!(result.is_ok());
}
#[test]
fn test_is_table_line() {
assert!(super::is_table_line("| A | B |"));
assert!(super::is_table_line("|---|---|"));
assert!(super::is_table_line("| Cell |"));
assert!(!super::is_table_line("Not a table"));
assert!(!super::is_table_line("| Only one pipe"));
assert!(!super::is_table_line(""));
}
#[test]
fn test_pdf_from_image_bytes_jpeg() {
let jpeg_bytes: Vec<u8> = vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00,
0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06,
0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D,
0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12, 0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D,
0x1A, 0x1C, 0x1C, 0x20, 0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28,
0x37, 0x29, 0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32,
0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x01,
0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02,
0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10,
0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00,
0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06,
0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42,
0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16,
0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55,
0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73,
0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5,
0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA,
0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6,
0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA,
0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08,
0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0xFB, 0xD5, 0xDB, 0x20, 0xA8, 0xF9, 0xFF, 0xD9,
];
let result = Pdf::from_image_bytes(&jpeg_bytes);
assert!(result.is_ok(), "Failed to create PDF from JPEG: {:?}", result.err());
let pdf = result.unwrap();
assert!(!pdf.as_bytes().is_empty());
assert!(pdf.as_bytes().starts_with(b"%PDF"));
}
#[test]
fn test_pdf_from_images_empty() {
let paths: Vec<&str> = vec![];
let result = Pdf::from_images(&paths);
assert!(result.is_err());
}
#[test]
fn test_calculate_image_page_layout() {
use crate::writer::ColorSpace;
use crate::writer::ImageData;
let builder = PdfBuilder::new().page_size(PageSize::Letter);
let image = ImageData::new(100, 50, ColorSpace::DeviceRGB, vec![0; 15000]);
let (page_w, page_h, x, y, w, h) = builder.calculate_image_page_layout(&image);
assert_eq!(page_w, 612.0);
assert_eq!(page_h, 792.0);
assert!(w > 0.0);
assert!(h > 0.0);
assert!((w / h - 2.0).abs() < 0.01);
assert!(x > 0.0);
assert!(y > 0.0);
}
#[test]
fn test_pdf_from_bytes() {
let pdf = Pdf::from_text("Hello from bytes").unwrap();
let bytes = pdf.into_bytes();
assert!(!bytes.is_empty());
let mut reopened = Pdf::from_bytes(bytes).unwrap();
assert_eq!(reopened.page_count().unwrap(), 1);
}
#[test]
fn test_pdf_from_bytes_invalid() {
let result = Pdf::from_bytes(vec![0, 1, 2, 3]);
assert!(result.is_err());
}
#[test]
fn to_bytes_returns_each_callers_own_document_under_concurrency() {
const THREADS: usize = 8;
const ITERATIONS: usize = 12;
let sources: Vec<Vec<u8>> = (0..THREADS)
.map(|i| {
Pdf::from_text(&format!("document number {i}"))
.unwrap()
.into_bytes()
})
.collect();
let handles: Vec<_> = sources
.into_iter()
.enumerate()
.map(|(index, source)| {
std::thread::spawn(move || {
let marker = format!("document number {index}");
for _ in 0..ITERATIONS {
let mut pdf = Pdf::from_bytes(source.clone()).unwrap();
let produced = pdf.to_bytes().unwrap();
assert!(
produced.starts_with(b"%PDF"),
"thread {index} got something that is not a PDF"
);
assert!(
produced
.windows(marker.len())
.any(|window| window == marker.as_bytes()),
"thread {index} was handed bytes that do not contain its own \
document's text — it read another thread's file"
);
}
})
})
.collect();
for handle in handles {
handle.join().expect("no worker thread may panic");
}
}
#[test]
fn test_pdf_from_bytes_edit_and_save() {
let pdf = Pdf::from_text("Editable content").unwrap();
let bytes = pdf.into_bytes();
let mut doc = Pdf::from_bytes(bytes).unwrap();
let count = doc.page_count().unwrap();
assert_eq!(count, 1);
let saved = doc.save_to_bytes().unwrap();
assert!(!saved.is_empty());
}
}