') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - fschutt/printpdf: Rust / WASM library for reading, writing and rendering PDF · GitHub
Skip to content

Repository files navigation

printpdf

CIDependencies

printpdf is a Rust library for creating, reading, writing and rendering PDF documents.

Website | Crates.io | Documentation | Donate

Note

HTML-to-PDF rendering (PdfDocument::from_html, html feature) works for basic layouts - tables, page breaks, headers/footers - but is still evolving. For pixel-exact control, position PDF elements manually with ops.

Features

  • Pages, Bookmarks, link annotations (read / write)
  • Layers (read / write)
  • Graphics: lines, shapes, bezier curves, SVG content (read / write)
  • Images encoding / decoding (read support: experimental / write supported with image)
  • Embedded fonts, Unicode support (read support: experimental / write)
  • Minifying file size (auto-subsetting fonts)
  • Advanced graphics - overprint, blending, etc.
  • Advanced typography - character / word scaling and spacing, superscript, subscript, etc.
  • Embedding SVGs (uses svg2pdf crate internally)

Experimental features:

  • Rendering PDF pages to SVG (one standalone SVG file per page)
    • You can use the resulting SVG to render the PDF page into an image using resvg
  • Extracting text from PDF pages (auto-decodes Unicode, line breaks and text positions)
  • XHTML layouting for simple page layout (using azul-layout + xmlparser)
    • Good enough for basic page layouting, book rendering, reports and forms, with automatic page-breaking

NOT supported are:

  • Gradients
  • Patterns
  • File attachements
  • Open Prepress Interface
  • Halftoning images
  • Conformance / error checking for various PDF standards
  • Embedded Javascript

See the WASM32 demo live at: https://fschutt.github.io/printpdf

Writing PDF

Basic example

use printpdf::*;fnmain(){letmut doc = PdfDocument::new("My first PDF");let page1_contents = vec![Op::Marker{ id:"debugging-marker".to_string()}];let page1 = PdfPage::new(Mm(210.0),Mm(297.0), page1_contents);letmut warnings = Vec::new();let pdf_bytes:Vec<u8> = doc
.with_pages(vec![page1]).save(&PdfSaveOptions::default(),&mut warnings);}

Graphics

use printpdf::*;fnmain(){letmut doc = PdfDocument::new("My first PDF");// Square shape. `bezier: true` would make the point a bezier control// handle for a curve. If you want holes, wind one ring counterclockwise.let line = Line{points:vec![LinePoint{ p:Point::new(Mm(100.0),Mm(100.0)), bezier:false},LinePoint{ p:Point::new(Mm(100.0),Mm(200.0)), bezier:false},LinePoint{ p:Point::new(Mm(300.0),Mm(200.0)), bezier:false},LinePoint{ p:Point::new(Mm(300.0),Mm(100.0)), bezier:false},],is_closed:true,};// Triangle shapelet polygon = Polygon{rings:vec![PolygonRing{
points: vec![LinePoint{ p:Point::new(Mm(150.0),Mm(150.0)), bezier:false},LinePoint{ p:Point::new(Mm(150.0),Mm(250.0)), bezier:false},LinePoint{ p:Point::new(Mm(350.0),Mm(250.0)), bezier:false},],}],mode:PaintMode::FillStroke,winding_order:WindingOrder::NonZero,};// Graphics configlet fill_color = Color::Cmyk(Cmyk::new(0.0,0.23,0.0,0.0,None));let outline_color = Color::Rgb(Rgb::new(0.75,1.0,0.64,None));let dash_pattern = LineDashPattern::new(0.0,&[20.0,10.0]);// dash, gaplet extgstate = ExtendedGraphicsState::default().with_overprint_stroke(true).with_blend_mode(BlendMode::multiply());let page1_contents = vec![// add line1 (square)Op::SetOutlineColor{ col:Color::Rgb(Rgb::new(0.75,1.0,0.64,None))},Op::SetOutlineThickness{ pt:Pt(10.0)},Op::DrawLine{ line: line },// add line2 (triangle)Op::SaveGraphicsState,Op::LoadGraphicsState{ gs: doc.add_graphics_state(extgstate)},Op::SetLineDashPattern{ dash: dash_pattern },Op::SetLineJoinStyle{ join:LineJoinStyle::Round},Op::SetLineCapStyle{ cap:LineCapStyle::Round},Op::SetFillColor{ col: fill_color },Op::SetOutlineThickness{ pt:Pt(15.0)},Op::SetOutlineColor{ col: outline_color },Op::DrawPolygon{ polygon: polygon },Op::RestoreGraphicsState,];let page1 = PdfPage::new(Mm(210.0),Mm(297.0), page1_contents);letmut warnings = Vec::new();let pdf_bytes:Vec<u8> = doc
.with_pages(vec![page1]).save(&PdfSaveOptions::default(),&mut warnings);}

Images

  • Images only get compressed in release mode. You might get huge PDFs (6 or more MB) in debug mode.
  • To make this process faster, use BufReader instead of directly reading from the file.
  • Scaling of images is implicitly done to fit one pixel = one dot at 300 dpi.
use printpdf::*;fnmain(){letmut doc = PdfDocument::new("My first PDF");let image_bytes = include_bytes!("assets/img/BMP_test.bmp");letmut warnings = Vec::new();// requires --feature bmplet image = RawImage::decode_from_bytes(image_bytes,&mut warnings).unwrap();// In the PDF, an image is an `XObject`, identified by a unique `XObjectId`let image_xobject_id = doc.add_image(&image);let page1_contents = vec![Op::UseXobject{ id: image_xobject_id.clone(), transform:XObjectTransform::default()}];let page1 = PdfPage::new(Mm(210.0),Mm(297.0), page1_contents);let pdf_bytes:Vec<u8> = doc
.with_pages(vec![page1]).save(&PdfSaveOptions::default(),&mut warnings);}

Fonts

use printpdf::*;fnmain(){letmut doc = PdfDocument::new("My first PDF");let roboto_bytes = include_bytes!("assets/fonts/RobotoMedium.ttf");let font_index = 0;// face index inside a .ttc collection; 0 for a plain .ttf/.otfletmut font_warnings = Vec::new();let font = ParsedFont::from_bytes(roboto_bytes, font_index,&mut font_warnings).unwrap();// printpdf automatically keeps track of which fonts are used in the PDFlet font_id = doc.add_font(&font);let text_pos = Point{x:Mm(10.0).into(),y:Mm(100.0).into(),};// from bottom leftletmut warnings = Vec::new();// Text-showing ops must sit between StartTextSection and EndTextSection -// most viewers drop text outside of one (save() warns if you forget).let page1_contents = vec![Op::StartTextSection,Op::SetTextCursor{ pos: text_pos },Op::SetLineHeight{ lh:Pt(33.0)},Op::SetFont{
font:PdfFontHandle::External(font_id.clone()),
size:Pt(16.0),},Op::ShowText{
items: vec![TextItem::Text("Lorem ipsum".to_string())],},Op::AddLineBreak,Op::ShowText{
items: vec![TextItem::Text("dolor sit amet".to_string())],},Op::EndTextSection,];let save_options = PdfSaveOptions{subset_fonts:true,// auto-subset fonts on save
..Default::default()};let page1 = PdfPage::new(Mm(210.0),Mm(297.0), page1_contents);let pdf_bytes:Vec<u8> = doc
.with_pages(vec![page1]).save(&save_options,&mut warnings);}

Tables, HTML

For creating tables, etc. printpdf uses a basic layout system, similar to wkhtmltopdf (although more limited in terms of features). It's good enough for basic page layouting, book rendering and reports / forms / etc. Includes automatic page-breaking.

Since printpdf supports WASM, there is an interactive demo at https://fschutt.github.io/printpdf - try playing with the XML.

See SYNTAX.md for the XML syntax description.

// needs --features="html"use printpdf::*;fnmain(){// See https://fschutt.github.io/printpdf for an interactive WASM demo!let html = r#" <html> <!-- printpdf automatically breaks content into pages --> <body style="padding:10mm"> <p style="color: red; font-family: sans-serif;" data-chapter="1" data-subsection="First subsection">Hello!</p> <div style="width:200px;height:200px;background:red;" data-chapter="1" data-subsection="Second subsection"> <p>World!</p> </div> </body> <!-- configure header and footer for each page --> <head> <header> <h4 style="color: #2e2e2e;min-height: 8mm;">Chapter {attr:chapter} * {attr:subsection}</h4> <p style="position: absolute;top:5mm;left:5mm;">{builtin:pagenum}</p> </header> <footer> <hr/> <footer/> </head> </html> "#;// named images to be used in the HTML, i.e. "image1.png" => Base64OrRaw::Raw(bytes)let images = BTreeMap::new();// named fonts to be used in the HTML, i.e. "Roboto" => Base64OrRaw::Raw(roboto_bytes)let fonts = BTreeMap::new();// page size, margins, etc. - all optional; printpdf auto-page-breakslet options = GeneratePdfOptions{page_width:Some(210.0),// mmpage_height:Some(297.0),// mm
..Default::default()};letmut warnings = Vec::new();let pdf_bytes = PdfDocument::from_html(html,&images,&fonts,&options,&mut warnings).unwrap().save(&PdfSaveOptions::default(),&mut warnings);}

Roadmap

The goal of printpdf is to be a general-use PDF library, such as libharu or similar. PDFs generated by printpdf should always adhere to a PDF standard, except if you turn it off. Currently, only the standard PDF/X-3:2002 is covered (i.e. valid PDF according to Adobe Acrobat). Over time, there will be more standards supported.

The printpdf wiki is live at: https://github.com/fschutt/printpdf/wiki

Here are some resources I found while working on this library:

License / Support

Library is licensed MIT.

You can donate (one-time or recurrent) at https://github.com/sponsors/fschutt. Thanks!

About

Rust / WASM library for reading, writing and rendering PDF

Topics

Resources

Stars

1.1k stars

Watchers

13 watching

Forks

Sponsor this project

Used by

Contributors

Languages