Repository: pdf-rs/pdf_render Branch: master Commit: 00b907936e45 Files: 32 Total size: 115.6 KB Directory structure: gitextract_y35u9yf9/ ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── download_fonts.sh ├── examples/ │ └── pdf2image/ │ ├── Cargo.toml │ ├── font_AAAAAH+Baskerville │ └── src/ │ └── main.rs ├── fonts.tar.bz ├── render/ │ ├── Cargo.toml │ ├── benches/ │ │ ├── render.rs │ │ └── view.rs │ ├── examples/ │ │ └── trace.rs │ └── src/ │ ├── backend.rs │ ├── cache.rs │ ├── font.rs │ ├── fontentry.rs │ ├── graphicsstate.rs │ ├── image.rs │ ├── lib.rs │ ├── renderstate.rs │ ├── scene.rs │ ├── textstate.rs │ └── tracer.rs ├── view/ │ ├── Cargo.toml │ ├── Makefile │ └── src/ │ ├── bin/ │ │ ├── convert.rs │ │ └── view.rs │ └── lib.rs └── wasm/ ├── index.html ├── index.js └── style.css ================================================ FILE CONTENTS ================================================ ================================================ FILE: .travis.yml ================================================ language: rust script: - cargo update - cargo build - cargo test ================================================ FILE: Cargo.toml ================================================ [workspace] members = [ "render", "view", "examples/pdf2image", ] [patch.crates-io] pathfinder_gl = { git = "https://github.com/servo/pathfinder" } pathfinder_webgl = { git = "https://github.com/servo/pathfinder" } pathfinder_gpu = { git = "https://github.com/servo/pathfinder" } pathfinder_content = { git = "https://github.com/servo/pathfinder" } pathfinder_color = { git = "https://github.com/servo/pathfinder" } pathfinder_geometry = { git = "https://github.com/servo/pathfinder" } pathfinder_renderer = { git = "https://github.com/servo/pathfinder" } pathfinder_resources = { git = "https://github.com/servo/pathfinder" } pathfinder_export = { git = "https://github.com/servo/pathfinder" } pathfinder_simd = { git = "https://github.com/servo/pathfinder" } [patch."https://github.com/s3bk/pathfinder_view"] pathfinder_view = { path = "../pathfinder_view", features=["icon"] } [patch."https://github.com/pdf-rs/pdf"] pdf = { path = "../pdf/pdf", default-features=false } #[patch."https://github.com/pdf-rs/font"] #font = { path = "../font" } [patch."https://github.com/servo/pathfinder"] pathfinder_gl = { path = "../pathfinder/gl" } pathfinder_webgl = { path = "../pathfinder/webgl" } pathfinder_gpu = { path = "../pathfinder/gpu" } pathfinder_content = { path = "../pathfinder/content" } pathfinder_color = { path = "../pathfinder/color" } pathfinder_renderer = { path = "../pathfinder/renderer" } pathfinder_resources = { path = "../pathfinder/resources" } pathfinder_export = { path = "../pathfinder/export" } pathfinder_simd = { path = "../pathfinder/simd" } pathfinder_geometry = { path = "../pathfinder/geometry" } ================================================ FILE: LICENSE ================================================ Copyright © 2020 The pdf-rs contributers. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # pdf_render [![Build Status](https://travis-ci.com/pdf-rs/pdf.svg?branch=master)](https://travis-ci.com/pdf-rs/pdf_render) Experimental PDF viewer building on [pdf](https://github.com/pdf-rs/pdf). Feel free to contribute with ideas, issues or code! Please join [us on Zulip](https://type.zulipchat.com/#narrow/stream/209232-pdf) if you have any questions or problems. # Fonts Get a copy of https://github.com/s3bk/pdf_fonts and set `STANDARD_FONTS` to the directory of `pdf_fonts`. # Viewer run it: `cargo run --bin view --release YOUR_FILE.pdf` Right now you can change pages with left and right arrow keys and zoom with '+' and '-'. Works for some files. ## [Try it in your browser](https://pdf-rs.github.io/view-wasm/) ================================================ FILE: download_fonts.sh ================================================ #!/usr/bin/env bash set -e TMPDIR=`mktemp -d` if [[ ! "$TMPDIR" || ! -d "$TMPDIR" ]]; then echo "Couldn't create temporary directory" exit 1 fi function cleanup { rm -rf "$TMPDIR" } trap cleanup EXIT curl http://ardownload.adobe.com/pub/adobe/reader/unix/9.x/9.5.5/enu/AdbeRdr9.5.5-1_i386linux_enu.deb > "$TMPDIR/AdbeRdr9.5.5-1_i386linux_enu.deb" (cd "$TMPDIR" && ar x AdbeRdr9.5.5-1_i386linux_enu.deb data.tar.gz) mkdir -p fonts/PFM tar xzf "$TMPDIR/data.tar.gz" --directory=fonts --strip-components=6 ./opt/Adobe/Reader9/Resource/Font/{AdobePiStd.otf,CourierStd-BoldOblique.otf,CourierStd-Bold.otf,CourierStd-Oblique.otf,CourierStd.otf,MinionPro-BoldIt.otf,MinionPro-Bold.otf,MinionPro-It.otf,MinionPro-Regular.otf,MyriadPro-BoldIt.otf,MyriadPro-Bold.otf,MyriadPro-It.otf,MyriadPro-Regular.otf,ZX______.PFB,ZY______.PFB,SY______.PFB} ./opt/Adobe/Reader9/Resource/Font/PFM/{zx______.pfm,zy______.pfm,SY______.PFM} export STANDARD_FONTS=$pwd/fonts ================================================ FILE: examples/pdf2image/Cargo.toml ================================================ [package] name = "pdf2image" version = "0.1.0" authors = ["Sebastian K "] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] pdf = { git = "https://github.com/pdf-rs/pdf" } pdf_render = { path = "../../render" } argh = "*" pathfinder_rasterize = { path = "../../../pathfinder_rasterize" } # git = "https://github.com/s3bk/pathfinder_rasterizer" } pathfinder_geometry = { git = "https://github.com/servo/pathfinder" } env_logger = "*" ================================================ FILE: examples/pdf2image/src/main.rs ================================================ use argh::FromArgs; use pdf::file::{File, FileOptions}; use pdf_render::{Cache, SceneBackend, render_page}; use pathfinder_rasterize::Rasterizer; use pathfinder_geometry::transform2d::Transform2F; use std::error::Error; use std::path::PathBuf; #[derive(FromArgs)] /// PDF rasterizer struct Options { /// DPI #[argh(option, default="150.")] dpi: f32, /// page to render (0 based) #[argh(option, default="0")] page: u32, /// input PDF file #[argh(positional)] pdf: PathBuf, /// output image #[argh(positional)] image: PathBuf, } fn main() -> Result<(), Box> { env_logger::init(); let opt: Options = argh::from_env(); let file = FileOptions::uncached().open(&opt.pdf)?; let resolver = file.resolver(); let page = file.get_page(opt.page)?; let mut cache = Cache::new(); let mut backend = SceneBackend::new(&mut cache); render_page(&mut backend, &resolver, &page, Transform2F::from_scale(opt.dpi / 25.4))?; let image = Rasterizer::new().rasterize(backend.finish(), None); image.save(opt.image)?; Ok(()) } ================================================ FILE: render/Cargo.toml ================================================ [package] name = "pdf_render" version = "0.1.0" authors = ["Sebastian Köln "] edition = "2021" [features] unstable = [] embed = ["dep:rust-embed"] [[bench]] name = "render" harness = false [dependencies.pdf] default-features=false features = ["cache", "dump"] git = "https://github.com/pdf-rs/pdf" [dependencies] pathfinder_renderer = { git = "https://github.com/servo/pathfinder" } pathfinder_color = { git = "https://github.com/servo/pathfinder" } pathfinder_geometry = { git = "https://github.com/servo/pathfinder" } pathfinder_resources = { git = "https://github.com/servo/pathfinder" } pathfinder_content = { git = "https://github.com/servo/pathfinder" } log = "0.4" font = { git = "https://github.com/pdf-rs/font" } pdf_encoding = "0.4" itertools = "*" image = "0.25" instant = "*" custom_debug_derive = "*" globalcache = { version = "0.3", features = ["sync"] } istring = { git = "https://github.com/s3bk/istring" } once_cell = "*" serde_json = "*" glyphmatcher = { git = "https://github.com/s3bk/glyphmatcher" } rust-embed = { version = "*", optional = true, features = ["interpolate-folder-path"] } [dev-dependencies] criterion = "0.3" env_logger = "*" ================================================ FILE: render/benches/render.rs ================================================ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use pdf::file::{FileOptions}; use pdf_render::{Cache, render_page, SceneBackend}; use std::time::Duration; fn bench_render_page(c: &mut Criterion) { let file = FileOptions::cached().open("/home/sebk/Downloads/10.1016@j.eswa.2020.114101.pdf").unwrap(); let resolver = file.resolver(); let mut group = c.benchmark_group("10.1016@j.eswa.2020.114101.pdf"); group.sample_size(50); group.warm_up_time(Duration::from_secs(1)); let mut cache = Cache::new(); let mut backend = SceneBackend::new(&mut cache); for (i, page) in file.pages().enumerate() { if let Ok(page) = page { group.bench_function(&format!("page {}", i), |b| b.iter(|| render_page(&mut backend, &resolver, &page, Default::default()).unwrap())); } } group.finish(); } criterion_group!(benches, bench_render_page); criterion_main!(benches); ================================================ FILE: render/benches/view.rs ================================================ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use pdf::file::{FileOptions}; use pdf::object::*; use std::path::Path; use pdf_render::{Cache, render_page, SceneBackend}; use pathfinder_renderer::scene::Scene; fn render_file(path: &Path) -> Vec { let file = FileOptions::cached().open(path).unwrap(); let resolver = file.resolver(); let mut cache = Cache::new(); file.pages().map(|page| { let p: &Page = &*page.unwrap(); let mut backend = SceneBackend::new(&mut cache); render_page(&mut backend, &resolver, p, Default::default()).unwrap(); backend.finish() }).collect() } fn bench_file(c: &mut Criterion, name: &str) { let path = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().join("files").join(name); c.bench_function(name, |b| b.iter(|| render_file(&path))); } macro_rules! bench_files { (@a $($file:expr, $name:ident;)*) => ( $( fn $name(c: &mut Criterion) { bench_file(c, $file) } )* ); (@b $($file:expr, $name:ident;)*) => ( criterion_group!(benches $(, $name)*); ); ($($file:expr, $name:ident;)*) => ( bench_files!(@a $($file, $name;)*); bench_files!(@b $($file, $name;)*); ); } bench_files!( "example.pdf", example; "ep.pdf", ep; "ep2.pdf", ep2; "libreoffice.pdf", libreoffice; "pdf-sample.pdf", pdf_sample; "xelatex-drawboard.pdf", xelatex_drawboard; "xelatex.pdf", xelatex; "PDF32000_2008.pdf", pdf32000; ); criterion_main!(benches); ================================================ FILE: render/examples/trace.rs ================================================ use pdf::file::FileOptions; use pdf_render::render_page; use pdf_render::tracer::{TraceCache, Tracer}; fn main() { env_logger::init(); let arg = std::env::args().nth(1).unwrap(); let file = FileOptions::cached().open(&arg).unwrap(); let resolver = file.resolver(); let mut cache = TraceCache::new(); for page in file.pages() { let p = page.unwrap(); let mut clip_paths = vec![]; let mut backend = Tracer::new(&mut cache, &mut clip_paths); render_page(&mut backend, &resolver, &p, Default::default()).unwrap(); let items = backend.finish(); for i in items { println!("{:?}", i); } } } ================================================ FILE: render/src/backend.rs ================================================ use pathfinder_geometry::{ transform2d::Transform2F, rect::RectF, }; use pathfinder_content::{ fill::FillRule, stroke::{StrokeStyle}, outline::Outline, }; use pdf::{object::{Ref, XObject, ImageXObject, Resolve, Resources, MaybeRef}, content::Op}; use pdf::error::PdfError; use font::Glyph; use super::{FontEntry, TextSpan, Fill}; use pdf::font::Font as PdfFont; use std::sync::Arc; #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub enum BlendMode { Overlay, Darken } pub trait Backend { type ClipPathId: Copy; fn create_clip_path(&mut self, path: Outline, fill_rule: FillRule, parent: Option) -> Self::ClipPathId; fn draw(&mut self, outline: &Outline, mode: &DrawMode, fill_rule: FillRule, transform: Transform2F, clip: Option); fn set_view_box(&mut self, r: RectF); fn draw_image(&mut self, xref: Ref, im: &ImageXObject, resources: &Resources, transform: Transform2F, mode: BlendMode, clip: Option, resolve: &impl Resolve); fn draw_inline_image(&mut self, im: &Arc, resources: &Resources, transform: Transform2F, mode: BlendMode, clip: Option, resolve: &impl Resolve); fn draw_glyph(&mut self, glyph: &Glyph, mode: &DrawMode, transform: Transform2F, clip: Option) { self.draw(&glyph.path, mode, FillRule::Winding, transform, clip); } fn get_font(&mut self, font_ref: &MaybeRef, resolve: &impl Resolve) -> Result>, PdfError>; fn add_text(&mut self, span: TextSpan, clip: Option); /// The following functions are for debugging PDF files and not relevant for rendering them. fn bug_text_no_font(&mut self, data: &[u8]) {} fn bug_text_invisible(&mut self, text: &str) {} fn bug_postscript(&mut self, data: &[u8]) {} fn bug_op(&mut self, op_nr: usize) {} fn inspect_op(&mut self, op: &Op) {} } #[derive(Clone, Debug)] pub struct FillMode { pub color: Fill, pub alpha: f32, pub mode: BlendMode, } pub enum DrawMode { Fill { fill: FillMode }, Stroke { stroke: FillMode, stroke_mode: Stroke }, FillStroke { fill: FillMode, stroke: FillMode, stroke_mode: Stroke }, } #[derive(Clone, Debug)] pub struct Stroke { pub dash_pattern: Option<(Vec, f32)>, pub style: StrokeStyle, } ================================================ FILE: render/src/cache.rs ================================================ use std::path::{PathBuf}; use std::sync::Arc; use pdf::object::*; use pdf::primitive::Name; use pdf::font::{Font as PdfFont}; use pdf::error::{Result}; use pathfinder_geometry::{ vector::{Vector2I}, }; use pathfinder_content::{ pattern::{Image}, }; use crate::BlendMode; use super::{fontentry::FontEntry}; use super::image::load_image; use super::font::{load_font, StandardCache}; use globalcache::{sync::SyncCache, ValueSize}; #[derive(Clone)] pub struct ImageResult(pub Arc>); impl ValueSize for ImageResult { fn size(&self) -> usize { match *self.0 { Ok(ref im) => im.pixels().len() * 4, Err(_) => 1, } } } pub struct Cache { // shared mapping of fontname -> font fonts: Arc>>>, images: Arc, BlendMode), ImageResult>>, std: StandardCache, missing_fonts: Vec, } impl Cache { pub fn new() -> Cache { Cache { fonts: SyncCache::new(), images: SyncCache::new(), std: StandardCache::new(), missing_fonts: Vec::new(), } } pub fn get_font(&mut self, pdf_font: &MaybeRef, resolve: &impl Resolve) -> Result>, > { let mut error = None; let val = self.fonts.get(&**pdf_font as *const PdfFont as usize, |_| match load_font(pdf_font, resolve, &mut self.std) { Ok(Some(f)) => Some(Arc::new(f)), Ok(None) => { if let Some(ref name) = pdf_font.name { self.missing_fonts.push(name.clone()); } None }, Err(e) => { error = Some(e); None } } ); match error { None => Ok(val), Some(e) => Err(e) } } pub fn get_image(&mut self, xobject_ref: Ref, im: &ImageXObject, resources: &Resources, resolve: &impl Resolve, mode: BlendMode) -> ImageResult { self.images.get((xobject_ref, mode), |_| ImageResult(Arc::new(load_image(im, resources, resolve, mode).map(|image| Image::new(Vector2I::new(im.width as i32, im.height as i32), Arc::new(image.into_data().into())) ))) ) } } impl Drop for Cache { fn drop(&mut self) { info!("missing fonts:"); for name in self.missing_fonts.iter() { info!("{}", name.as_str()); } } } ================================================ FILE: render/src/font.rs ================================================ use std::borrow::Cow; use std::path::{PathBuf}; use std::ops::Deref; use std::collections::HashMap; use glyphmatcher::FontDb; use pdf::object::*; use pdf::font::{Font as PdfFont}; use pdf::error::{Result, PdfError}; use font::{self}; use std::sync::Arc; use super::FontEntry; use globalcache::{sync::SyncCache, ValueSize}; use std::hash::{Hash, Hasher}; #[derive(Clone)] pub struct FontRc(Arc); impl ValueSize for FontRc { #[inline] fn size(&self) -> usize { 1 // TODO } } impl From> for FontRc { #[inline] fn from(f: Box) -> Self { FontRc(f.into()) } } impl Deref for FontRc { type Target = dyn font::Font + Send + Sync + 'static; #[inline] fn deref(&self) -> &Self::Target { &*self.0 } } impl PartialEq for FontRc { #[inline] fn eq(&self, rhs: &Self) -> bool { Arc::as_ptr(&self.0) == Arc::as_ptr(&rhs.0) } } impl Eq for FontRc {} impl Hash for FontRc { #[inline] fn hash(&self, state: &mut H) { Arc::as_ptr(&self.0).hash(state) } } pub struct StandardCache { inner: Arc>>, #[cfg(not(feature="embed"))] dir: PathBuf, #[cfg(feature="embed")] dir: EmbeddedStandardFonts, fonts: HashMap, dump: Dump, require_unique_unicode: bool, } impl StandardCache { #[cfg(not(feature="embed"))] pub fn new() -> StandardCache { let standard_fonts = PathBuf::from(std::env::var_os("STANDARD_FONTS").expect("STANDARD_FONTS is not set. Please check https://github.com/pdf-rs/pdf_render/#fonts for instructions.")); let data = standard_fonts.read_file("fonts.json").expect("can't read fonts.json"); let fonts: HashMap = serde_json::from_slice(&data).expect("fonts.json is invalid"); let dump = match std::env::var("DUMP_FONT").as_deref() { Err(_) => Dump::Never, Ok("always") => Dump::Always, Ok("error") => Dump::OnError, Ok(_) => Dump::Never }; StandardCache { inner: SyncCache::new(), dir: standard_fonts, fonts, dump, require_unique_unicode: false, } } #[cfg(feature="embed")] pub fn new() -> StandardCache { let ref data = EmbeddedStandardFonts::get("fonts.json").unwrap().data; let fonts: HashMap = serde_json::from_slice(&data).expect("fonts.json is invalid"); StandardCache { inner: SyncCache::new(), fonts, dir: EmbeddedStandardFonts, dump: Dump::Never, require_unique_unicode: false, } } pub fn require_unique_unicode(&mut self, r: bool) { self.require_unique_unicode = r; } } pub trait DirRead: Sized { fn read_file(&self, name: &str) -> Result>; fn sub_dir(&self, name: &str) -> Option; } impl DirRead for PathBuf { fn read_file(&self, name: &str) -> Result> { std::fs::read(self.join(name)).map_err(|e| e.into()).map(|d| d.into()) } fn sub_dir(&self, name: &str) -> Option { let sub = self.join(name); if sub.is_dir() { Some(sub) } else { None } } } #[cfg(feature="embed")] #[derive(rust_embed::Embed)] #[folder = "$STANDARD_FONTS"] pub struct EmbeddedStandardFonts; #[cfg(feature="embed")] impl DirRead for EmbeddedStandardFonts { fn read_file(&self, name: &str) -> Result> { EmbeddedStandardFonts::get(name).map(|f| f.data).ok_or_else(|| PdfError::Other { msg: "Filed {name:?} not embedded".into() }) } fn sub_dir(&self, name: &str) -> Option { None } } #[derive(Debug)] enum Dump { Never, OnError, Always } pub fn load_font(font_ref: &MaybeRef, resolve: &impl Resolve, cache: &StandardCache) -> Result> { let pdf_font = font_ref.clone(); debug!("loading {:?}", pdf_font); let font: FontRc = match pdf_font.embedded_data(resolve) { Some(Ok(data)) => { debug!("loading embedded font"); let font = font::parse(&data).map_err(|e| { PdfError::Other { msg: format!("Font Error: {:?}", e) } }); if matches!(cache.dump, Dump::Always) || (matches!(cache.dump, Dump::OnError) && font.is_err()) { let name = format!("font_{}", pdf_font.name.as_ref().map(|s| s.as_str()).unwrap_or("unnamed")); std::fs::write(&name, &data).unwrap(); println!("font dumped in {}", name); } FontRc::from(font?) } Some(Err(e)) => return Err(e), None => { debug!("no embedded font."); let name = match pdf_font.name { Some(ref name) => name.as_str(), None => return Ok(None) }; debug!("loading {name} instead"); match cache.fonts.get(name).or_else(|| cache.fonts.get("Arial")) { Some(file_name) => { let val = cache.inner.get(file_name.clone(), |_| { let data = match cache.dir.read_file(file_name) { Ok(data) => data, Err(e) => { warn!("can't open {} for {:?} {:?}", file_name, pdf_font.name, e); return None; } }; match font::parse(&data) { Ok(f) => Some(f.into()), Err(e) => { warn!("Font Error: {:?}", e); return None; } } }); match val { Some(f) => f, None => { return Ok(None); } } } None => { warn!("no font for {:?}", pdf_font.name); return Ok(None); } } } }; Ok(Some(FontEntry::build(font, pdf_font, None, resolve, cache.require_unique_unicode)?)) } ================================================ FILE: render/src/fontentry.rs ================================================ use std::collections::{HashMap, HashSet}; use font::{self, GlyphId, TrueTypeFont, CffFont, Type1Font, OpenTypeFont}; use glyphmatcher::FontDb; use itertools::Itertools; use pdf::encoding::BaseEncoding; use pdf::font::{Font as PdfFont, Widths, CidToGidMap}; use pdf::object::{Resolve, MaybeRef}; use pdf::error::PdfError; use pdf_encoding::{Encoding, glyphname_to_unicode}; use istring::SmallString; use crate::font::FontRc; pub struct FontEntry { pub font: FontRc, pub pdf_font: MaybeRef, pub cmap: HashMap)>, pub widths: Option, pub is_cid: bool, pub name: String, } impl FontEntry { pub fn build(font: FontRc, pdf_font: MaybeRef, font_db: Option<&FontDb>, resolve: &impl Resolve, require_unique_unicode: bool) -> Result { let mut is_cid = pdf_font.is_cid(); let name = match pdf_font.data { pdf::font::FontData::Type0(ref t0) => t0.descendant_fonts[0].name.as_ref(), _ => pdf_font.name.as_ref() }; let encoding = pdf_font.encoding().clone(); let base_encoding = encoding.as_ref().map(|e| &e.base); let to_unicode = t!(pdf_font.to_unicode(resolve).transpose()); let mut font_codepoints = None; let font_cmap = font.downcast_ref::().and_then(|ttf| ttf.cmap.as_ref()) .or_else(|| font.downcast_ref::().and_then(|otf| otf.cmap.as_ref())); let glyph_unicode: HashMap = { if let Some(type1) = font.downcast_ref::() { debug!("Font is Type1"); font_codepoints = Some(&type1.codepoints); type1.unicode_names().map(|(gid, s)| (gid, s.into())).collect() } else if let Some(cmap) = font_cmap { cmap.items().filter_map(|(cp, gid)| std::char::from_u32(cp).map(|c| (gid, c.into()))).collect() } else if let Some(cff) = font.downcast_ref::() { cff.unicode_map.iter().map(|(&u, &gid)| (GlyphId(gid as u32), u.into())).collect() } else { (0..font.num_glyphs()) .filter_map(|gid| std::char::from_u32(gid).map(|c| (GlyphId(gid), c.into()))) .collect() } }; debug!("to_unicode: {:?}", to_unicode); let build_map = || -> HashMap)> { if let Some(ref to_unicode) = to_unicode { let mut num1 = 0; // dbg!(font.encoding()); let mut map: HashMap<_, _> = to_unicode.iter().map(|(cid, s)| { let gid = font.gid_for_codepoint(cid as u32); if gid.is_some() { num1 += 1; } (cid, (gid.unwrap_or(GlyphId(cid as u32)), Some(s.into()))) }).collect(); if let Some(cff) = font.downcast_ref::() { let mut num2 = 0; let map2: HashMap<_, _> = to_unicode.iter().map(|(cid, s)| { let gid = cff.sid_map.get(&cid).map(|&n| GlyphId(n as u32)); if gid.is_some() { num2 += 1; } (cid, (gid.unwrap_or(GlyphId(cid as u32)), Some(s.into()))) }).collect(); if num2 > num1 { map = map2; } } map } else if let Some(cmap) = font_cmap { cmap.items().map(|(cid, gid)| (cid as u16, (gid, None))).collect() } else if let Some(cff) = font.downcast_ref::() { if cff.cid { cff.sid_map.iter().map(|(&sid, &gid)| (sid as u16, (GlyphId(gid as u32), None))).collect() } else { cff.codepoint_map.iter().enumerate().filter(|&(_, &gid)| gid != 0).map(|(cid, &gid)| (cid as u16, (GlyphId(gid as u32), None))).collect() } } else { Default::default() } }; let mut cmap = if let Some(map) = pdf_font.cid_to_gid_map() { is_cid = true; debug!("gid to cid map: {:?}", map); match map { CidToGidMap::Identity => { let mut map: HashMap<_, _> = (0 .. font.num_glyphs()).map(|n| (n as u16, (GlyphId(n as u32), None))).collect(); if let Some(ref to_unicode) = to_unicode { for (cid, s) in to_unicode.iter() { if let Some((gid, uni)) = map.get_mut(&cid) { *uni = Some(s.into()); } } } map } CidToGidMap::Table(ref data) => { data.iter().enumerate().map(|(cid, &gid)| { let unicode = match to_unicode { Some(ref u) => u.get(cid as u16).map(|s| s.into()), None => glyph_unicode.get(&GlyphId(gid as u32)).cloned() }; (cid as u16, (GlyphId(gid as u32), unicode)) }).collect() } } } else if base_encoding == Some(&BaseEncoding::IdentityH) { is_cid = true; build_map() } else { let mut cmap = HashMap::)>::new(); let source_encoding = match base_encoding { Some(BaseEncoding::StandardEncoding) => Some(Encoding::AdobeStandard), Some(BaseEncoding::SymbolEncoding) => Some(Encoding::AdobeSymbol), Some(BaseEncoding::WinAnsiEncoding) => Some(Encoding::WinAnsiEncoding), Some(BaseEncoding::MacRomanEncoding) => Some(Encoding::MacRomanEncoding), Some(BaseEncoding::MacExpertEncoding) => Some(Encoding::AdobeExpert), ref e => { warn!("unsupported pdf encoding {:?}", e); None } }; let font_encoding = font.encoding(); debug!("{:?} -> {:?}", source_encoding, font_encoding); match (source_encoding, font_encoding) { (Some(source), Some(dest)) => { if let Some(transcoder) = source.to(dest) { let forward = source.forward_map().unwrap(); for b in 0 .. 256 { if let Some(gid) = transcoder.translate(b).and_then(|cp| font.gid_for_codepoint(cp)) { cmap.insert(b as u16, (gid, forward.get(b as u8).map(|c| c.into()))); //debug!("{} -> {:?}", b, gid); } } } }, (Some(enc), None) => { if let Some(encoder) = enc.to(Encoding::Unicode) { for b in 0 .. 256 { let unicode = encoder.translate(b as u32); if let Some(gid) = unicode.and_then(|c| font.gid_for_unicode_codepoint(c)) { cmap.insert(b, (gid, unicode.and_then(std::char::from_u32).map(|c| c.into()))); debug!("{} -> {:?}", b, gid); } } } } _ => { if let Some(cff) = font.downcast_ref::() { for (cp, &gid) in cff.codepoint_map.iter().enumerate() { let gid = GlyphId(gid as u32); let unicode = glyph_unicode.get(&gid).cloned(); cmap.insert(cp as u16, (gid, unicode)); } } else { warn!("can't translate from text encoding {:?} to font encoding {:?}", base_encoding, font_encoding); } // assuming same encoding } } if let Some(encoding) = encoding { for (&cp, name) in encoding.differences.iter() { let uni = glyphname_to_unicode(name); let gid = font.gid_for_name(&name).or_else(|| uni.and_then(|s| s.chars().next()).and_then(|cp| font.gid_for_unicode_codepoint(cp as u32)) ).or_else(|| font.gid_for_codepoint(cp) ).unwrap_or(GlyphId(cp)); let unicode = uni.map(|s| s.into()) .or_else(|| std::char::from_u32(0xf000 + gid.0).map(SmallString::from)); debug!("{} -> gid {:?}, unicode {:?}", cp, gid, unicode); cmap.insert(cp as u16, (gid, unicode)); } } else { if let Some(ref u) = to_unicode { debug!("using to_unicode to build cmap"); for (cp, unicode) in u.iter() { if let Some(gid) = font.gid_for_unicode_codepoint(cp as u32) { cmap.insert(cp as u16, (gid, Some(unicode.into()))); } } } else if let Some(codepoints) = font_codepoints { for (&cp, &gid) in codepoints.iter() { cmap.insert(cp as u16, (GlyphId(gid), glyph_unicode.get(&GlyphId(gid)).cloned())); } } else { debug!("assuming text has unicode codepoints"); for (&gid, unicode) in glyph_unicode.iter() { if let Some(cp) = unicode.chars().next() { cmap.insert(cp as u16, (gid, Some(unicode.clone()))); } } } } if cmap.len() == 0 { is_cid = true; build_map() } else { cmap } }; if let Some(font_db) = font_db { if let Some(name) = name { let ps_name = name.split("+").nth(1).unwrap_or(name); info!("request font {ps_name} ({})", name.as_str()); if let Some(map) = font_db.check_font(ps_name, &*font) { if map.len() > 0 { info!("Got good unicode map for {ps_name}"); } else { info!("font {ps_name} did not match"); } for (cp, (gid, uni)) in cmap.iter_mut() { let good_uni = map.get(gid); match (uni.as_mut(), good_uni) { (Some(uni), Some(good_uni)) if uni != good_uni => { // println!("mismatching unicode for gid {gid:?}: {good_uni:?} != {uni:?}"); *uni = good_uni.clone(); } (None, Some(good_uni)) => { // println!("missing unicode for gid {gid:?} added {good_uni:?}"); *uni = Some(good_uni.clone()); } (Some(uni), None) => { // println!("glyph {} missing (has uni {:?})", gid.0, uni); } _ => {} } } } else { info!("missing {ps_name} font"); } } } let widths = pdf_font.widths(resolve)?; let name = pdf_font.name.as_ref().ok_or_else(|| PdfError::Other { msg: "font has no name".into() })?.as_str().into(); if require_unique_unicode { let mut next_code = 0xE000; let mut by_gid: Vec<_> = cmap.values_mut().collect(); by_gid.sort_unstable_by_key(|t| t.0.0); let reserved_in_used: HashSet = by_gid.iter().map(|(gid, _)| gid.0).filter(|gid| (0xE000 .. 0xF800).contains(gid)).collect(); if reserved_in_used.len() > 0 { info!("gid in privated use area: {}", reserved_in_used.iter().format(", ")); } let mut rev_map = HashMap::new(); for (gid, uni_o) in by_gid.iter_mut() { if let Some(uni) = uni_o { use std::collections::hash_map::Entry; match rev_map.entry(uni.clone()) { Entry::Vacant(e) => { e.insert(*gid); } Entry::Occupied(e) => { info!("Duplicate unicode {uni:?} for {gid:?} and {:?}", e.get()); *uni_o = None; } } } } for (gid, uni) in by_gid.iter_mut() { if uni.is_none() && !font.is_empty_glyph(*gid) { *uni = Some(std::char::from_u32(next_code).unwrap().into()); next_code += 1; while reserved_in_used.contains(&next_code) { next_code += 1; } if next_code >= 0xF8000 { warn!("too many unmapped glpyhs in {:?}", font.name().postscript_name); break; } } } if next_code > 0xE000 { info!("mapped {} glyphs in private use area", next_code - 0xE000); } } Ok(FontEntry { font, pdf_font, cmap, is_cid, widths, name, }) } } impl globalcache::ValueSize for FontEntry { fn size(&self) -> usize { 1 // TODO } } ================================================ FILE: render/src/graphicsstate.rs ================================================ use pathfinder_content::stroke::StrokeStyle; use pathfinder_renderer::{paint::PaintId, scene::ClipPath}; use pdf::object::ColorSpace; use pathfinder_geometry::{transform2d::Transform2F, rect::RectF}; use crate::{Fill, backend::Stroke, Backend}; pub struct GraphicsState<'a, B: Backend> { pub transform: Transform2F, pub stroke_style: StrokeStyle, pub fill_color: Fill, pub fill_color_alpha: f32, pub fill_paint: Option, pub stroke_color: Fill, pub stroke_color_alpha: f32, pub stroke_paint: Option, pub clip_path_id: Option, pub clip_path: Option, pub clip_path_rect: Option, pub fill_color_space: &'a ColorSpace, pub stroke_color_space: &'a ColorSpace, pub dash_pattern: Option<(&'a [f32], f32)>, pub stroke_alpha: f32, pub fill_alpha: f32, pub overprint_fill: bool, pub overprint_stroke: bool, pub overprint_mode: i32, } impl<'a, B: Backend> Clone for GraphicsState<'a, B> { fn clone(&self) -> Self { GraphicsState { clip_path: self.clip_path.clone(), .. *self } } } impl<'a, B: Backend> GraphicsState<'a, B> { pub fn set_fill_color(&mut self, fill: Fill) { if fill != self.fill_color { self.fill_color = fill; self.fill_paint = None; } } pub fn set_fill_alpha(&mut self, alpha: f32) { let a = self.fill_alpha * alpha; if a != self.fill_color_alpha { self.fill_color_alpha = a; self.fill_paint = None; } } pub fn set_stroke_color(&mut self, fill: Fill) { if fill != self.stroke_color { self.stroke_color = fill; self.stroke_paint = None; } } pub fn set_stroke_alpha(&mut self, alpha: f32) { let a = self.stroke_alpha * alpha; if a != self.stroke_color_alpha { self.stroke_alpha = a; self.stroke_paint = None; } } pub fn stroke(&self) -> Stroke { Stroke { style: self.stroke_style, dash_pattern: self.dash_pattern.map(|(a, p)| (a.into(), p)) } } } ================================================ FILE: render/src/image.rs ================================================ use image::{RgbaImage, ImageBuffer, Rgba}; use pdf::object::*; use pdf::error::PdfError; use pathfinder_color::ColorU; use std::borrow::Cow; use std::path::Path; use std::sync::Arc; use crate::BlendMode; #[derive(Hash, PartialEq, Eq, Clone)] pub struct ImageData<'a> { data: Cow<'a, [ColorU]>, width: u32, height: u32, } impl<'a> ImageData<'a> { pub fn new(data: impl Into>, width: u32, height: u32) -> Option { let data = data.into(); if width as usize * height as usize != data.len() { return None; } Some(ImageData { data, width, height }) } pub fn width(&self) -> u32 { self.width } pub fn height(&self) -> u32 { self.height } pub fn data(&self) -> &[ColorU] { &*self.data } pub fn into_data(self) -> Cow<'a, [ColorU]> { self.data } pub fn rgba_data(&self) -> &[u8] { let ptr: *const ColorU = self.data.as_ptr(); let len = self.data.len(); unsafe { std::slice::from_raw_parts(ptr.cast(), 4 * len) } } /// angle must be in range 0 .. 4 pub fn rotate(&self, angle: u8) -> ImageData<'_> { match angle { 0 => ImageData { data: Cow::Borrowed(&*self.data), width: self.width, height: self.height }, 1 => { let mut data = Vec::with_capacity(self.data.len()); for y in 0 .. self.width as usize { for x in (0 .. self.height as usize).rev() { data.push(self.data[x * self.width as usize + y]); } } ImageData::new( data, self.height, self.width ).unwrap() } 2 => { let data: Vec = self.data.iter().rev().cloned().collect(); ImageData::new( data, self.width, self.height ).unwrap() } 3 => { let mut data = Vec::with_capacity(self.data.len()); for y in (0 .. self.width as usize).rev() { for x in 0 .. self.height as usize { data.push(self.data[x * self.width as usize + y]); } } ImageData::new( data, self.height, self.width ).unwrap() } _ => panic!("invalid rotation") } } pub fn safe(&self, path: &Path) { let data = self.rgba_data(); ImageBuffer::, &[u8]>::from_raw(self.width, self.height, data).unwrap().save(path).unwrap() } } fn resize_alpha(data: &[u8], src_width: u32, src_height: u32, dest_width: u32, dest_height: u32) -> Option> { use image::{ImageBuffer, imageops::{resize, FilterType}, Luma}; let src: ImageBuffer, &[u8]> = ImageBuffer::from_raw(src_width, src_height, data)?; let dest = resize(&src, dest_width, dest_height, FilterType::CatmullRom); Some(dest.into_raw()) } pub fn load_image(image: &ImageXObject, resources: &Resources, resolve: &impl Resolve, mode: BlendMode) -> Result, PdfError> { let raw_data = image.image_data(resolve)?; let pixel_count = image.width as usize * image.height as usize; if raw_data.len() % pixel_count != 0 { warn!("invalid data length {} bytes for {} pixels", raw_data.len(), pixel_count); info!("image: {:?}", image.inner.info.info); info!("filters: {:?}", image.inner.filters); } enum Data<'a> { Arc(Arc<[u8]>), Vec(Vec), Slice(&'a [u8]) } impl<'a> std::ops::Deref for Data<'a> { type Target = [u8]; fn deref(&self) -> &[u8] { match self { Data::Arc(ref d) => &**d, Data::Vec(ref d) => &*d, Data::Slice(s) => s } } } impl<'a> From> for Data<'a> { fn from(v: Vec) -> Self { Data::Vec(v) } } let mask = t!(image.smask.map(|r| resolve.get(r)).transpose()); let alpha = match mask { Some(ref mask) => { let data = Data::Arc(t!((**mask).data(resolve))); let mask_width = mask.width as usize; let mask_height = mask.height as usize; let bits_per_component = mask.bits_per_component.ok_or_else(|| PdfError::Other { msg: format!("no bits per component")})?; let bits = mask_width * mask_height * bits_per_component as usize; assert_eq!(data.len(), (bits + 7) / 8); let mut alpha: Data = match bits_per_component { 1 => data.iter().flat_map(|&b| (0..8).map(move |i| ex(b >> i, 1))).collect::>().into(), 2 => data.iter().flat_map(|&b| (0..4).map(move |i| ex(b >> 2*i, 2))).collect::>().into(), 4 => data.iter().flat_map(|&b| (0..2).map(move |i| ex(b >> 4*i, 4))).collect::>().into(), 8 => data, 12 => data.chunks_exact(3).flat_map(|c| [c[0], c[1] << 4 | c[2] >> 4]).collect::>().into(), 16 => data.chunks_exact(2).map(|c| c[0]).collect::>().into(), n => return Err(PdfError::Other { msg: format!("invalid bits per component {}", n)}) }; if mask.width != image.width || mask.height != image.height { alpha = resize_alpha(&*alpha, mask.width, mask.height, image.width, image.height).unwrap().into(); } alpha } None => Data::Slice(&[][..]) }; #[inline] fn ex(b: u8, bits: u8) -> u8 { b & ((1 << bits) - 1) } fn resolve_cs<'a>(cs: &'a ColorSpace, resources: &'a Resources) -> Option<&'a ColorSpace> { match cs { ColorSpace::Icc(icc) => { match icc.info.alternate { Some(ref b) => Some(&**b), None => match icc.info.components { 1 => Some(&ColorSpace::DeviceGray), 3 => Some(&ColorSpace::DeviceRGB), 4 => Some(&ColorSpace::DeviceCMYK), _ => None } } } ColorSpace::Named(ref name) => resources.color_spaces.get(name), _ => Some(cs), } } let cs = image.color_space.as_ref().and_then(|cs| resolve_cs(cs, &resources)); let alpha = alpha.iter().cloned().chain(std::iter::repeat(255)); let data_ratio = (raw_data.len() * 8) / pixel_count; // dbg!(data_ratio); debug!("CS: {cs:?}"); let data = match data_ratio { 1 | 2 | 4 | 8 => { let pixel_data: Cow<[u8]> = match data_ratio { 1 => raw_data.iter().flat_map(|&b| (0..8).map(move |i| ex(b >> i, 1))).take(pixel_count).collect::>().into(), 2 => raw_data.iter().flat_map(|&b| (0..4).map(move |i| ex(b >> 2*i, 2))).take(pixel_count).collect::>().into(), 4 => raw_data.iter().flat_map(|&b| (0..2).map(move |i| ex(b >> 4*i, 4))).take(pixel_count).collect::>().into(), 8 => Cow::Borrowed(&raw_data[..pixel_count]), n => return Err(PdfError::Other { msg: format!("invalid bits per component {}", n)}) }; let pixel_data: &[u8] = &*pixel_data; // dbg!(&cs); match cs { Some(&ColorSpace::DeviceGray) => { assert_eq!(pixel_data.len(), pixel_count); pixel_data.iter().zip(alpha).map(|(&g, a)| ColorU { r: g, g: g, b: g, a }).collect() } Some(&ColorSpace::Indexed(ref base, hival, ref lookup)) => { match resolve_cs(&**base, resources) { Some(ColorSpace::DeviceRGB) => { let mut data = Vec::with_capacity(pixel_data.len()); for (&b, a) in pixel_data.iter().zip(alpha) { let off = b as usize * 3; let c = lookup.get(off .. off + 3).ok_or(PdfError::Bounds { index: off, len: lookup.len() })?; data.push(rgb2rgba(c, a, mode)); } data } Some(ColorSpace::DeviceCMYK) => { debug!("indexed CMYK {}", lookup.len()); let mut data = Vec::with_capacity(pixel_data.len()); for (&b, a) in pixel_data.iter().zip(alpha) { let off = b as usize * 4; let c = lookup.get(off .. off + 4).ok_or(PdfError::Bounds { index: off, len: lookup.len() })?; data.push(cmyk2color(c.try_into().unwrap(), a, BlendMode::Darken)); } data } _ => unimplemented!("base cs={:?}", base), } } Some(&ColorSpace::Separation(_, ref alt, ref func)) => { let mut lut = [[0u8; 3]; 256]; match resolve_cs(alt, resources) { Some(ColorSpace::DeviceRGB) => { for (i, rgb) in lut.iter_mut().enumerate() { let mut c = [0.; 3]; func.apply(&[i as f32 / 255.], &mut c)?; let [r, g, b] = c; *rgb = rgb2rgb(r, g, b, mode); } } Some(ColorSpace::DeviceCMYK) => { for (i, rgb) in lut.iter_mut().enumerate() { let mut c = [0.; 4]; func.apply(&[i as f32 / 255.], &mut c)?; let [c, m, y, k] = c; *rgb = cmyk2rgb([(c * 255.) as u8, (m * 255.) as u8, (y * 255.) as u8, (k * 255.) as u8], mode); } } _ => unimplemented!("alt cs={:?}", alt), } pixel_data.iter().zip(alpha).map(|(&b, a)| { let [r, g, b] = lut[b as usize]; ColorU { r, g, b, a } }).collect() } None => { info!("image has data/pixel ratio of 1, but no colorspace"); assert_eq!(pixel_data.len(), pixel_count); pixel_data.iter().zip(alpha).map(|(&g, a)| ColorU { r: g, g: g, b: g, a }).collect() } _ => unimplemented!("cs={:?}", cs), } } 24 => { if !matches!(cs, Some(ColorSpace::DeviceRGB)) { info!("image has data/pixel ratio of 3, but colorspace is {:?}", cs); } raw_data[..pixel_count * 3].chunks_exact(3).zip(alpha).map(|(c, a)| rgb2rgba(c, a, mode)).collect() } 32 => { if !matches!(cs, Some(ColorSpace::DeviceCMYK)) { info!("image has data/pixel ratio of 4, but colorspace is {:?}", cs); } cmyk2color_arr(&raw_data[..pixel_count * 4], alpha, mode) } _ => unimplemented!("data/pixel ratio {}", data_ratio), }; let data_len = data.len(); match ImageData::new(data, image.width as u32, image.height as u32) { Some(data) => Ok(data), None => { warn!("image width: {}", image.width); warn!("image height: {}", image.height); warn!("data.len(): {}", data_len); warn!("data_ratio: {data_ratio}"); Err(PdfError::Other { msg: "size mismatch".into() }) } } } fn rgb2rgba(c: &[u8], a: u8, mode: BlendMode) -> ColorU { match mode { BlendMode::Overlay => { ColorU { r: c[0], g: c[1], b: c[2], a } } BlendMode::Darken => { ColorU { r: 255 - c[0], g: 255 - c[1], b: 255 - c[2], a } } } } fn rgb2rgb(r: f32, g: f32, b: f32, mode: BlendMode) -> [u8; 3] { match mode { BlendMode::Overlay => { [ (255. * r) as u8, (255. * g) as u8, (255. * b) as u8 ] } BlendMode::Darken => { [ 255 - (255. * r) as u8, 255 - (255. * g) as u8, 255 - (255. * b) as u8 ] } } } /* red = 1.0 – min ( 1.0, cyan + black ) green = 1.0 – min ( 1.0, magenta + black ) blue = 1.0 – min ( 1.0, yellow + black ) */ #[inline] fn cmyk2rgb([c, m, y, k]: [u8; 4], mode: BlendMode) -> [u8; 3] { match mode { BlendMode::Darken => { let r = 255 - c.saturating_add(k); let g = 255 - m.saturating_add(k); let b = 255 - y.saturating_add(k); [r, g, b] } BlendMode::Overlay => { let (c, m, y, k) = (255 - c, 255 - m, 255 - y, 255 - k); let r = 255 - c.saturating_add(k); let g = 255 - m.saturating_add(k); let b = 255 - y.saturating_add(k); [r, g, b] } } } #[inline] fn cmyk2color(cmyk: [u8; 4], a: u8, mode: BlendMode) -> ColorU { let [r, g, b] = cmyk2rgb(cmyk, mode); ColorU::new(r, g, b, a) } fn cmyk2color_arr(data: &[u8], alpha: impl Iterator, mode: BlendMode) -> Vec { data.chunks_exact(4).zip(alpha).map(|(c, a)| { let mut buf = [0; 4]; buf.copy_from_slice(c); cmyk2color(buf, a, mode) }).collect() } ================================================ FILE: render/src/lib.rs ================================================ #[macro_use] extern crate log; #[macro_use] extern crate pdf; macro_rules! assert_eq { ($a:expr, $b:expr) => { if $a != $b { return Err(pdf::error::PdfError::Other { msg: format!("{} ({}) != {} ({})", stringify!($a), $a, stringify!($b), $b)}); } }; } macro_rules! unimplemented { ($msg:tt $(, $arg:expr)*) => { return Err(pdf::error::PdfError::Other { msg: format!(concat!("Unimplemented: ", $msg) $(, $arg)*) }) }; } mod cache; mod fontentry; mod graphicsstate; mod renderstate; mod textstate; mod backend; pub mod tracer; mod image; mod scene; mod font; pub use cache::{Cache}; pub use fontentry::{FontEntry}; pub use backend::{DrawMode, Backend, BlendMode, FillMode}; pub use scene::SceneBackend; pub use crate::image::{load_image, ImageData}; use custom_debug_derive::Debug; use pdf::{object::*, content::TextMode}; use pdf::error::PdfError; use pathfinder_geometry::{ vector::{Vector2F}, rect::RectF, transform2d::Transform2F, }; use renderstate::RenderState; use std::sync::Arc; use itertools::Itertools; const SCALE: f32 = 25.4 / 72.; #[derive(Copy, Clone, Default)] pub struct BBox(Option); impl BBox { pub fn empty() -> Self { BBox(None) } pub fn add(&mut self, r2: RectF) { self.0 = Some(match self.0 { Some(r1) => r1.union_rect(r2), None => r2 }); } pub fn add_bbox(&mut self, bb: Self) { if let Some(r) = bb.0 { self.add(r); } } pub fn rect(self) -> Option { self.0 } } impl From for BBox { fn from(r: RectF) -> Self { BBox(Some(r)) } } pub fn page_bounds(page: &Page) -> RectF { let Rect { left, right, top, bottom } = page.media_box().expect("no media box"); RectF::from_points(Vector2F::new(left, bottom), Vector2F::new(right, top)) * SCALE } pub fn render_page(backend: &mut impl Backend, resolve: &impl Resolve, page: &Page, transform: Transform2F) -> Result { let bounds = page_bounds(page); let rotate = Transform2F::from_rotation(page.rotate as f32 * std::f32::consts::PI / 180.); let br = rotate * RectF::new(Vector2F::zero(), bounds.size()); let translate = Transform2F::from_translation(Vector2F::new( -br.min_x().min(br.max_x()), -br.min_y().min(br.max_y()), )); let view_box = transform * translate * br; backend.set_view_box(view_box); let root_transformation = transform * translate * rotate * Transform2F::row_major(SCALE, 0.0, -bounds.min_x(), 0.0, -SCALE, bounds.max_y()); let resources = t!(page.resources()); let contents = try_opt!(page.contents.as_ref()); let ops = contents.operations(resolve)?; let mut renderstate = RenderState::new(backend, resolve, &resources, root_transformation); for (i, op) in ops.iter().enumerate() { debug!("op {}: {:?}", i, op); renderstate.draw_op(op, i)?; } Ok(root_transformation) } pub fn render_pattern(backend: &mut impl Backend, pattern: &Pattern, resolve: &impl Resolve) -> Result<(), PdfError> { match pattern { Pattern::Stream(ref dict, ref ops) => { let resources = resolve.get(dict.resources)?; let mut renderstate = RenderState::new(backend, resolve, &*resources, Transform2F::default()); for (i, op) in ops.iter().enumerate() { debug!("op {}: {:?}", i, op); renderstate.draw_op(op, i)?; } } Pattern::Dict(_) => {} } Ok(()) } #[derive(Copy, Clone, PartialEq, Debug)] pub enum Fill { Solid(f32, f32, f32), Pattern(Ref), } impl Fill { pub fn black() -> Self { Fill::Solid(0., 0., 0.) } } #[derive(Debug)] pub struct TextSpan { // A rect with the origin at the baseline, a height of 1em and width that corresponds to the advance width. pub rect: RectF, // width in textspace units (before applying transform) pub width: f32, // Bounding box of the rendered outline pub bbox: Option, pub font_size: f32, #[debug(skip)] pub font: Option>, pub text: String, pub chars: Vec, pub color: Fill, pub alpha: f32, // apply this transform to a text draw in at the origin with the given width and font-size pub transform: Transform2F, pub mode: TextMode, pub op_nr: usize, } impl TextSpan { pub fn parts(&self) -> impl Iterator + '_ { self.chars.iter().cloned() .chain(std::iter::once(TextChar { offset: self.text.len(), pos: self.width, width: 0.0 })) .tuple_windows() .map(|(a, b)| Part { text: &self.text[a.offset..b.offset], pos: a.pos, width: a.width, offset: a.offset }) } pub fn rparts(&self) -> impl Iterator + '_ { self.chars.iter().cloned() .chain(std::iter::once(TextChar { offset: self.text.len(), pos: self.width, width: 0.0 })).rev() .tuple_windows() .map(|(b, a)| Part { text: &self.text[a.offset..b.offset], pos: a.pos, width: a.width, offset: a.offset }) } } pub struct Part<'a> { pub text: &'a str, pub pos: f32, pub width: f32, pub offset: usize, } #[derive(Debug, Clone, Copy)] pub struct TextChar { pub offset: usize, pub pos: f32, pub width: f32, } ================================================ FILE: render/src/renderstate.rs ================================================ use pathfinder_content::outline::ContourIterFlags; use pathfinder_renderer::scene::ClipPath; use pdf::object::*; use pdf::primitive::{Primitive, Dictionary}; use pdf::content::{Op, Matrix, Point, Rect, Color, Rgb, Cmyk, Winding, FormXObject}; use pdf::error::{PdfError, Result}; use pdf::content::TextDrawAdjusted; use crate::backend::{Backend, BlendMode, Stroke, FillMode}; use pathfinder_geometry::{ vector::Vector2F, rect::RectF, transform2d::Transform2F, }; use pathfinder_content::{ fill::FillRule, stroke::{LineCap, LineJoin, StrokeStyle}, outline::{Outline, Contour}, }; use super::{ graphicsstate::GraphicsState, textstate::{TextState, Span}, DrawMode, TextSpan, Fill, }; trait Cvt { type Out; fn cvt(self) -> Self::Out; } impl Cvt for Point { type Out = Vector2F; fn cvt(self) -> Self::Out { Vector2F::new(self.x, self.y) } } impl Cvt for Matrix { type Out = Transform2F; fn cvt(self) -> Self::Out { let Matrix { a, b, c, d, e, f } = self; Transform2F::row_major(a, c, e, b, d, f) } } impl Cvt for Rect { type Out = RectF; fn cvt(self) -> Self::Out { RectF::new( Vector2F::new(self.x, self.y), Vector2F::new(self.width, self.height) ) } } impl Cvt for Winding { type Out = FillRule; fn cvt(self) -> Self::Out { match self { Winding::NonZero => FillRule::Winding, Winding::EvenOdd => FillRule::EvenOdd } } } impl Cvt for Rgb { type Out = (f32, f32, f32); fn cvt(self) -> Self::Out { let Rgb { red, green, blue } = self; (red, green, blue) } } impl Cvt for Cmyk { type Out = (f32, f32, f32, f32); fn cvt(self) -> Self::Out { let Cmyk { cyan, magenta, yellow, key } = self; (cyan, magenta, yellow, key) } } pub struct RenderState<'a, R: Resolve, B: Backend> { graphics_state: GraphicsState<'a, B>, text_state: TextState, stack: Vec<(GraphicsState<'a, B>, TextState)>, current_outline: Outline, current_contour: Contour, resolve: &'a R, resources: &'a Resources, backend: &'a mut B, } impl<'a, R: Resolve, B: Backend> RenderState<'a, R, B> { pub fn new(backend: &'a mut B, resolve: &'a R, resources: &'a Resources, root_transformation: Transform2F) -> Self { let graphics_state = GraphicsState { transform: root_transformation, fill_color: Fill::black(), fill_color_alpha: 1.0, fill_paint: None, fill_alpha: 1.0, stroke_color: Fill::black(), stroke_color_alpha: 1.0, stroke_paint: None, stroke_alpha: 1.0, clip_path_id: None, clip_path: None, clip_path_rect: None, fill_color_space: &ColorSpace::DeviceRGB, stroke_color_space: &ColorSpace::DeviceRGB, stroke_style: StrokeStyle { line_cap: LineCap::Butt, line_join: LineJoin::Miter(1.0), line_width: 1.0, }, dash_pattern: None, overprint_fill: false, overprint_stroke: false, overprint_mode: 0, }; let text_state = TextState::new(); let stack = vec![]; let current_outline = Outline::new(); let current_contour = Contour::new(); RenderState { graphics_state, text_state, stack, current_outline, current_contour, resources, resolve, backend, } } fn draw(&mut self, mode: &DrawMode, fill_rule: FillRule) { self.flush(); self.backend.draw(&self.current_outline, mode, fill_rule, self.graphics_state.transform, self.graphics_state.clip_path_id); self.current_outline.clear(); } #[allow(unused_variables)] pub fn draw_op(&mut self, op: &'a Op, op_nr: usize) -> Result<()> { self.backend.inspect_op(op); self.backend.bug_op(op_nr); match *op { Op::BeginMarkedContent { .. } => {} Op::EndMarkedContent { .. } => {} Op::MarkedContentPoint { .. } => {} Op::Close => { self.current_contour.close(); } Op::MoveTo { p } => { self.flush(); self.current_contour.push_endpoint(p.cvt()); }, Op::LineTo { p } => { self.current_contour.push_endpoint(p.cvt()); }, Op::CurveTo { c1, c2, p } => { self.current_contour.push_cubic(c1.cvt(), c2.cvt(), p.cvt()); }, Op::Rect { rect } => { self.flush(); self.current_outline.push_contour(Contour::from_rect(rect.cvt())); }, Op::EndPath => { self.current_contour.clear(); self.current_outline.clear(); } Op::Stroke => { self.draw(&DrawMode::Stroke { stroke: FillMode { color: self.graphics_state.stroke_color, alpha: self.graphics_state.stroke_color_alpha, mode: self.blend_mode_stroke(), }, stroke_mode: self.graphics_state.stroke()}, FillRule::Winding ); }, Op::FillAndStroke { winding } => { self.draw(&DrawMode::FillStroke { fill: FillMode { color: self.graphics_state.fill_color, alpha: self.graphics_state.fill_color_alpha, mode: self.blend_mode_fill(), }, stroke: FillMode { color: self.graphics_state.stroke_color, alpha: self.graphics_state.stroke_color_alpha, mode: self.blend_mode_stroke() }, stroke_mode: self.graphics_state.stroke() }, winding.cvt()); } Op::Fill { winding } => { self.draw(&DrawMode::Fill { fill: FillMode { color: self.graphics_state.fill_color, alpha: self.graphics_state.fill_color_alpha, mode: self.blend_mode_fill(), }, }, winding.cvt()); } Op::Shade { ref name } => {}, Op::Clip { winding } => { self.flush(); let mut path = self.current_outline.clone().transformed(&self.graphics_state.transform); let clip_path_rect = to_rect(&path); let (path, r, parent) = match (self.graphics_state.clip_path_rect, clip_path_rect, self.graphics_state.clip_path_id) { (Some(r1), Some(r2), Some(p)) => { let r = r1.intersection(r2).unwrap_or_default(); (Outline::from_rect(r), Some(r), None) } (Some(r), None, Some(p)) => { path.clip_against_polygon(&[r.origin(), r.upper_right(), r.lower_right(), r.lower_left()]); (path, None, None) } (None, Some(r), Some(p)) => { let mut path = self.graphics_state.clip_path.as_ref().unwrap().outline.clone(); path.clip_against_polygon(&[r.origin(), r.upper_right(), r.lower_right(), r.lower_left()]); (path, None, None) } (None, Some(r), None) => { (path, Some(r), None) } (None, None, Some(p)) => (path, None, Some(p)), (None, None, None) => (path, None, None), _ => unreachable!() }; let id = self.backend.create_clip_path(path.clone(), winding.cvt(), parent); self.graphics_state.clip_path_id = Some(id); let mut clip = ClipPath::new(path); clip.set_fill_rule(winding.cvt()); self.graphics_state.clip_path = Some(clip); self.graphics_state.clip_path_rect = r; }, Op::Save => { self.stack.push((self.graphics_state.clone(), self.text_state.clone())); }, Op::Restore => { let (g, t) = self.stack.pop().ok_or_else(|| pdf::error::PdfError::Other { msg: "graphcs stack is empty".into() })?; self.graphics_state = g; self.text_state = t; }, Op::Transform { matrix } => { self.graphics_state.transform = self.graphics_state.transform * matrix.cvt(); } Op::LineWidth { width } => self.graphics_state.stroke_style.line_width = width, Op::Dash { ref pattern, phase } => self.graphics_state.dash_pattern = Some((&*pattern, phase)), Op::LineJoin { join } => {}, Op::LineCap { cap } => {}, Op::MiterLimit { limit } => {}, Op::Flatness { tolerance } => {}, Op::GraphicsState { ref name } => { let gs = try_opt!(self.resources.graphics_states.get(name)); debug!("GS: {gs:?}"); if let Some(lw) = gs.line_width { self.graphics_state.stroke_style.line_width = lw; } self.graphics_state.set_fill_alpha(gs.fill_alpha.unwrap_or(1.0)); self.graphics_state.set_stroke_alpha(gs.stroke_alpha.unwrap_or(1.0)); if let Some((font_ref, size)) = gs.font { let font = self.resolve.get(font_ref)?; if let Some(e) = self.backend.get_font(&MaybeRef::Indirect(font), self.resolve)? { debug!("new font: {} at size {}", e.name, size); self.text_state.font_entry = Some(e); self.text_state.font_size = size; } else { self.text_state.font_entry = None; } } if let Some(op) = gs.overprint { self.graphics_state.overprint_fill = op; self.graphics_state.overprint_stroke = op; } if let Some(op) = gs.overprint_fill { self.graphics_state.overprint_fill = op; } if let Some(m) = gs.overprint_mode { self.graphics_state.overprint_mode = m; } }, Op::StrokeColor { ref color } => { let mode = self.blend_mode_stroke(); let color = t!(convert_color(&mut self.graphics_state.stroke_color_space, color, &self.resources, self.resolve, mode)); self.graphics_state.set_stroke_color(color); }, Op::FillColor { ref color } => { let mode = self.blend_mode_fill(); let color = t!(convert_color(&mut self.graphics_state.fill_color_space, color, &self.resources, self.resolve, mode)); self.graphics_state.set_fill_color(color); }, Op::FillColorSpace { ref name } => { self.graphics_state.fill_color_space = self.color_space(name)?; self.graphics_state.set_fill_color(Fill::black()); }, Op::StrokeColorSpace { ref name } => { self.graphics_state.stroke_color_space = self.color_space(name)?; self.graphics_state.set_stroke_color(Fill::black()); }, Op::RenderingIntent { intent } => {}, Op::BeginText => self.text_state.reset_matrix(), Op::EndText => {}, Op::CharSpacing { char_space } => self.text_state.char_space = char_space, Op::WordSpacing { word_space } => self.text_state.word_space = word_space, Op::TextScaling { horiz_scale } => self.text_state.horiz_scale = 0.01 * horiz_scale, Op::Leading { leading } => self.text_state.leading = leading, Op::TextFont { ref name, size } => { let font = match self.resources.fonts.get(name) { Some(font_ref) => { let font = font_ref.load(self.resolve)?; self.backend.get_font(&font, self.resolve)? }, None => None }; if let Some(e) = font { debug!("new font: {} (is_cid={:?})", e.name, e.is_cid); self.text_state.font_entry = Some(e); self.text_state.font_size = size; } else { info!("no font {}", name); self.text_state.font_entry = None; } }, Op::TextRenderMode { mode } => self.text_state.mode = mode, Op::TextRise { rise } => self.text_state.rise = rise, Op::MoveTextPosition { translation } => self.text_state.translate(translation.cvt()), Op::SetTextMatrix { matrix } => self.text_state.set_matrix(matrix.cvt()), Op::TextNewline => self.text_state.next_line(), Op::TextDraw { ref text } => { let fill_mode = self.blend_mode_fill(); let stroke_mode = self.blend_mode_stroke(); self.text(|backend, text_state, graphics_state, span| { text_state.draw_text(backend, graphics_state, &text.data, span, fill_mode, stroke_mode); }, op_nr); }, Op::TextDrawAdjusted { ref array } => { let fill_mode = self.blend_mode_fill(); let stroke_mode = self.blend_mode_stroke(); self.text(|backend, text_state, graphics_state, span| { for arg in array { match *arg { TextDrawAdjusted::Text(ref data) => { text_state.draw_text(backend, graphics_state, data.as_bytes(), span, fill_mode, stroke_mode); }, TextDrawAdjusted::Spacing(offset) => { // because why not PDF… let advance = text_state.advance(-0.001 * offset); span.width += advance; } } } }, op_nr); }, Op::XObject { ref name } => { let &xobject_ref = self.resources.xobjects.get(name).ok_or(PdfError::NotFound { word: name.as_str().into()})?; let xobject = self.resolve.get(xobject_ref)?; let mode = self.blend_mode_fill(); match *xobject { XObject::Image(ref im) => { self.backend.draw_image(xobject_ref, im, self.resources, self.graphics_state.transform, mode, self.graphics_state.clip_path_id, self.resolve); } XObject::Form(ref content) => { self.draw_form(content)?; } XObject::Postscript(ref ps) => { let data = ps.data(self.resolve)?; self.backend.bug_postscript(&data); warn!("Got PostScript?!"); } } }, Op::InlineImage { ref image } => { let mode = self.blend_mode_fill(); self.backend.draw_inline_image(image, &self.resources, self.graphics_state.transform, mode, self.graphics_state.clip_path_id, self.resolve); } } Ok(()) } fn blend_mode_fill(&self) -> BlendMode { if self.graphics_state.overprint_fill { BlendMode::Darken } else { BlendMode::Overlay } } fn blend_mode_stroke(&self) -> BlendMode { if self.graphics_state.overprint_stroke { BlendMode::Darken } else { BlendMode::Overlay } } fn text(&mut self, inner: impl FnOnce(&mut B, &mut TextState, &mut GraphicsState, &mut Span), op_nr: usize) { let mut span = Span::default(); let tm = self.text_state.text_matrix; let origin = tm.translation(); inner(&mut self.backend, &mut self.text_state, &mut self.graphics_state, &mut span); let transform = self.graphics_state.transform * tm * Transform2F::from_scale(Vector2F::new(1.0, -1.0)); let p1 = origin; let p2 = (tm * Transform2F::from_translation(Vector2F::new(span.width, self.text_state.font_size))).translation(); let clip = self.graphics_state.clip_path_id; debug!("text {}", span.text); self.backend.add_text(TextSpan { rect: self.graphics_state.transform * RectF::from_points(p1.min(p2), p1.max(p2)), width: span.width, bbox: span.bbox.rect(), text: span.text, chars: span.chars, font: self.text_state.font_entry.clone(), font_size: self.text_state.font_size, color: self.graphics_state.fill_color, alpha: self.graphics_state.fill_color_alpha, mode: self.text_state.mode, transform, op_nr }, clip); } fn color_space(&self, name: &str) -> Result<&'a ColorSpace> { match name { "DeviceGray" => return Ok(&ColorSpace::DeviceGray), "DeviceRGB" => return Ok(&ColorSpace::DeviceRGB), "DeviceCMYK" => return Ok(&ColorSpace::DeviceCMYK), "Pattern" => return Ok(&ColorSpace::Pattern), _ => {} } match self.resources.color_spaces.get(name) { Some(cs) => Ok(cs), None => Err(PdfError::Other { msg: format!("color space {:?} not present", name) }) } } fn flush(&mut self) { if !self.current_contour.is_empty() { self.current_outline.push_contour(self.current_contour.clone()); self.current_contour.clear(); } } fn draw_form(&mut self, form: &FormXObject) -> Result<()> { let graphics_state = GraphicsState { stroke_alpha: self.graphics_state.stroke_color_alpha, fill_alpha: self.graphics_state.fill_color_alpha, clip_path_id: self.graphics_state.clip_path_id, clip_path: self.graphics_state.clip_path.clone(), .. self.graphics_state }; let resources = match form.dict().resources { Some(ref r) => &*r, None => self.resources }; let mut inner = RenderState { graphics_state: graphics_state, text_state: self.text_state.clone(), resources, stack: vec![], current_outline: Outline::new(), current_contour: Contour::new(), backend: self.backend, resolve: self.resolve, }; let ops = t!(form.operations(self.resolve)); for (i, op) in ops.iter().enumerate() { debug!(" form op {}: {:?}", i, op); inner.draw_op(op, i)?; } Ok(()) } #[allow(dead_code)] fn get_properties<'b>(&'b self, p: &'b Primitive) -> Result<&'b Dictionary> { match p { Primitive::Dictionary(ref dict) => Ok(dict), Primitive::Name(ref name) => self.resources.properties.get(name.as_str()) .map(|rc| &**rc) .ok_or_else(|| { PdfError::MissingEntry { typ: "Properties", field: name.into() } }), p => Err(PdfError::UnexpectedPrimitive { expected: "Dictionary or Name", found: p.get_debug_name() }) } } } fn convert_color<'a>(cs: &mut &'a ColorSpace, color: &Color, resources: &Resources, resolve: &impl Resolve, mode: BlendMode) -> Result { match convert_color2(cs, color, resources, mode) { Ok(color) => Ok(color), Err(e) if resolve.options().allow_error_in_option => { warn!("failed to convert color: {:?}", e); Ok(Fill::Solid(0.0, 0.0, 0.0)) } Err(e) => Err(e) } } #[allow(unused_variables)] fn convert_color2<'a>(cs: &mut &'a ColorSpace, color: &Color, resources: &Resources, mode: BlendMode) -> Result { match *color { Color::Gray(g) => { *cs = &ColorSpace::DeviceGray; Ok(gray2rgb(g)) } Color::Rgb(rgb) => { *cs = &ColorSpace::DeviceRGB; let (r, g, b) = rgb.cvt(); Ok(Fill::Solid(r, g, b)) } Color::Cmyk(cmyk) => { *cs = &ColorSpace::DeviceCMYK; Ok(cmyk2rgb(cmyk.cvt(), mode)) } Color::Other(ref args) => { let cs = match **cs { ColorSpace::Icc(ref icc) => { match icc.info.alternate { Some(ref alt) => alt, None => { match args.len() { 3 => &ColorSpace::DeviceRGB, 4 => &ColorSpace::DeviceCMYK, _ => return Err(PdfError::Other { msg: format!("ICC profile without alternate color space") }) } } } } ColorSpace::Named(ref name) => { resources.color_spaces.get(name).ok_or_else(|| PdfError::Other { msg: format!("named color space {} not found", name) } )? } _ => &**cs }; match *cs { ColorSpace::Icc(_) => return Err(PdfError::Other { msg: format!("nested ICC color space") }), ColorSpace::DeviceGray | ColorSpace::CalGray(_) => { if args.len() != 1 { return Err(PdfError::Other { msg: format!("expected 1 color arguments, got {:?}", args) }); } let g = args[0].as_number()?; Ok(gray2rgb(g)) } ColorSpace::DeviceRGB | ColorSpace::CalRGB(_) => { if args.len() != 3 { return Err(PdfError::Other { msg: format!("expected 3 color arguments, got {:?}", args) }); } let r = args[0].as_number()?; let g = args[1].as_number()?; let b = args[2].as_number()?; Ok(Fill::Solid(r, g, b)) } ColorSpace::DeviceCMYK | ColorSpace::CalCMYK(_) => { if args.len() != 4 { return Err(PdfError::Other { msg: format!("expected 4 color arguments, got {:?}", args) }); } let c = args[0].as_number()?; let m = args[1].as_number()?; let y = args[2].as_number()?; let k = args[3].as_number()?; Ok(cmyk2rgb((c, m, y, k), mode)) } ColorSpace::DeviceN { ref names, ref alt, ref tint, ref attr } => { assert_eq!(args.len(), tint.input_dim()); let mut input = vec![0.; args.len()]; for (i, a) in input.iter_mut().zip(args.iter()) { *i = a.as_number()?; } let mut out = vec![0.0; tint.output_dim()]; tint.apply(&input, &mut out)?; let alt = match **alt { ColorSpace::Icc(ref icc) => icc.info.alternate.as_ref().map(|b| &**b), ref a => Some(a), }; match alt { Some(ColorSpace::DeviceGray) => Ok(Fill::Solid(out[0], out[0], out[0])), Some(ColorSpace::DeviceRGB) => { Ok(Fill::Solid(out[0], out[1], out[2])) } Some(ColorSpace::DeviceCMYK) => { Ok(cmyk2rgb((out[0], out[1], out[2], out[3]), mode)) } _ => unimplemented!("DeviceN colorspace") } } ColorSpace::Separation(ref name, ref alt, ref f) => { debug!("Separation(name={}, alt={:?}, f={:?}", name, alt, f); if args.len() != 1 { return Err(PdfError::Other { msg: format!("expected 1 color arguments, got {:?}", args) }); } let x = args[0].as_number()?; let cs = match **alt { ColorSpace::Icc(ref info) => &**info.alternate.as_ref().ok_or( PdfError::Other { msg: format!("no alternate color space in ICC profile {:?}", info) } )?, _ => alt, }; match cs { &ColorSpace::DeviceCMYK => { let mut cmyk = [0.0; 4]; f.apply(&[x], &mut cmyk)?; let [c, m, y, k] = cmyk; //debug!("c={c}, m={m}, y={y}, k={k}"); Ok(cmyk2rgb((c, m, y, k), mode)) }, &ColorSpace::DeviceRGB => { let mut rgb = [0.0, 0.0, 0.0]; f.apply(&[x], &mut rgb)?; let [r, g, b] = rgb; //debug!("r={r}, g={g}, b={b}"); Ok(Fill::Solid(r, g, b)) }, &ColorSpace::DeviceGray => { let mut gray = [0.0]; f.apply(&[x], &mut gray)?; let [gray] = gray; //debug!("gray={gray}"); Ok(Fill::Solid(gray, gray, gray)) } c => unimplemented!("Separation(alt={:?})", c) } } ColorSpace::Indexed(ref cs, hival, ref lut) => { if args.len() != 1 { return Err(PdfError::Other { msg: format!("expected 1 color arguments, got {:?}", args) }); } let i = args[0].as_integer()?; match **cs { ColorSpace::DeviceRGB => { let c = &lut[3 * i as usize ..]; let cvt = |b: u8| b as f32; Ok(Fill::Solid(cvt(c[0]), cvt(c[1]), cvt(c[2]))) } ColorSpace::DeviceCMYK => { let c = &lut[4 * i as usize ..]; let cvt = |b: u8| b as f32; Ok(cmyk2rgb((cvt(c[0]), cvt(c[1]), cvt(c[2]), cvt(c[3])), mode)) } ref base => unimplemented!("Indexed colorspace with base {:?}", base) } } ColorSpace::Pattern => { let name = args[0].as_name()?; if let Some(&pat) = resources.pattern.get(name) { Ok(Fill::Pattern(pat)) } else { unimplemented!("Pattern {} not found", name) } } ColorSpace::Other(ref p) => unimplemented!("Other Color space {:?}", p), ColorSpace::Named(ref p) => unimplemented!("nested Named {:?}", p), } } } } fn gray2rgb(g: f32) -> Fill { Fill::Solid(g, g, g) } fn cmyk2rgb((c, m, y, k): (f32, f32, f32, f32), mode: BlendMode) -> Fill { let clamp = |f| if f > 1.0 { 1.0 } else { f }; Fill::Solid( 1.0 - clamp(c + k), 1.0 - clamp(m + k), 1.0 - clamp(y + k), ) } fn to_rect(o: &Outline) -> Option { if o.contours().len() != 1 { return None; } let c = &o.contours()[0]; if c.len() != 4 { return None; } if !c.iter(ContourIterFlags::IGNORE_CLOSE_SEGMENT).all(|segment| { let line = segment.baseline; segment.is_line() && (line.from_x() == line.to_x()) ^ (line.from_y() == line.to_y()) }) { return None; } Some(c.bounds()) } ================================================ FILE: render/src/scene.rs ================================================ use pathfinder_color::{ColorF, ColorU}; use pathfinder_content::{ fill::FillRule, stroke::{OutlineStrokeToFill}, outline::Outline, pattern::{Pattern}, dash::OutlineDash, }; use pathfinder_renderer::{ scene::{DrawPath, ClipPath, ClipPathId, Scene}, paint::{PaintId, Paint}, }; use pathfinder_geometry::{ vector::{Vector2F}, rect::RectF, transform2d::Transform2F, }; use pdf::object::{Ref, XObject, ImageXObject, Resolve, Resources, MaybeRef}; use crate::backend; use super::{FontEntry, TextSpan, DrawMode, Backend, Fill, Cache}; use pdf::font::Font as PdfFont; use pdf::error::PdfError; use std::sync::Arc; pub struct SceneBackend<'a> { scene: Scene, cache: &'a mut Cache, } impl<'a> SceneBackend<'a> { pub fn new(cache: &'a mut Cache) -> Self { let scene = Scene::new(); SceneBackend { scene, cache } } pub fn finish(self) -> Scene { self.scene } fn paint(&mut self, fill: Fill, alpha: f32) -> PaintId { let paint = match fill { Fill::Solid(r, g, b) => Paint::from_color(ColorF::new(r, g, b, alpha).to_u8()), Fill::Pattern(_) => { Paint::black() } }; self.scene.push_paint(&paint) } } impl<'a> Backend for SceneBackend<'a> { type ClipPathId = ClipPathId; fn create_clip_path(&mut self, path: Outline, fill_rule: FillRule, parent: Option) -> Self::ClipPathId { let mut clip = ClipPath::new(path); clip.set_fill_rule(fill_rule); clip.set_clip_path(parent); self.scene.push_clip_path(clip) } fn set_view_box(&mut self, view_box: RectF) { self.scene.set_view_box(view_box); let white = self.scene.push_paint(&Paint::from_color(ColorU::white())); self.scene.push_draw_path(DrawPath::new(Outline::from_rect(view_box), white)); } fn draw(&mut self, outline: &Outline, mode: &DrawMode, fill_rule: FillRule, transform: Transform2F, clip: Option) { match mode { DrawMode::Fill { fill } | DrawMode::FillStroke {fill, .. } => { let paint = self.paint(fill.color, fill.alpha); let mut draw_path = DrawPath::new(outline.clone().transformed(&transform), paint); draw_path.set_clip_path(clip); draw_path.set_fill_rule(fill_rule); draw_path.set_blend_mode(blend_mode(fill.mode)); self.scene.push_draw_path(draw_path); } _ => {} } match mode { DrawMode::Stroke { stroke, stroke_mode }| DrawMode::FillStroke { stroke, stroke_mode, .. } => { let paint = self.paint(stroke.color, stroke.alpha); let contour = match stroke_mode.dash_pattern { Some((ref pat, phase)) => { let dashed = OutlineDash::new(outline, &*pat, phase).into_outline(); let mut stroke = OutlineStrokeToFill::new(&dashed, stroke_mode.style); stroke.offset(); stroke.into_outline() } None => { let mut stroke = OutlineStrokeToFill::new(outline, stroke_mode.style); stroke.offset(); stroke.into_outline() } }; let mut draw_path = DrawPath::new(contour.transformed(&transform), paint); draw_path.set_clip_path(clip); draw_path.set_fill_rule(fill_rule); draw_path.set_blend_mode(blend_mode(stroke.mode)); self.scene.push_draw_path(draw_path); } _ => {} } } fn draw_image(&mut self, xobject_ref: Ref, im: &ImageXObject, resources: &Resources, transform: Transform2F, mode: backend::BlendMode, clip: Option, resolve: &impl Resolve) { if let Ok(ref image) = *self.cache.get_image(xobject_ref, im, resources, resolve, mode).0 { let size = image.size(); let size_f = size.to_f32(); let outline = Outline::from_rect(transform * RectF::new(Vector2F::default(), Vector2F::new(1.0, 1.0))); let im_tr = transform * Transform2F::from_scale(Vector2F::new(1.0 / size_f.x(), -1.0 / size_f.y())) * Transform2F::from_translation(Vector2F::new(0.0, -size_f.y())); let mut pattern = Pattern::from_image(image.clone()); pattern.apply_transform(im_tr); let paint = Paint::from_pattern(pattern); let paint_id = self.scene.push_paint(&paint); let mut draw_path = DrawPath::new(outline, paint_id); draw_path.set_clip_path(clip); draw_path.set_blend_mode(blend_mode(mode)); self.scene.push_draw_path(draw_path); } } fn draw_inline_image(&mut self, _im: &Arc, _resources: &Resources, _transform: Transform2F, mode: backend::BlendMode, clip: Option, _resolve: &impl Resolve) { } fn get_font(&mut self, font_ref: &MaybeRef, resolve: &impl Resolve) -> Result>, PdfError> { self.cache.get_font(font_ref, resolve) } fn add_text(&mut self, span: TextSpan, clip: Option) {} } fn blend_mode(mode: backend::BlendMode) -> pathfinder_content::effects::BlendMode { match mode { crate::BlendMode::Darken => pathfinder_content::effects::BlendMode::Multiply, crate::BlendMode::Overlay => pathfinder_content::effects::BlendMode::Overlay, } } ================================================ FILE: render/src/textstate.rs ================================================ use pathfinder_geometry::{ vector::Vector2F, transform2d::Transform2F, }; use font::GlyphId; use crate::{BlendMode, backend::{FillMode, Stroke}}; use super::{ BBox, fontentry::{FontEntry}, graphicsstate::{GraphicsState}, DrawMode, Backend, TextChar, }; use std::convert::TryInto; use pdf::content::TextMode; use std::sync::Arc; use itertools::Either; use istring::SmallString; #[derive(Clone)] pub struct TextState { pub text_matrix: Transform2F, // tracks current glyph pub line_matrix: Transform2F, // tracks current line pub char_space: f32, // Character spacing pub word_space: f32, // Word spacing pub horiz_scale: f32, // Horizontal scaling pub leading: f32, // Leading pub font_entry: Option>, // Text font pub font_size: f32, // Text font size pub mode: TextMode, // Text rendering mode pub rise: f32, // Text rise pub knockout: f32, //Text knockout } impl TextState { pub fn new() -> TextState { TextState { text_matrix: Transform2F::default(), line_matrix: Transform2F::default(), char_space: 0., word_space: 0., horiz_scale: 1., leading: 0., font_entry: None, font_size: 0., mode: TextMode::Fill, rise: 0., knockout: 0. } } pub fn reset_matrix(&mut self) { self.set_matrix(Transform2F::default()); } pub fn translate(&mut self, v: Vector2F) { let m = self.line_matrix * Transform2F::from_translation(v); self.set_matrix(m); } // move to the next line pub fn next_line(&mut self) { self.translate(Vector2F::new(0., -self.leading)); } // set text and line matrix pub fn set_matrix(&mut self, m: Transform2F) { self.text_matrix = m; self.line_matrix = m; } pub fn draw_text(&mut self, backend: &mut B, gs: &GraphicsState, data: &[u8], span: &mut Span, fill_mode: BlendMode, stroke_mode: BlendMode) { let e = match self.font_entry { Some(ref e) => e, None => { debug!("no font set"); return; } }; let codepoints = if e.is_cid { Either::Left(data.chunks_exact(2).map(|s| u16::from_be_bytes(s.try_into().unwrap()))) } else { Either::Right(data.iter().map(|&b| b as u16)) }; let glyphs = codepoints.map(|cid| (cid, e.cmap.get(&cid).map(|&(gid, ref uni)| (gid, uni.clone()))) ); let fill = FillMode { color: gs.fill_color, alpha: gs.fill_color_alpha, mode: fill_mode }; let stroke = FillMode { color: gs.stroke_color, alpha: gs.stroke_color_alpha, mode: stroke_mode }; let stroke_mode = gs.stroke(); let draw_mode = match self.mode { TextMode::Fill => Some(DrawMode::Fill { fill }), TextMode::FillAndClip => Some(DrawMode::Fill { fill }), TextMode::FillThenStroke => Some(DrawMode::FillStroke { fill, stroke, stroke_mode }), TextMode::Invisible => None, TextMode::Stroke => Some(DrawMode::Stroke { stroke, stroke_mode }), TextMode::StrokeAndClip => Some(DrawMode::Stroke { stroke, stroke_mode }), }; let e = self.font_entry.as_ref().expect("no font"); let tr = Transform2F::row_major( self.horiz_scale * self.font_size, 0., 0., 0., self.font_size, self.rise ) * e.font.font_matrix(); for (cid, t) in glyphs { let (gid, unicode, is_space) = match t { Some((gid, unicode)) => { let is_space = !e.is_cid && unicode.as_deref() == Some(" "); (gid, unicode, is_space) } None => (GlyphId(0), None, cid == 0x20) }; //debug!("cid {} -> gid {:?} {:?}", cid, gid, unicode); let glyph = e.font.glyph(gid); let width: f32 = e.widths.as_ref().map(|w| w.get(cid as usize) * 0.001 * self.horiz_scale * self.font_size) .or_else(|| glyph.as_ref().map(|g| tr.m11() * g.metrics.advance)) .unwrap_or(0.0); if is_space { let advance = (self.char_space + self.word_space) * self.horiz_scale + width; self.text_matrix = self.text_matrix * Transform2F::from_translation(Vector2F::new(advance, 0.)); let offset = span.text.len(); span.text.push(' '); span.chars.push(TextChar { offset, pos: span.width, width }); span.width += advance; continue; } if let Some(glyph) = glyph { let transform = gs.transform * self.text_matrix * tr; if glyph.path.len() != 0 { span.bbox.add(gs.transform * transform * glyph.path.bounds()); if let Some(ref draw_mode) = draw_mode { backend.draw_glyph(&glyph, draw_mode, transform, gs.clip_path_id); } } } else { debug!("no glyph for gid {:?}", gid); } let advance = self.char_space * self.horiz_scale + width; self.text_matrix = self.text_matrix * Transform2F::from_translation(Vector2F::new(advance, 0.)); let offset = span.text.len(); if let Some(s) = unicode { span.text.push_str(&*s); span.chars.push(TextChar { offset, pos: span.width, width }); } span.width += advance; } } pub fn advance(&mut self, delta: f32) -> f32 { //debug!("advance by {}", delta); let advance = delta * self.font_size * self.horiz_scale; self.text_matrix = self.text_matrix * Transform2F::from_translation(Vector2F::new(advance, 0.)); advance } } #[derive(Default)] pub struct Span { pub text: String, pub chars: Vec, pub width: f32, pub bbox: BBox, } ================================================ FILE: render/src/tracer.rs ================================================ use crate::{TextSpan, DrawMode, Backend, FontEntry, Fill, backend::{BlendMode, FillMode}, BBox}; use pathfinder_content::{ outline::Outline, fill::FillRule, }; use pathfinder_geometry::{ rect::RectF, transform2d::Transform2F, vector::Vector2F, }; use pathfinder_content::{ stroke::{StrokeStyle}, }; use pdf::object::{Ref, XObject, ImageXObject, Resolve, Resources, MaybeRef}; use font::Glyph; use pdf::font::Font as PdfFont; use pdf::error::PdfError; use std::sync::Arc; use std::path::PathBuf; use crate::font::{load_font, StandardCache}; use globalcache::sync::SyncCache; use crate::backend::Stroke; pub struct ClipPath { pub path: Outline, pub fill_rule: FillRule, pub parent: Option, } #[derive(Copy, Clone, Debug)] pub struct ClipPathId(pub usize); pub struct Tracer<'a> { pub items: Vec, clip_paths: &'a mut Vec, pub view_box: RectF, cache: &'a TraceCache, op_nr: usize, } pub struct TraceCache { fonts: Arc>>>, std: StandardCache, } fn font_key(font_ref: &MaybeRef) -> u64 { match font_ref { MaybeRef::Direct(ref shared) => shared.as_ref() as *const PdfFont as _, MaybeRef::Indirect(re) => re.get_ref().get_inner().id as _ } } impl TraceCache { pub fn new() -> Self { TraceCache { fonts: SyncCache::new(), std: StandardCache::new(), } } pub fn get_font(&self, font_ref: &MaybeRef, resolve: &impl Resolve) -> Result>, PdfError> { let mut error = None; let val = self.fonts.get(font_key(font_ref), |_| match load_font(font_ref, resolve, &self.std) { Ok(Some(f)) => Some(Arc::new(f)), Ok(None) => None, Err(e) => { error = Some(e); None } } ); match error { None => Ok(val), Some(e) => Err(e) } } pub fn require_unique_unicode(&mut self, require_unique_unicode: bool) { self.std.require_unique_unicode(require_unique_unicode); } } impl<'a> Tracer<'a> { pub fn new(cache: &'a TraceCache, clip_paths: &'a mut Vec) -> Self { Tracer { items: vec![], view_box: RectF::new(Vector2F::zero(), Vector2F::zero()), cache, op_nr: 0, clip_paths, } } pub fn finish(self) -> Vec { self.items } pub fn view_box(&self) -> RectF { self.view_box } } impl<'a> Backend for Tracer<'a> { type ClipPathId = ClipPathId; fn create_clip_path(&mut self, path: Outline, fill_rule: FillRule, parent: Option) -> ClipPathId { let id = ClipPathId(self.clip_paths.len()); self.clip_paths.push(ClipPath { path, fill_rule, parent, }); id } fn draw(&mut self, outline: &Outline, mode: &DrawMode, _fill_rule: FillRule, transform: Transform2F, clip: Option) { let stroke = match mode { DrawMode::FillStroke { stroke, stroke_mode, .. } | DrawMode::Stroke { stroke, stroke_mode } => Some((stroke.clone(), stroke_mode.clone())), DrawMode::Fill { .. } => None, }; self.items.push(DrawItem::Vector(VectorPath { outline: outline.clone(), fill: match mode { DrawMode::Fill { fill } | DrawMode::FillStroke { fill, .. } => Some(fill.clone()), _ => None }, stroke, transform, clip, op_nr: self.op_nr, })); } fn set_view_box(&mut self, r: RectF) { self.view_box = r; } fn draw_image(&mut self, xref: Ref, _im: &ImageXObject, _resources: &Resources, transform: Transform2F, mode: BlendMode, clip: Option, _resolve: &impl Resolve) { let rect = transform * RectF::new( Vector2F::new(0.0, 0.0), Vector2F::new(1.0, 1.0) ); self.items.push(DrawItem::Image(ImageObject { rect, id: xref, transform, op_nr: self.op_nr, mode, clip })); } fn draw_inline_image(&mut self, im: &Arc, _resources: &Resources, transform: Transform2F, mode: BlendMode, clip: Option, _resolve: &impl Resolve) { let rect = transform * RectF::new( Vector2F::new(0.0, 0.0), Vector2F::new(1.0, 1.0) ); self.items.push(DrawItem::InlineImage(InlineImageObject { rect, im: im.clone(), transform, op_nr: self.op_nr, mode, clip })); } fn draw_glyph(&mut self, _glyph: &Glyph, _mode: &DrawMode, _transform: Transform2F, clip: Option) {} fn get_font(&mut self, font_ref: &MaybeRef, resolve: &impl Resolve) -> Result>, PdfError> { self.cache.get_font(font_ref, resolve) } fn add_text(&mut self, span: TextSpan, clip: Option) { self.items.push(DrawItem::Text(span, clip)); } fn bug_op(&mut self, op_nr: usize) { self.op_nr = op_nr; } } #[derive(Debug)] pub struct ImageObject { pub rect: RectF, pub id: Ref, pub transform: Transform2F, pub op_nr: usize, pub mode: BlendMode, pub clip: Option, } #[derive(Debug)] pub struct InlineImageObject { pub rect: RectF, pub im: Arc, pub transform: Transform2F, pub op_nr: usize, pub mode: BlendMode, pub clip: Option, } #[derive(Debug)] pub enum DrawItem { Vector(VectorPath), Image(ImageObject), InlineImage(InlineImageObject), Text(TextSpan, Option), } #[derive(Debug)] pub struct VectorPath { pub outline: Outline, pub fill: Option, pub stroke: Option<(FillMode, Stroke)>, pub transform: Transform2F, pub op_nr: usize, pub clip: Option, } ================================================ FILE: view/Cargo.toml ================================================ [package] name = "pdf_view" version = "0.1.0" authors = ["Sebastian Köln "] edition = "2018" [features] unstable = [] [dependencies.pdf] git = "https://github.com/pdf-rs/pdf" default-features=false features = ["dump"] [dependencies.pdf_render] path = "../render" [dependencies.pathfinder_view] git = "https://github.com/s3bk/pathfinder_view" features = ["icon"] [dependencies] pathfinder_renderer = { git = "https://github.com/servo/pathfinder" } pathfinder_color = { git = "https://github.com/servo/pathfinder" } pathfinder_geometry = { git = "https://github.com/servo/pathfinder" } pathfinder_resources = { git = "https://github.com/servo/pathfinder" } pathfinder_content = { git = "https://github.com/servo/pathfinder" } pathfinder_export = { git = "https://github.com/servo/pathfinder" } log = { version = "0.4" } structopt = "0.3" font = { git = "https://github.com/pdf-rs/font" } pdf_encoding = "0.1" itertools = "*" image = "0.25" [dev-dependencies] criterion = "0.3" [target.wasm32-unknown-unknown.dependencies] wasm-bindgen = "0.2.48" js-sys = "0.3" web-sys = { version = "*", features = ["HtmlCanvasElement", "WebGl2RenderingContext"] } console_log = "0.2" console_error_panic_hook = "0.1.6" getrandom = { version = "0.2.3", features = ["js"]} [target.'cfg(unix)'.dependencies] env_logger = "0.8" pdf_render = { path = "../render" } [lib] crate-type = ["cdylib", "rlib"] ================================================ FILE: view/Makefile ================================================ DST = /home/sebk/data/view_wasm build: wasm-pack build -t no-modules --release cp pkg/pdf_view.js pkg/pdf_view_bg.wasm $(DST)/pkg/ cp ../wasm/* $(DST)/ publish: git -C $(DST) commit -a -m "update" git -C $(DST) push .PHONY: all all: build ================================================ FILE: view/src/bin/convert.rs ================================================ use pdf::file::{File as PdfFile, FileOptions}; use pdf::object::*; use pdf::error::PdfError; use std::fs::File; use std::io::BufWriter; use std::path::PathBuf; use pdf_render::{Cache, SceneBackend, render_page}; use pathfinder_export::{FileFormat, Export}; use pathfinder_geometry::transform2d::Transform2F; use structopt::StructOpt; #[derive(Debug, StructOpt)] #[structopt(name = "example", about = "An example of StructOpt usage.")] struct Opt { #[structopt(long = "dpi", default_value = "300")] dpi: f32, /// Format to generate. (svg | png | ps | pdf) #[structopt(short = "f", long="format")] format: String, /// (first) page to generate #[structopt(short = "p", long="page", default_value="0")] page: u32, /// Number of pages to generate, defaults to 1 #[structopt(short = "n", long="pages", default_value="1")] pages: u32, #[structopt(long = "placeholder", default_value="\"{}\"")] placeholder: String, /// Number of digits to zero-pad the page number to #[structopt(long = "digits", default_value="1")] digits: usize, /// Input file #[structopt(parse(from_os_str))] input: PathBuf, /// Output file. use '{}' (can be chaged via --palaceholder) as a replacement for the page output: String, } fn main() -> Result<(), PdfError> { env_logger::init(); let opt = Opt::from_args(); let format = match opt.format.as_str() { "svg" => FileFormat::SVG, "pdf" => FileFormat::PDF, // "png" => FileFormat::PNG, "ps" => FileFormat::PS, _ => panic!("invalid format") }; if opt.pages > 1 { assert!(opt.output.contains(&opt.placeholder), "output name does not contain a placeholder"); } let transform = Transform2F::from_scale(opt.dpi / 25.4); println!("read: {:?}", opt.input); let file = FileOptions::cached().open(&opt.input)?; let resolver = file.resolver(); let mut cache = Cache::new(); for (i, page) in file.pages().enumerate().skip(opt.page as usize).take(opt.pages as usize) { println!("page {}", i); let p: &Page = &*page.unwrap(); let mut backend = SceneBackend::new(&mut cache); render_page(&mut backend, &resolver, p, transform)?; let output = if opt.pages > 1 { let replacement = format!("{page:0digits$}", page=i, digits=opt.digits); opt.output.replace(opt.placeholder.as_str(), &replacement) } else { opt.output.clone() }; let mut writer = BufWriter::new(File::create(&output)?); backend.finish().export(&mut writer, format)?; } Ok(()) } ================================================ FILE: view/src/bin/view.rs ================================================ use pathfinder_renderer::gpu::options::RendererLevel; use pathfinder_view::{show, Config}; use pathfinder_resources::embedded::EmbeddedResourceLoader; use pathfinder_color::ColorF; use pdf::file::FileOptions; use pdf_view::PdfView; fn main() { env_logger::init(); let path = std::env::args().nth(1).unwrap(); let file = FileOptions::uncached().open(&path).unwrap(); let view = PdfView::new(file); let mut config = Config::new(Box::new(EmbeddedResourceLoader)); config.zoom = true; config.pan = true; config.background = ColorF::new(0.9, 0.9, 0.9, 1.0); config.render_level = RendererLevel::D3D9; show(view, config); } ================================================ FILE: view/src/lib.rs ================================================ #[macro_use] extern crate log; use std::sync::Arc; use pathfinder_view::{Config, Interactive, Context, Emitter, view::{ElementState, KeyCode, KeyEvent, ModifiersState}}; use pathfinder_renderer::scene::Scene; use pathfinder_geometry::vector::Vector2F; use pdf::file::{File as PdfFile, Cache as PdfCache, Log}; use pdf::any::AnySync; use pdf::PdfError; use pdf::backend::Backend; use pdf_render::{Cache, SceneBackend, page_bounds, render_page}; #[cfg(target_arch = "wasm32")] use pathfinder_view::WasmView; pub struct PdfView { file: PdfFile, num_pages: usize, cache: Cache, } impl PdfView where B: Backend + 'static, OC: PdfCache>> + 'static, SC: PdfCache, Arc>> + 'static, L: Log { pub fn new(file: PdfFile) -> Self { PdfView { num_pages: file.num_pages() as usize, file, cache: Cache::new(), } } } impl Interactive for PdfView where B: Backend + 'static, OC: PdfCache>> + 'static, SC: PdfCache, Arc>> + 'static, L: Log + 'static { type Event = Vec; fn title(&self) -> String { self.file.trailer.info_dict.as_ref() .and_then(|info| info.title.as_ref()) .and_then(|p| p.to_string().ok()) .unwrap_or_else(|| "PDF View".into()) } fn init(&mut self, ctx: &mut Context, sender: Emitter) { ctx.num_pages = self.num_pages; ctx.set_icon(image::load_from_memory_with_format(include_bytes!("../../logo.png"), image::ImageFormat::Png).unwrap().to_rgba8().into()); } fn scene(&mut self, ctx: &mut Context) -> Scene { info!("drawing page {}", ctx.page_nr()); let page = self.file.get_page(ctx.page_nr as u32).unwrap(); ctx.set_bounds(page_bounds(&page)); let mut backend = SceneBackend::new(&mut self.cache); let resolver = self.file.resolver(); render_page(&mut backend, &resolver, &page, ctx.view_transform()).unwrap(); backend.finish() } fn mouse_input(&mut self, ctx: &mut Context, page: usize, pos: Vector2F, state: ElementState) { if state != ElementState::Pressed { return; } info!("x={}, y={}", pos.x(), pos.y()); } fn keyboard_input(&mut self, ctx: &mut Context, state: ModifiersState, event: KeyEvent) { if event.state == ElementState::Released { return; } if state.shift_key() { let page = ctx.page_nr(); match event.physical_key { KeyCode::ArrowRight => ctx.goto_page(page + 10), KeyCode::ArrowLeft => ctx.goto_page(page.saturating_sub(10)), _ => return } } match event.physical_key { KeyCode::ArrowRight | KeyCode::PageDown => ctx.next_page(), KeyCode::ArrowLeft | KeyCode::PageUp => ctx.prev_page(), _ => return } } } #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; #[cfg(target_arch = "wasm32")] use js_sys::Uint8Array; #[cfg(target_arch = "wasm32")] use web_sys::{HtmlCanvasElement, WebGl2RenderingContext}; #[cfg(target_arch = "wasm32")] #[wasm_bindgen(start)] pub fn run() { std::panic::set_hook(Box::new(console_error_panic_hook::hook)); console_log::init_with_level(log::Level::Info); warn!("test"); } #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn show(canvas: HtmlCanvasElement, context: WebGl2RenderingContext, data: &Uint8Array) -> WasmView { use pathfinder_resources::embedded::EmbeddedResourceLoader; let data: Vec = data.to_vec(); info!("got {} bytes of data", data.len()); let file = PdfFile::from_data(data).expect("failed to parse PDF"); info!("got the file"); let view = PdfView::new(file); let mut config = Config::new(Box::new(EmbeddedResourceLoader)); config.zoom = false; config.pan = false; WasmView::new( canvas, context, config, Box::new(view) as _ ) } ================================================ FILE: wasm/index.html ================================================ PDF View

loading …

================================================ FILE: wasm/index.js ================================================ wasm_bindgen("pkg/pdf_view_bg.wasm").catch(console.error) .then(show_logo); //display("Drop a PDF here"); function show_logo() { fetch("logo.pdf") .then(r => r.arrayBuffer()) .then(buf => show_data(new Uint8Array(buf))); } function set_scroll_factors() {} function drop_handler(e) { e.stopPropagation(); e.preventDefault(); show(e.dataTransfer.files[0]); } function dragover_handler(e) { e.stopPropagation(); e.preventDefault(); } function display(msg) { delete document.getElementById("drop").style.display; document.getElementById("msg").innerText = msg || ""; } let view; function init_view(data, attempt) { let canvas = document.getElementById("canvas"); let context = canvas.getContext("webgl2"); if (context == null) { if (attempt < 10) { setTimeout(function() { init_view(data, attempt+1) }, 1000); display(`retrying ${attempt}`); } return; } view = wasm_bindgen.show(canvas, context, data); display(); let requested = false; function animation_frame(time) { requested = false; view.animation_frame(time); } function check(request_redraw) { if (request_redraw && !requested) { window.requestAnimationFrame(animation_frame); requested = true; } } window.addEventListener("keydown", e => check(view.key_down(e)), {capture: true}); window.addEventListener("keyup", e => check(view.key_up(e)), {capture: true}); canvas.addEventListener("mousemove", e => check(view.mouse_move(e))); canvas.addEventListener("mouseup", e => check(view.mouse_up(e))); canvas.addEventListener("mousedown", e => check(view.mouse_down(e))); window.addEventListener("resize", e => check(view.resize(e))); view.render(); } function show_data(data) { try { init_view(data, 0); } catch (e) { display("oops. try another one."); } } function show(file) { let reader = new FileReader(); reader.onload = function() { let data = new Uint8Array(reader.result); show_data(data); }; reader.readAsArrayBuffer(file); } function open() { var input = document.createElement('input'); input.type = 'file'; input.onchange = e => { // getting a hold of the file reference var file = e.target.files[0]; show(file); }; input.click(); } document.addEventListener("drop", drop_handler, false); document.addEventListener("dragover", dragover_handler, false); ================================================ FILE: wasm/style.css ================================================ body { display: flex; flex-direction: row; align-content: center; background-color: rgba(220, 200, 160, 255); padding: 10px; } #canvas { align-self: center; margin-left: auto; margin-right: auto; } #drop { display: flex; left: 0; right: 0; bottom: 0; flex-direction: row; align-items: center; position: fixed; } #drop p { text-align: center; flex: auto; font-size: 10pt; color: black; margin: 0; } #open { } #banner { background-color: rgba(0, 0, 0, 0.8); padding: 0.5em; font-family: sans-serif; } #banner a { color: gold; text-decoration: none; font-weight: bold; } #banner a:hover { color: white; }