This commit is contained in:
commit
b41388c3ed
|
@ -1537,6 +1537,16 @@ dependencies = [
|
|||
"pathfinder_simd 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathfinder_pdf"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pathfinder_content 0.1.0",
|
||||
"pathfinder_geometry 0.3.0",
|
||||
"pathfinder_renderer 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathfinder_renderer"
|
||||
version = "0.1.0"
|
||||
|
@ -2117,6 +2127,15 @@ dependencies = [
|
|||
"usvg 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "svg2pdf"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"pathfinder_pdf 0.1.0",
|
||||
"pathfinder_svg 0.1.0",
|
||||
"usvg 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "svgdom"
|
||||
version = "0.17.0"
|
||||
|
|
|
@ -13,11 +13,13 @@ members = [
|
|||
"examples/canvas_moire",
|
||||
"examples/canvas_text",
|
||||
"examples/lottie_basic",
|
||||
"examples/svg2pdf",
|
||||
"examples/swf_basic",
|
||||
"geometry",
|
||||
"gl",
|
||||
"gpu",
|
||||
"lottie",
|
||||
"pdf",
|
||||
"metal",
|
||||
"renderer",
|
||||
"simd",
|
||||
|
|
30
c/src/lib.rs
30
c/src/lib.rs
|
@ -13,8 +13,8 @@
|
|||
use font_kit::handle::Handle;
|
||||
use foreign_types::ForeignTypeRef;
|
||||
use gl;
|
||||
use pathfinder_canvas::{CanvasFontContext, CanvasRenderingContext2D, FillStyle, LineJoin};
|
||||
use pathfinder_canvas::{Path2D, TextMetrics};
|
||||
use pathfinder_canvas::{CanvasFontContext, CanvasRenderingContext2D, FillStyle, LineJoin, Path2D};
|
||||
use pathfinder_canvas::{TextAlign, TextMetrics};
|
||||
use pathfinder_content::color::{ColorF, ColorU};
|
||||
use pathfinder_content::outline::ArcDirection;
|
||||
use pathfinder_content::stroke::LineCap;
|
||||
|
@ -43,13 +43,17 @@ use pathfinder_metal::MetalDevice;
|
|||
|
||||
// `canvas`
|
||||
|
||||
pub const PF_LINE_CAP_BUTT: u8 = 0;
|
||||
pub const PF_LINE_CAP_SQUARE: u8 = 1;
|
||||
pub const PF_LINE_CAP_ROUND: u8 = 2;
|
||||
pub const PF_LINE_CAP_BUTT: u8 = 0;
|
||||
pub const PF_LINE_CAP_SQUARE: u8 = 1;
|
||||
pub const PF_LINE_CAP_ROUND: u8 = 2;
|
||||
|
||||
pub const PF_LINE_JOIN_MITER: u8 = 0;
|
||||
pub const PF_LINE_JOIN_BEVEL: u8 = 1;
|
||||
pub const PF_LINE_JOIN_ROUND: u8 = 2;
|
||||
pub const PF_LINE_JOIN_MITER: u8 = 0;
|
||||
pub const PF_LINE_JOIN_BEVEL: u8 = 1;
|
||||
pub const PF_LINE_JOIN_ROUND: u8 = 2;
|
||||
|
||||
pub const PF_TEXT_ALIGN_LEFT: u8 = 0;
|
||||
pub const PF_TEXT_ALIGN_CENTER: u8 = 1;
|
||||
pub const PF_TEXT_ALIGN_RIGHT: u8 = 2;
|
||||
|
||||
// `content`
|
||||
|
||||
|
@ -73,6 +77,7 @@ pub type PFFillStyleRef = *mut FillStyle;
|
|||
pub type PFLineCap = u8;
|
||||
pub type PFLineJoin = u8;
|
||||
pub type PFArcDirection = u8;
|
||||
pub type PFTextAlign = u8;
|
||||
#[repr(C)]
|
||||
pub struct PFTextMetrics {
|
||||
pub width: f32,
|
||||
|
@ -290,6 +295,15 @@ pub unsafe extern "C" fn PFCanvasSetFontSize(canvas: PFCanvasRef, new_font_size:
|
|||
(*canvas).set_font_size(new_font_size)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PFCanvasSetTextAlign(canvas: PFCanvasRef, new_text_align: PFTextAlign) {
|
||||
(*canvas).set_text_align(match new_text_align {
|
||||
PF_TEXT_ALIGN_CENTER => TextAlign::Center,
|
||||
PF_TEXT_ALIGN_RIGHT => TextAlign::Right,
|
||||
_ => TextAlign::Left,
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PFCanvasSetFillStyle(canvas: PFCanvasRef, fill_style: PFFillStyleRef) {
|
||||
(*canvas).set_fill_style(*fill_style)
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "svg2pdf"
|
||||
version = "0.1.0"
|
||||
authors = ["Sebastian Köln <sebk@rynx.org>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
pathfinder_svg = { path = "../../svg" }
|
||||
pathfinder_pdf = { path = "../../pdf" }
|
||||
usvg = "*"
|
|
@ -0,0 +1,20 @@
|
|||
use std::fs::File;
|
||||
use std::io::{Read, BufWriter};
|
||||
use std::error::Error;
|
||||
use pathfinder_svg::BuiltSVG;
|
||||
use pathfinder_pdf::make_pdf;
|
||||
use usvg::{Tree, Options};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let input = args.next().expect("no input given");
|
||||
let output = args.next().expect("no output given");
|
||||
|
||||
let mut data = Vec::new();
|
||||
File::open(input)?.read_to_end(&mut data)?;
|
||||
let svg = BuiltSVG::from_tree(Tree::from_data(&data, &Options::default()).unwrap());
|
||||
|
||||
make_pdf(BufWriter::new(File::create(output)?), &svg.scene);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -868,6 +868,11 @@ impl MetalDevice {
|
|||
|
||||
render_command_encoder.use_resource(&data_buffer, MTLResourceUsage::Read);
|
||||
|
||||
// Metal expects the data buffer to remain live. (Issue #199.)
|
||||
// FIXME(pcwalton): When do we deallocate this? What are the expected
|
||||
// lifetime semantics?
|
||||
mem::forget(data_buffer);
|
||||
|
||||
if let Some(vertex_argument_buffer) = vertex_argument_buffer {
|
||||
let range = NSRange::new(0, vertex_argument_buffer.length());
|
||||
vertex_argument_buffer.did_modify_range(range);
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "pathfinder_pdf"
|
||||
version = "0.1.0"
|
||||
authors = ["Sebastian Köln <sebk@rynx.org>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
pathfinder_renderer = { path = "../renderer" }
|
||||
pathfinder_geometry = { path = "../geometry" }
|
||||
pathfinder_content = { path = "../content" }
|
||||
deflate = "*"
|
|
@ -0,0 +1,73 @@
|
|||
use pathfinder_renderer::{scene::Scene};
|
||||
use pathfinder_geometry::{vector::Vector2F, rect::RectF};
|
||||
use pathfinder_content::{outline::Outline, segment::{Segment, SegmentKind}, color::ColorF};
|
||||
use std::io::Write;
|
||||
|
||||
mod pdf;
|
||||
use pdf::Pdf;
|
||||
|
||||
pub struct PdfBuilder {
|
||||
pdf: Pdf
|
||||
}
|
||||
|
||||
impl PdfBuilder {
|
||||
pub fn new() -> PdfBuilder {
|
||||
PdfBuilder {
|
||||
pdf: Pdf::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_scene(&mut self, scene: &Scene) {
|
||||
let view_box = scene.view_box();
|
||||
self.pdf.add_page(view_box.size());
|
||||
|
||||
let height = view_box.size().y();
|
||||
let tr = |v: Vector2F| -> Vector2F {
|
||||
let r = v - view_box.origin();
|
||||
Vector2F::new(r.x(), height - r.y())
|
||||
};
|
||||
|
||||
for (paint, outline) in scene.paths() {
|
||||
self.pdf.set_fill_color(paint.color);
|
||||
|
||||
for contour in outline.contours() {
|
||||
for (segment_index, segment) in contour.iter().enumerate() {
|
||||
if segment_index == 0 {
|
||||
self.pdf.move_to(tr(segment.baseline.from()));
|
||||
}
|
||||
|
||||
match segment.kind {
|
||||
SegmentKind::None => {}
|
||||
SegmentKind::Line => self.pdf.line_to(tr(segment.baseline.to())),
|
||||
SegmentKind::Quadratic => {
|
||||
let current = segment.baseline.from();
|
||||
let c = segment.ctrl.from();
|
||||
let p = segment.baseline.to();
|
||||
let c1 = Vector2F::splat(2./3.) * c + Vector2F::splat(1./3.) * current;
|
||||
let c2 = Vector2F::splat(2./3.) * c + Vector2F::splat(1./3.) * p;
|
||||
self.pdf.cubic_to(c1, c2, p);
|
||||
}
|
||||
SegmentKind::Cubic => self.pdf.cubic_to(tr(segment.ctrl.from()), tr(segment.ctrl.to()), tr(segment.baseline.to()))
|
||||
}
|
||||
}
|
||||
|
||||
if contour.is_closed() {
|
||||
self.pdf.close();
|
||||
}
|
||||
}
|
||||
|
||||
// closes implicitly
|
||||
self.pdf.fill();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write<W: Write>(mut self, out: W) {
|
||||
self.pdf.write_to(out);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_pdf<W: Write>(mut writer: W, scene: &Scene) {
|
||||
let mut pdf = PdfBuilder::new();
|
||||
pdf.add_scene(scene);
|
||||
pdf.write(writer);
|
||||
}
|
|
@ -0,0 +1,293 @@
|
|||
//! This is a heavily modified version of the pdfpdf crate by Benjamin Kimock <kimockb@gmail.com> (aka. saethlin)
|
||||
|
||||
use pathfinder_geometry::{vector::Vector2F, rect::RectF};
|
||||
use pathfinder_content::color::ColorU;
|
||||
use std::io::{self, Write, Cursor, Seek};
|
||||
use deflate::Compression;
|
||||
|
||||
struct Counter<T> {
|
||||
inner: T,
|
||||
count: u64
|
||||
}
|
||||
impl<T> Counter<T> {
|
||||
pub fn new(inner: T) -> Counter<T> {
|
||||
Counter {
|
||||
inner,
|
||||
count: 0
|
||||
}
|
||||
}
|
||||
pub fn pos(&self) -> u64 {
|
||||
self.count
|
||||
}
|
||||
pub fn into_inner(self) -> T {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
impl<W: Write> Write for Counter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self.inner.write(buf) {
|
||||
Ok(n) => {
|
||||
self.count += n as u64;
|
||||
Ok(n)
|
||||
},
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
|
||||
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
|
||||
self.inner.write_all(buf)?;
|
||||
self.count += buf.len() as u64;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a PDF internal object
|
||||
struct PdfObject {
|
||||
contents: Vec<u8>,
|
||||
is_page: bool,
|
||||
is_xobject: bool,
|
||||
offset: Option<u64>,
|
||||
}
|
||||
|
||||
/// The top-level struct that represents a (partially) in-memory PDF file
|
||||
pub struct Pdf {
|
||||
page_buffer: Vec<u8>,
|
||||
objects: Vec<PdfObject>,
|
||||
page_size: Option<Vector2F>,
|
||||
compression: Option<Compression>,
|
||||
}
|
||||
|
||||
impl Default for Pdf {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Pdf {
|
||||
/// Create a new blank PDF document
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
page_buffer: Vec::new(),
|
||||
objects: vec![
|
||||
PdfObject {
|
||||
contents: Vec::new(),
|
||||
is_page: false,
|
||||
is_xobject: false,
|
||||
offset: None,
|
||||
},
|
||||
PdfObject {
|
||||
contents: Vec::new(),
|
||||
is_page: false,
|
||||
is_xobject: false,
|
||||
offset: None,
|
||||
},
|
||||
],
|
||||
page_size: None,
|
||||
compression: Some(Compression::Fast)
|
||||
}
|
||||
}
|
||||
|
||||
fn add_object(&mut self, data: Vec<u8>, is_page: bool, is_xobject: bool) -> usize {
|
||||
self.objects.push(PdfObject {
|
||||
contents: data,
|
||||
is_page,
|
||||
is_xobject,
|
||||
offset: None,
|
||||
});
|
||||
self.objects.len()
|
||||
}
|
||||
|
||||
/// Sets the compression level for this document
|
||||
/// Calls to this method do not affect data produced by operations before the last .add_page
|
||||
#[inline]
|
||||
pub fn set_compression(&mut self, compression: Option<Compression>) {
|
||||
self.compression = compression;
|
||||
}
|
||||
|
||||
/// Set the PDF clipping box for the current page
|
||||
#[inline]
|
||||
pub fn set_clipping_box(&mut self, rect: RectF) {
|
||||
let origin = rect.origin();
|
||||
let size = rect.size();
|
||||
writeln!(self.page_buffer, "{} {} {} {} re W n",
|
||||
origin.x(),
|
||||
origin.y(),
|
||||
size.x(),
|
||||
size.y()
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
/// Set the current line width
|
||||
#[inline]
|
||||
pub fn set_line_width(&mut self, width: f32) {
|
||||
writeln!(self.page_buffer, "{} w", width).unwrap();
|
||||
}
|
||||
|
||||
/// Set the color for all subsequent drawing operations
|
||||
#[inline]
|
||||
pub fn set_stroke_color(&mut self, color: ColorU) {
|
||||
let norm = |color| f32::from(color) / 255.0;
|
||||
writeln!(self.page_buffer, "{} {} {} RG",
|
||||
norm(color.r),
|
||||
norm(color.g),
|
||||
norm(color.b)
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
/// Set the color for all subsequent drawing operations
|
||||
#[inline]
|
||||
pub fn set_fill_color(&mut self, color: ColorU) {
|
||||
let norm = |color| f32::from(color) / 255.0;
|
||||
writeln!(self.page_buffer, "{} {} {} rg",
|
||||
norm(color.r),
|
||||
norm(color.g),
|
||||
norm(color.b)
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
/// Move to a new page in the PDF document
|
||||
#[inline]
|
||||
pub fn add_page(&mut self, size: Vector2F) {
|
||||
// Compress and write out the previous page if it exists
|
||||
if !self.page_buffer.is_empty() {
|
||||
self.end_page();
|
||||
self.page_buffer.clear();
|
||||
}
|
||||
|
||||
self.page_buffer
|
||||
.extend("/DeviceRGB cs /DeviceRGB CS\n1 j 1 J\n".bytes());
|
||||
self.page_size = Some(size);
|
||||
}
|
||||
|
||||
pub fn move_to(&mut self, p: Vector2F) {
|
||||
writeln!(self.page_buffer, "{} {} m", p.x(), p.y()).unwrap();
|
||||
}
|
||||
|
||||
pub fn line_to(&mut self, p: Vector2F) {
|
||||
writeln!(self.page_buffer, "{} {} l", p.x(), p.y()).unwrap();
|
||||
}
|
||||
|
||||
pub fn cubic_to(&mut self, c1: Vector2F, c2: Vector2F, p: Vector2F) {
|
||||
writeln!(self.page_buffer, "{} {} {} {} {} {} c", c1.x(), c1.y(), c2.x(), c2.y(), p.x(), p.y()).unwrap();
|
||||
}
|
||||
pub fn fill(&mut self) {
|
||||
writeln!(self.page_buffer, "f").unwrap();
|
||||
}
|
||||
|
||||
pub fn stroke(&mut self) {
|
||||
writeln!(self.page_buffer, "s").unwrap();
|
||||
}
|
||||
|
||||
pub fn close(&mut self) {
|
||||
writeln!(self.page_buffer, "h").unwrap();
|
||||
}
|
||||
/// Dump a page out to disk
|
||||
fn end_page(&mut self) {
|
||||
let size = match self.page_size.take() {
|
||||
Some(size) => size,
|
||||
None => return // no page started
|
||||
};
|
||||
let page_stream = if let Some(level) = self.compression {
|
||||
let compressed = deflate::deflate_bytes_zlib_conf(&self.page_buffer, level);
|
||||
let mut page = format!(
|
||||
"<< /Length {} /Filter [/FlateDecode] >>\nstream\n",
|
||||
compressed.len()
|
||||
)
|
||||
.into_bytes();
|
||||
page.extend_from_slice(&compressed);
|
||||
page.extend(b"endstream\n");
|
||||
page
|
||||
} else {
|
||||
let mut page = Vec::new();
|
||||
page.extend(format!("<< /Length {} >>\nstream\n", self.page_buffer.len()).bytes());
|
||||
page.extend(&self.page_buffer);
|
||||
page.extend(b"endstream\n");
|
||||
page
|
||||
};
|
||||
|
||||
// Create the stream object for this page
|
||||
let stream_object_id = self.add_object(page_stream, false, false);
|
||||
|
||||
// Create the page object, which describes settings for the whole page
|
||||
let mut page_object = b"<< /Type /Page\n \
|
||||
/Parent 2 0 R\n \
|
||||
/Resources <<\n"
|
||||
.to_vec();
|
||||
|
||||
for (idx, obj) in self.objects.iter().enumerate().filter(|&(_, o)| o.is_xobject) {
|
||||
write!(page_object, "/XObject {} 0 R ", idx+1).unwrap();
|
||||
}
|
||||
|
||||
write!(page_object,
|
||||
" >>\n \
|
||||
/MediaBox [0 0 {} {}]\n \
|
||||
/Contents {} 0 R\n\
|
||||
>>\n",
|
||||
size.x(), size.y(), stream_object_id
|
||||
).unwrap();
|
||||
self.add_object(page_object, true, false);
|
||||
}
|
||||
|
||||
/// Write the in-memory PDF representation to disk
|
||||
pub fn write_to<W>(&mut self, writer: W) -> io::Result<()> where W: Write {
|
||||
let mut out = Counter::new(writer);
|
||||
out.write_all(b"%PDF-1.7\n%\xB5\xED\xAE\xFB\n")?;
|
||||
|
||||
if !self.page_buffer.is_empty() {
|
||||
self.end_page();
|
||||
}
|
||||
|
||||
// Write out each object
|
||||
for (idx, obj) in self.objects.iter_mut().enumerate().skip(2) {
|
||||
obj.offset = Some(out.pos());
|
||||
write!(out, "{} 0 obj\n", idx+1)?;
|
||||
out.write_all(&obj.contents)?;
|
||||
out.write_all(b"endobj\n")?;
|
||||
}
|
||||
|
||||
// Write out the page tree object
|
||||
self.objects[1].offset = Some(out.pos());
|
||||
out.write_all(b"2 0 obj\n")?;
|
||||
out.write_all(b"<< /Type /Pages\n")?;
|
||||
write!(out,
|
||||
"/Count {}\n",
|
||||
self.objects.iter().filter(|o| o.is_page).count()
|
||||
)?;
|
||||
out.write_all(b"/Kids [")?;
|
||||
for (idx, obj) in self.objects.iter().enumerate().filter(|&(_, obj)| obj.is_page) {
|
||||
write!(out, "{} 0 R ", idx + 1)?;
|
||||
}
|
||||
out.write_all(b"] >>\nendobj\n")?;
|
||||
|
||||
// Write out the catalog dictionary object
|
||||
self.objects[0].offset = Some(out.pos());
|
||||
out.write_all(b"1 0 obj\n<< /Type /Catalog\n/Pages 2 0 R >>\nendobj\n")?;
|
||||
|
||||
// Write the cross-reference table
|
||||
let startxref = out.pos() + 1; // NOTE: apparently there's some 1-based indexing??
|
||||
out.write_all(b"xref\n")?;
|
||||
write!(out, "0 {}\n", self.objects.len() + 1)?;
|
||||
out.write_all(b"0000000000 65535 f \n")?;
|
||||
|
||||
for obj in &self.objects {
|
||||
write!(out, "{:010} 00000 f \n", obj.offset.unwrap())?;
|
||||
}
|
||||
|
||||
// Write the document trailer
|
||||
out.write_all(b"trailer\n")?;
|
||||
write!(out, "<< /Size {}\n", self.objects.len())?;
|
||||
out.write_all(b"/Root 1 0 R >>\n")?;
|
||||
|
||||
// Write the offset to the xref table
|
||||
write!(out, "startxref\n{}\n", startxref)?;
|
||||
|
||||
// Write the PDF EOF
|
||||
out.write_all(b"%%EOF")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -198,8 +198,28 @@ impl Scene {
|
|||
writeln!(writer, "</svg>")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn paths<'a>(&'a self) -> PathIter {
|
||||
PathIter {
|
||||
scene: self,
|
||||
pos: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct PathIter<'a> {
|
||||
scene: &'a Scene,
|
||||
pos: usize
|
||||
}
|
||||
impl<'a> Iterator for PathIter<'a> {
|
||||
type Item = (&'a Paint, &'a Outline);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let item = self.scene.paths.get(self.pos).map(|path_object| {
|
||||
(self.scene.paints.get(path_object.paint.0 as usize).unwrap(), &path_object.outline)
|
||||
});
|
||||
self.pos += 1;
|
||||
item
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PathObject {
|
||||
outline: Outline,
|
||||
|
|
Loading…
Reference in New Issue