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 [](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 <s3bk@protonmail.com>"]
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<dyn Error>> {
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 <s3bk@protonmail.com>"]
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<Scene> {
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>) -> Self::ClipPathId;
fn draw(&mut self, outline: &Outline, mode: &DrawMode, fill_rule: FillRule, transform: Transform2F, clip: Option<Self::ClipPathId>);
fn set_view_box(&mut self, r: RectF);
fn draw_image(&mut self, xref: Ref<XObject>, im: &ImageXObject, resources: &Resources, transform: Transform2F, mode: BlendMode, clip: Option<Self::ClipPathId>, resolve: &impl Resolve);
fn draw_inline_image(&mut self, im: &Arc<ImageXObject>, resources: &Resources, transform: Transform2F, mode: BlendMode, clip: Option<Self::ClipPathId>, resolve: &impl Resolve);
fn draw_glyph(&mut self, glyph: &Glyph, mode: &DrawMode, transform: Transform2F, clip: Option<Self::ClipPathId>) {
self.draw(&glyph.path, mode, FillRule::Winding, transform, clip);
}
fn get_font(&mut self, font_ref: &MaybeRef<PdfFont>, resolve: &impl Resolve) -> Result<Option<Arc<FontEntry>>, PdfError>;
fn add_text(&mut self, span: TextSpan, clip: Option<Self::ClipPathId>);
/// 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>, 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<Result<Image>>);
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<SyncCache<usize, Option<Arc<FontEntry>>>>,
images: Arc<SyncCache<(Ref<XObject>, BlendMode), ImageResult>>,
std: StandardCache,
missing_fonts: Vec<Name>,
}
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<PdfFont>, resolve: &impl Resolve) -> Result<Option<Arc<FontEntry>>, > {
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<XObject>, 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<dyn font::Font + Send + Sync + 'static>);
impl ValueSize for FontRc {
#[inline]
fn size(&self) -> usize {
1 // TODO
}
}
impl From<Box<dyn font::Font + Send + Sync + 'static>> for FontRc {
#[inline]
fn from(f: Box<dyn font::Font + Send + Sync + 'static>) -> 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<H: Hasher>(&self, state: &mut H) {
Arc::as_ptr(&self.0).hash(state)
}
}
pub struct StandardCache {
inner: Arc<SyncCache<String, Option<FontRc>>>,
#[cfg(not(feature="embed"))]
dir: PathBuf,
#[cfg(feature="embed")]
dir: EmbeddedStandardFonts,
fonts: HashMap<String, String>,
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<String, String> = 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<String, String> = 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<Cow<'static, [u8]>>;
fn sub_dir(&self, name: &str) -> Option<Self>;
}
impl DirRead for PathBuf {
fn read_file(&self, name: &str) -> Result<Cow<'static, [u8]>> {
std::fs::read(self.join(name)).map_err(|e| e.into()).map(|d| d.into())
}
fn sub_dir(&self, name: &str) -> Option<Self> {
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<Cow<'static, [u8]>> {
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<Self> {
None
}
}
#[derive(Debug)]
enum Dump {
Never,
OnError,
Always
}
pub fn load_font(font_ref: &MaybeRef<PdfFont>, resolve: &impl Resolve, cache: &StandardCache) -> Result<Option<FontEntry>> {
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<PdfFont>,
pub cmap: HashMap<u16, (GlyphId, Option<SmallString>)>,
pub widths: Option<Widths>,
pub is_cid: bool,
pub name: String,
}
impl FontEntry {
pub fn build(font: FontRc, pdf_font: MaybeRef<PdfFont>, font_db: Option<&FontDb>, resolve: &impl Resolve, require_unique_unicode: bool) -> Result<FontEntry, PdfError> {
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::<TrueTypeFont>().and_then(|ttf| ttf.cmap.as_ref())
.or_else(|| font.downcast_ref::<OpenTypeFont>().and_then(|otf| otf.cmap.as_ref()));
let glyph_unicode: HashMap<GlyphId, SmallString> = {
if let Some(type1) = font.downcast_ref::<Type1Font>() {
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::<CffFont>() {
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<u16, (GlyphId, Option<SmallString>)> {
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::<CffFont>() {
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::<CffFont>() {
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::<u16, (GlyphId, Option<SmallString>)>::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::<CffFont>() {
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<u32> = 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<PaintId>,
pub stroke_color: Fill,
pub stroke_color_alpha: f32,
pub stroke_paint: Option<PaintId>,
pub clip_path_id: Option<B::ClipPathId>,
pub clip_path: Option<ClipPath>,
pub clip_path_rect: Option<RectF>,
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<Cow<'a, [ColorU]>>, width: u32, height: u32) -> Option<Self> {
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<ColorU> = 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::<Rgba<u8>, &[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<Vec<u8>> {
use image::{ImageBuffer, imageops::{resize, FilterType}, Luma};
let src: ImageBuffer<Luma<u8>, &[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<ImageData<'static>, 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<u8>),
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<Vec<u8>> for Data<'a> {
fn from(v: Vec<u8>) -> 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::<Vec<u8>>().into(),
2 => data.iter().flat_map(|&b| (0..4).map(move |i| ex(b >> 2*i, 2))).collect::<Vec<u8>>().into(),
4 => data.iter().flat_map(|&b| (0..2).map(move |i| ex(b >> 4*i, 4))).collect::<Vec<u8>>().into(),
8 => data,
12 => data.chunks_exact(3).flat_map(|c| [c[0], c[1] << 4 | c[2] >> 4]).collect::<Vec<u8>>().into(),
16 => data.chunks_exact(2).map(|c| c[0]).collect::<Vec<u8>>().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::<Vec<u8>>().into(),
2 => raw_data.iter().flat_map(|&b| (0..4).map(move |i| ex(b >> 2*i, 2))).take(pixel_count).collect::<Vec<u8>>().into(),
4 => raw_data.iter().flat_map(|&b| (0..2).map(move |i| ex(b >> 4*i, 4))).take(pixel_count).collect::<Vec<u8>>().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<Item=u8>, mode: BlendMode) -> Vec<ColorU> {
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<RectF>);
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<RectF> {
self.0
}
}
impl From<RectF> 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<Transform2F, PdfError> {
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<Pattern>),
}
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<RectF>,
pub font_size: f32,
#[debug(skip)]
pub font: Option<Arc<FontEntry>>,
pub text: String,
pub chars: Vec<TextChar>,
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<Item=Part> + '_ {
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<Item=Part> + '_ {
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<B>, &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<Fill> {
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<Fill> {
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<RectF> {
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>) -> 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<ClipPathId>) {
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<XObject>, im: &ImageXObject, resources: &Resources, transform: Transform2F, mode: backend::BlendMode, clip: Option<ClipPathId>, 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<ImageXObject>, _resources: &Resources, _transform: Transform2F, mode: backend::BlendMode, clip: Option<ClipPathId>, _resolve: &impl Resolve) {
}
fn get_font(&mut self, font_ref: &MaybeRef<PdfFont>, resolve: &impl Resolve) -> Result<Option<Arc<FontEntry>>, PdfError> {
self.cache.get_font(font_ref, resolve)
}
fn add_text(&mut self, span: TextSpan, clip: Option<Self::ClipPathId>) {}
}
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<Arc<FontEntry>>, // 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<B: Backend>(&mut self, backend: &mut B, gs: &GraphicsState<B>, 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<TextChar>,
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<ClipPathId>,
}
#[derive(Copy, Clone, Debug)]
pub struct ClipPathId(pub usize);
pub struct Tracer<'a> {
pub items: Vec<DrawItem>,
clip_paths: &'a mut Vec<ClipPath>,
pub view_box: RectF,
cache: &'a TraceCache,
op_nr: usize,
}
pub struct TraceCache {
fonts: Arc<SyncCache<u64, Option<Arc<FontEntry>>>>,
std: StandardCache,
}
fn font_key(font_ref: &MaybeRef<PdfFont>) -> 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<PdfFont>, resolve: &impl Resolve) -> Result<Option<Arc<FontEntry>>, 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<ClipPath>) -> Self {
Tracer {
items: vec![],
view_box: RectF::new(Vector2F::zero(), Vector2F::zero()),
cache,
op_nr: 0,
clip_paths,
}
}
pub fn finish(self) -> Vec<DrawItem> {
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>) -> 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<ClipPathId>) {
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<XObject>, _im: &ImageXObject, _resources: &Resources, transform: Transform2F, mode: BlendMode, clip: Option<ClipPathId>, _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<ImageXObject>, _resources: &Resources, transform: Transform2F, mode: BlendMode, clip: Option<ClipPathId>, _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<ClipPathId>) {}
fn get_font(&mut self, font_ref: &MaybeRef<PdfFont>, resolve: &impl Resolve) -> Result<Option<Arc<FontEntry>>, PdfError> {
self.cache.get_font(font_ref, resolve)
}
fn add_text(&mut self, span: TextSpan, clip: Option<Self::ClipPathId>) {
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<XObject>,
pub transform: Transform2F,
pub op_nr: usize,
pub mode: BlendMode,
pub clip: Option<ClipPathId>,
}
#[derive(Debug)]
pub struct InlineImageObject {
pub rect: RectF,
pub im: Arc<ImageXObject>,
pub transform: Transform2F,
pub op_nr: usize,
pub mode: BlendMode,
pub clip: Option<ClipPathId>,
}
#[derive(Debug)]
pub enum DrawItem {
Vector(VectorPath),
Image(ImageObject),
InlineImage(InlineImageObject),
Text(TextSpan, Option<ClipPathId>),
}
#[derive(Debug)]
pub struct VectorPath {
pub outline: Outline,
pub fill: Option<FillMode>,
pub stroke: Option<(FillMode, Stroke)>,
pub transform: Transform2F,
pub op_nr: usize,
pub clip: Option<ClipPathId>,
}
================================================
FILE: view/Cargo.toml
================================================
[package]
name = "pdf_view"
version = "0.1.0"
authors = ["Sebastian Köln <s3bk@protonmail.com>"]
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<B: Backend, OC, SC, L> {
file: PdfFile<B, OC, SC, L>,
num_pages: usize,
cache: Cache,
}
impl<B, OC, SC, L> PdfView<B, OC, SC, L>
where
B: Backend + 'static,
OC: PdfCache<Result<AnySync, Arc<PdfError>>> + 'static,
SC: PdfCache<Result<Arc<[u8]>, Arc<PdfError>>> + 'static,
L: Log
{
pub fn new(file: PdfFile<B, OC, SC, L>) -> Self {
PdfView {
num_pages: file.num_pages() as usize,
file,
cache: Cache::new(),
}
}
}
impl<B, OC, SC, L> Interactive for PdfView<B, OC, SC, L>
where
B: Backend + 'static,
OC: PdfCache<Result<AnySync, Arc<PdfError>>> + 'static,
SC: PdfCache<Result<Arc<[u8]>, Arc<PdfError>>> + 'static,
L: Log + 'static
{
type Event = Vec<u8>;
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<Self::Event>) {
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<u8> = 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
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>PDF View</title>
<script type="text/javascript" src="pkg/pdf_view.js"></script>
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<canvas id="canvas" tabindex="0"></canvas>
<script type="text/javascript" src="index.js"></script>
<div id="drop">
<input type="file" id="file-selector" onchange="show(event.target.files[0])"></input>
<p id="msg">loading …</p>
<div id="banner"><a href="https://github.com/pdf-rs/pdf_render/">Code</a></div>
</div>
</body>
</html>
================================================
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;
}
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
SYMBOL INDEX (194 symbols across 19 files)
FILE: examples/pdf2image/src/main.rs
type Options (line 12) | struct Options {
function main (line 30) | fn main() -> Result<(), Box<dyn Error>> {
FILE: render/benches/render.rs
function bench_render_page (line 7) | fn bench_render_page(c: &mut Criterion) {
FILE: render/benches/view.rs
function render_file (line 9) | fn render_file(path: &Path) -> Vec<Scene> {
function bench_file (line 22) | fn bench_file(c: &mut Criterion, name: &str) {
FILE: render/examples/trace.rs
function main (line 5) | fn main() {
FILE: render/src/backend.rs
type BlendMode (line 19) | pub enum BlendMode {
type Backend (line 24) | pub trait Backend {
method create_clip_path (line 27) | fn create_clip_path(&mut self, path: Outline, fill_rule: FillRule, par...
method draw (line 28) | fn draw(&mut self, outline: &Outline, mode: &DrawMode, fill_rule: Fill...
method set_view_box (line 29) | fn set_view_box(&mut self, r: RectF);
method draw_image (line 30) | fn draw_image(&mut self, xref: Ref<XObject>, im: &ImageXObject, resour...
method draw_inline_image (line 31) | fn draw_inline_image(&mut self, im: &Arc<ImageXObject>, resources: &Re...
method draw_glyph (line 32) | fn draw_glyph(&mut self, glyph: &Glyph, mode: &DrawMode, transform: Tr...
method get_font (line 35) | fn get_font(&mut self, font_ref: &MaybeRef<PdfFont>, resolve: &impl Re...
method add_text (line 36) | fn add_text(&mut self, span: TextSpan, clip: Option<Self::ClipPathId>);
method bug_text_no_font (line 39) | fn bug_text_no_font(&mut self, data: &[u8]) {}
method bug_text_invisible (line 40) | fn bug_text_invisible(&mut self, text: &str) {}
method bug_postscript (line 41) | fn bug_postscript(&mut self, data: &[u8]) {}
method bug_op (line 42) | fn bug_op(&mut self, op_nr: usize) {}
method inspect_op (line 43) | fn inspect_op(&mut self, op: &Op) {}
type FillMode (line 47) | pub struct FillMode {
type DrawMode (line 52) | pub enum DrawMode {
type Stroke (line 58) | pub struct Stroke {
FILE: render/src/cache.rs
type ImageResult (line 24) | pub struct ImageResult(pub Arc<Result<Image>>);
method size (line 26) | fn size(&self) -> usize {
type Cache (line 34) | pub struct Cache {
method new (line 43) | pub fn new() -> Cache {
method get_font (line 51) | pub fn get_font(&mut self, pdf_font: &MaybeRef<PdfFont>, resolve: &imp...
method get_image (line 74) | pub fn get_image(&mut self, xobject_ref: Ref<XObject>, im: &ImageXObje...
method drop (line 83) | fn drop(&mut self) {
FILE: render/src/font.rs
type FontRc (line 17) | pub struct FontRc(Arc<dyn font::Font + Send + Sync + 'static>);
method from (line 26) | fn from(f: Box<dyn font::Font + Send + Sync + 'static>) -> Self {
method size (line 20) | fn size(&self) -> usize {
type Target (line 31) | type Target = dyn font::Font + Send + Sync + 'static;
method deref (line 33) | fn deref(&self) -> &Self::Target {
method eq (line 39) | fn eq(&self, rhs: &Self) -> bool {
method hash (line 46) | fn hash<H: Hasher>(&self, state: &mut H) {
type StandardCache (line 50) | pub struct StandardCache {
method new (line 65) | pub fn new() -> StandardCache {
method new (line 86) | pub fn new() -> StandardCache {
method require_unique_unicode (line 99) | pub fn require_unique_unicode(&mut self, r: bool) {
type DirRead (line 104) | pub trait DirRead: Sized {
method read_file (line 105) | fn read_file(&self, name: &str) -> Result<Cow<'static, [u8]>>;
method sub_dir (line 106) | fn sub_dir(&self, name: &str) -> Option<Self>;
method read_file (line 110) | fn read_file(&self, name: &str) -> Result<Cow<'static, [u8]>> {
method sub_dir (line 113) | fn sub_dir(&self, name: &str) -> Option<Self> {
method read_file (line 130) | fn read_file(&self, name: &str) -> Result<Cow<'static, [u8]>> {
method sub_dir (line 133) | fn sub_dir(&self, name: &str) -> Option<Self> {
type EmbeddedStandardFonts (line 126) | pub struct EmbeddedStandardFonts;
type Dump (line 139) | enum Dump {
function load_font (line 145) | pub fn load_font(font_ref: &MaybeRef<PdfFont>, resolve: &impl Resolve, c...
FILE: render/src/fontentry.rs
type FontEntry (line 13) | pub struct FontEntry {
method build (line 24) | pub fn build(font: FontRc, pdf_font: MaybeRef<PdfFont>, font_db: Optio...
method size (line 324) | fn size(&self) -> usize {
FILE: render/src/graphicsstate.rs
type GraphicsState (line 8) | pub struct GraphicsState<'a, B: Backend> {
method clone (line 34) | fn clone(&self) -> Self {
function set_fill_color (line 44) | pub fn set_fill_color(&mut self, fill: Fill) {
function set_fill_alpha (line 50) | pub fn set_fill_alpha(&mut self, alpha: f32) {
function set_stroke_color (line 57) | pub fn set_stroke_color(&mut self, fill: Fill) {
function set_stroke_alpha (line 63) | pub fn set_stroke_alpha(&mut self, alpha: f32) {
function stroke (line 70) | pub fn stroke(&self) -> Stroke {
FILE: render/src/image.rs
type ImageData (line 12) | pub struct ImageData<'a> {
function new (line 18) | pub fn new(data: impl Into<Cow<'a, [ColorU]>>, width: u32, height: u32) ...
function width (line 25) | pub fn width(&self) -> u32 {
function height (line 28) | pub fn height(&self) -> u32 {
function data (line 31) | pub fn data(&self) -> &[ColorU] {
function into_data (line 34) | pub fn into_data(self) -> Cow<'a, [ColorU]> {
function rgba_data (line 37) | pub fn rgba_data(&self) -> &[u8] {
function rotate (line 45) | pub fn rotate(&self, angle: u8) -> ImageData<'_> {
function safe (line 94) | pub fn safe(&self, path: &Path) {
function resize_alpha (line 100) | fn resize_alpha(data: &[u8], src_width: u32, src_height: u32, dest_width...
function load_image (line 109) | pub fn load_image(image: &ImageXObject, resources: &Resources, resolve: ...
function rgb2rgba (line 300) | fn rgb2rgba(c: &[u8], a: u8, mode: BlendMode) -> ColorU {
function rgb2rgb (line 311) | fn rgb2rgb(r: f32, g: f32, b: f32, mode: BlendMode) -> [u8; 3] {
function cmyk2rgb (line 329) | fn cmyk2rgb([c, m, y, k]: [u8; 4], mode: BlendMode) -> [u8; 3] {
function cmyk2color (line 348) | fn cmyk2color(cmyk: [u8; 4], a: u8, mode: BlendMode) -> ColorU {
function cmyk2color_arr (line 353) | fn cmyk2color_arr(data: &[u8], alpha: impl Iterator<Item=u8>, mode: Blen...
FILE: render/src/lib.rs
constant SCALE (line 46) | const SCALE: f32 = 25.4 / 72.;
type BBox (line 50) | pub struct BBox(Option<RectF>);
method empty (line 52) | pub fn empty() -> Self {
method add (line 55) | pub fn add(&mut self, r2: RectF) {
method add_bbox (line 61) | pub fn add_bbox(&mut self, bb: Self) {
method rect (line 66) | pub fn rect(self) -> Option<RectF> {
method from (line 71) | fn from(r: RectF) -> Self {
function page_bounds (line 77) | pub fn page_bounds(page: &Page) -> RectF {
function render_page (line 81) | pub fn render_page(backend: &mut impl Backend, resolve: &impl Resolve, p...
function render_pattern (line 109) | pub fn render_pattern(backend: &mut impl Backend, pattern: &Pattern, res...
type Fill (line 126) | pub enum Fill {
method black (line 131) | pub fn black() -> Self {
type TextSpan (line 137) | pub struct TextSpan {
method parts (line 159) | pub fn parts(&self) -> impl Iterator<Item=Part> + '_ {
method rparts (line 170) | pub fn rparts(&self) -> impl Iterator<Item=Part> + '_ {
type Part (line 182) | pub struct Part<'a> {
type TextChar (line 189) | pub struct TextChar {
FILE: render/src/renderstate.rs
type Cvt (line 27) | trait Cvt {
method cvt (line 29) | fn cvt(self) -> Self::Out;
type Out (line 32) | type Out = Vector2F;
method cvt (line 33) | fn cvt(self) -> Self::Out {
type Out (line 38) | type Out = Transform2F;
method cvt (line 39) | fn cvt(self) -> Self::Out {
type Out (line 45) | type Out = RectF;
method cvt (line 46) | fn cvt(self) -> Self::Out {
type Out (line 54) | type Out = FillRule;
method cvt (line 55) | fn cvt(self) -> Self::Out {
type Out (line 63) | type Out = (f32, f32, f32);
method cvt (line 64) | fn cvt(self) -> Self::Out {
type Out (line 70) | type Out = (f32, f32, f32, f32);
method cvt (line 71) | fn cvt(self) -> Self::Out {
type RenderState (line 77) | pub struct RenderState<'a, R: Resolve, B: Backend> {
function new (line 89) | pub fn new(backend: &'a mut B, resolve: &'a R, resources: &'a Resources,...
function draw (line 131) | fn draw(&mut self, mode: &DrawMode, fill_rule: FillRule) {
function draw_op (line 137) | pub fn draw_op(&mut self, op: &'a Op, op_nr: usize) -> Result<()> {
function blend_mode_fill (line 383) | fn blend_mode_fill(&self) -> BlendMode {
function blend_mode_stroke (line 390) | fn blend_mode_stroke(&self) -> BlendMode {
function text (line 398) | fn text(&mut self, inner: impl FnOnce(&mut B, &mut TextState, &mut Graph...
function color_space (line 427) | fn color_space(&self, name: &str) -> Result<&'a ColorSpace> {
function flush (line 440) | fn flush(&mut self) {
function draw_form (line 446) | fn draw_form(&mut self, form: &FormXObject) -> Result<()> {
function get_properties (line 479) | fn get_properties<'b>(&'b self, p: &'b Primitive) -> Result<&'b Dictiona...
function convert_color (line 495) | fn convert_color<'a>(cs: &mut &'a ColorSpace, color: &Color, resources: ...
function convert_color2 (line 506) | fn convert_color2<'a>(cs: &mut &'a ColorSpace, color: &Color, resources:...
function gray2rgb (line 666) | fn gray2rgb(g: f32) -> Fill {
function cmyk2rgb (line 670) | fn cmyk2rgb((c, m, y, k): (f32, f32, f32, f32), mode: BlendMode) -> Fill {
function to_rect (line 680) | fn to_rect(o: &Outline) -> Option<RectF> {
FILE: render/src/scene.rs
type SceneBackend (line 25) | pub struct SceneBackend<'a> {
function new (line 30) | pub fn new(cache: &'a mut Cache) -> Self {
function finish (line 37) | pub fn finish(self) -> Scene {
function paint (line 40) | fn paint(&mut self, fill: Fill, alpha: f32) -> PaintId {
type ClipPathId (line 51) | type ClipPathId = ClipPathId;
method create_clip_path (line 52) | fn create_clip_path(&mut self, path: Outline, fill_rule: FillRule, paren...
method set_view_box (line 58) | fn set_view_box(&mut self, view_box: RectF) {
method draw (line 65) | fn draw(&mut self, outline: &Outline, mode: &DrawMode, fill_rule: FillRu...
method draw_image (line 103) | fn draw_image(&mut self, xobject_ref: Ref<XObject>, im: &ImageXObject, r...
method draw_inline_image (line 123) | fn draw_inline_image(&mut self, _im: &Arc<ImageXObject>, _resources: &Re...
method get_font (line 127) | fn get_font(&mut self, font_ref: &MaybeRef<PdfFont>, resolve: &impl Reso...
method add_text (line 130) | fn add_text(&mut self, span: TextSpan, clip: Option<Self::ClipPathId>) {}
function blend_mode (line 133) | fn blend_mode(mode: backend::BlendMode) -> pathfinder_content::effects::...
FILE: render/src/textstate.rs
type TextState (line 23) | pub struct TextState {
method new (line 37) | pub fn new() -> TextState {
method reset_matrix (line 52) | pub fn reset_matrix(&mut self) {
method translate (line 55) | pub fn translate(&mut self, v: Vector2F) {
method next_line (line 61) | pub fn next_line(&mut self) {
method set_matrix (line 65) | pub fn set_matrix(&mut self, m: Transform2F) {
method draw_text (line 69) | pub fn draw_text<B: Backend>(&mut self, backend: &mut B, gs: &Graphics...
method advance (line 162) | pub fn advance(&mut self, delta: f32) -> f32 {
type Span (line 171) | pub struct Span {
FILE: render/src/tracer.rs
type ClipPath (line 24) | pub struct ClipPath {
type ClipPathId (line 31) | pub struct ClipPathId(pub usize);
type Tracer (line 33) | pub struct Tracer<'a> {
type TraceCache (line 40) | pub struct TraceCache {
method new (line 51) | pub fn new() -> Self {
method get_font (line 57) | pub fn get_font(&self, font_ref: &MaybeRef<PdfFont>, resolve: &impl Re...
method require_unique_unicode (line 74) | pub fn require_unique_unicode(&mut self, require_unique_unicode: bool) {
function font_key (line 44) | fn font_key(font_ref: &MaybeRef<PdfFont>) -> u64 {
function new (line 79) | pub fn new(cache: &'a TraceCache, clip_paths: &'a mut Vec<ClipPath>) -> ...
function finish (line 88) | pub fn finish(self) -> Vec<DrawItem> {
function view_box (line 91) | pub fn view_box(&self) -> RectF {
type ClipPathId (line 96) | type ClipPathId = ClipPathId;
method create_clip_path (line 98) | fn create_clip_path(&mut self, path: Outline, fill_rule: FillRule, paren...
method draw (line 107) | fn draw(&mut self, outline: &Outline, mode: &DrawMode, _fill_rule: FillR...
method set_view_box (line 124) | fn set_view_box(&mut self, r: RectF) {
method draw_image (line 127) | fn draw_image(&mut self, xref: Ref<XObject>, _im: &ImageXObject, _resour...
method draw_inline_image (line 135) | fn draw_inline_image(&mut self, im: &Arc<ImageXObject>, _resources: &Res...
method draw_glyph (line 144) | fn draw_glyph(&mut self, _glyph: &Glyph, _mode: &DrawMode, _transform: T...
method get_font (line 145) | fn get_font(&mut self, font_ref: &MaybeRef<PdfFont>, resolve: &impl Reso...
method add_text (line 148) | fn add_text(&mut self, span: TextSpan, clip: Option<Self::ClipPathId>) {
method bug_op (line 151) | fn bug_op(&mut self, op_nr: usize) {
type ImageObject (line 157) | pub struct ImageObject {
type InlineImageObject (line 166) | pub struct InlineImageObject {
type DrawItem (line 176) | pub enum DrawItem {
type VectorPath (line 184) | pub struct VectorPath {
FILE: view/src/bin/convert.rs
type Opt (line 14) | struct Opt {
function main (line 46) | fn main() -> Result<(), PdfError> {
FILE: view/src/bin/view.rs
function main (line 10) | fn main() {
FILE: view/src/lib.rs
type PdfView (line 17) | pub struct PdfView<B: Backend, OC, SC, L> {
function new (line 29) | pub fn new(file: PdfFile<B, OC, SC, L>) -> Self {
type Event (line 44) | type Event = Vec<u8>;
method title (line 45) | fn title(&self) -> String {
method init (line 51) | fn init(&mut self, ctx: &mut Context, sender: Emitter<Self::Event>) {
method scene (line 55) | fn scene(&mut self, ctx: &mut Context) -> Scene {
method mouse_input (line 66) | fn mouse_input(&mut self, ctx: &mut Context, page: usize, pos: Vector2F,...
method keyboard_input (line 70) | fn keyboard_input(&mut self, ctx: &mut Context, state: ModifiersState, e...
function run (line 101) | pub fn run() {
function show (line 109) | pub fn show(canvas: HtmlCanvasElement, context: WebGl2RenderingContext, ...
FILE: wasm/index.js
function show_logo (line 6) | function show_logo() {
function set_scroll_factors (line 12) | function set_scroll_factors() {}
function drop_handler (line 14) | function drop_handler(e) {
function dragover_handler (line 19) | function dragover_handler(e) {
function display (line 24) | function display(msg) {
function init_view (line 30) | function init_view(data, attempt) {
function show_data (line 65) | function show_data(data) {
function show (line 73) | function show(file) {
function open (line 82) | function open() {
Condensed preview — 32 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (124K chars).
[
{
"path": ".travis.yml",
"chars": 72,
"preview": "language: rust\n\nscript:\n - cargo update\n - cargo build\n - cargo test\n"
},
{
"path": "Cargo.toml",
"chars": 1641,
"preview": "[workspace]\nmembers = [\n \"render\",\n \"view\",\n \"examples/pdf2image\",\n]\n[patch.crates-io]\npathfinder_gl = { git = "
},
{
"path": "LICENSE",
"chars": 1066,
"preview": "Copyright © 2020 The pdf-rs contributers.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy "
},
{
"path": "README.md",
"chars": 729,
"preview": "# pdf_render [](https://travis-ci.com/pdf-rs/pdf_rend"
},
{
"path": "download_fonts.sh",
"chars": 962,
"preview": "#!/usr/bin/env bash\nset -e\n\nTMPDIR=`mktemp -d`\nif [[ ! \"$TMPDIR\" || ! -d \"$TMPDIR\" ]]; then\n echo \"Couldn't create te"
},
{
"path": "examples/pdf2image/Cargo.toml",
"chars": 535,
"preview": "[package]\nname = \"pdf2image\"\nversion = \"0.1.0\"\nauthors = [\"Sebastian K <s3bk@protonmail.com>\"]\nedition = \"2018\"\n\n# See m"
},
{
"path": "examples/pdf2image/src/main.rs",
"chars": 1115,
"preview": "use argh::FromArgs;\nuse pdf::file::{File, FileOptions};\nuse pdf_render::{Cache, SceneBackend, render_page};\nuse pathfind"
},
{
"path": "render/Cargo.toml",
"chars": 1189,
"preview": "[package]\nname = \"pdf_render\"\nversion = \"0.1.0\"\nauthors = [\"Sebastian Köln <s3bk@protonmail.com>\"]\nedition = \"2021\"\n\n[fe"
},
{
"path": "render/benches/render.rs",
"chars": 942,
"preview": "use criterion::{black_box, criterion_group, criterion_main, Criterion};\n\nuse pdf::file::{FileOptions};\nuse pdf_render::{"
},
{
"path": "render/benches/view.rs",
"chars": 1592,
"preview": "use criterion::{black_box, criterion_group, criterion_main, Criterion};\n\nuse pdf::file::{FileOptions};\nuse pdf::object::"
},
{
"path": "render/examples/trace.rs",
"chars": 684,
"preview": "use pdf::file::FileOptions;\nuse pdf_render::render_page;\nuse pdf_render::tracer::{TraceCache, Tracer};\n\nfn main() {\n "
},
{
"path": "render/src/backend.rs",
"chars": 2377,
"preview": "use pathfinder_geometry::{\n transform2d::Transform2F,\n rect::RectF,\n};\nuse pathfinder_content::{\n fill::FillRul"
},
{
"path": "render/src/cache.rs",
"chars": 2578,
"preview": "use std::path::{PathBuf};\nuse std::sync::Arc;\n\nuse pdf::object::*;\nuse pdf::primitive::Name;\nuse pdf::font::{Font as Pdf"
},
{
"path": "render/src/font.rs",
"chars": 6502,
"preview": "use std::borrow::Cow;\nuse std::path::{PathBuf};\nuse std::ops::Deref;\nuse std::collections::HashMap;\nuse glyphmatcher::Fo"
},
{
"path": "render/src/fontentry.rs",
"chars": 14373,
"preview": "use std::collections::{HashMap, HashSet};\nuse font::{self, GlyphId, TrueTypeFont, CffFont, Type1Font, OpenTypeFont};\nuse"
},
{
"path": "render/src/graphicsstate.rs",
"chars": 2187,
"preview": "use pathfinder_content::stroke::StrokeStyle;\nuse pathfinder_renderer::{paint::PaintId, scene::ClipPath};\nuse pdf::object"
},
{
"path": "render/src/image.rs",
"chars": 14034,
"preview": "use image::{RgbaImage, ImageBuffer, Rgba};\nuse pdf::object::*;\nuse pdf::error::PdfError;\nuse pathfinder_color::ColorU;\nu"
},
{
"path": "render/src/lib.rs",
"chars": 5581,
"preview": "#[macro_use] extern crate log;\n#[macro_use] extern crate pdf;\n\nmacro_rules! assert_eq {\n ($a:expr, $b:expr) => {\n "
},
{
"path": "render/src/renderstate.rs",
"chars": 29066,
"preview": "use pathfinder_content::outline::ContourIterFlags;\nuse pathfinder_renderer::scene::ClipPath;\nuse pdf::object::*;\nuse pdf"
},
{
"path": "render/src/scene.rs",
"chars": 5688,
"preview": "use pathfinder_color::{ColorF, ColorU};\nuse pathfinder_content::{\n fill::FillRule,\n stroke::{OutlineStrokeToFill},"
},
{
"path": "render/src/textstate.rs",
"chars": 6316,
"preview": "use pathfinder_geometry::{\n vector::Vector2F,\n transform2d::Transform2F,\n};\nuse font::GlyphId;\nuse crate::{BlendMo"
},
{
"path": "render/src/tracer.rs",
"chars": 6052,
"preview": "use crate::{TextSpan, DrawMode, Backend, FontEntry, Fill, backend::{BlendMode, FillMode}, BBox};\nuse pathfinder_content:"
},
{
"path": "view/Cargo.toml",
"chars": 1412,
"preview": "[package]\nname = \"pdf_view\"\nversion = \"0.1.0\"\nauthors = [\"Sebastian Köln <s3bk@protonmail.com>\"]\nedition = \"2018\"\n\n[feat"
},
{
"path": "view/Makefile",
"chars": 248,
"preview": "DST = /home/sebk/data/view_wasm\n\nbuild:\n\twasm-pack build -t no-modules --release\n\tcp pkg/pdf_view.js pkg/pdf_view_bg.was"
},
{
"path": "view/src/bin/convert.rs",
"chars": 2656,
"preview": "use pdf::file::{File as PdfFile, FileOptions};\nuse pdf::object::*;\nuse pdf::error::PdfError;\nuse std::fs::File;\nuse std:"
},
{
"path": "view/src/bin/view.rs",
"chars": 661,
"preview": "use pathfinder_renderer::gpu::options::RendererLevel;\nuse pathfinder_view::{show, Config};\nuse pathfinder_resources::emb"
},
{
"path": "view/src/lib.rs",
"chars": 4203,
"preview": "#[macro_use] extern crate log;\n\nuse std::sync::Arc;\nuse pathfinder_view::{Config, Interactive, Context, Emitter, view::{"
},
{
"path": "wasm/index.html",
"chars": 660,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <title>PDF View</title>\n <script type=\"t"
},
{
"path": "wasm/index.js",
"chars": 2550,
"preview": "wasm_bindgen(\"pkg/pdf_view_bg.wasm\").catch(console.error)\n.then(show_logo);\n//display(\"Drop a PDF here\");\n\n\nfunction sho"
},
{
"path": "wasm/style.css",
"chars": 710,
"preview": "body {\n display: flex;\n flex-direction: row;\n align-content: center;\n background-color: rgba(220, 200, 160, "
}
]
// ... and 2 more files (download for full content)
About this extraction
This page contains the full source code of the pdf-rs/pdf_render GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 32 files (115.6 KB), approximately 29.5k tokens, and a symbol index with 194 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.