diff --git a/Cargo.lock b/Cargo.lock index 215c4503..8fe5c64a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 5d8e378e..174b1bcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/c/src/lib.rs b/c/src/lib.rs index 6b6235c1..7b2e2ef9 100644 --- a/c/src/lib.rs +++ b/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) diff --git a/examples/svg2pdf/Cargo.toml b/examples/svg2pdf/Cargo.toml new file mode 100644 index 00000000..e656de8b --- /dev/null +++ b/examples/svg2pdf/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "svg2pdf" +version = "0.1.0" +authors = ["Sebastian Köln "] +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 = "*" diff --git a/examples/svg2pdf/src/main.rs b/examples/svg2pdf/src/main.rs new file mode 100644 index 00000000..9dd7b88e --- /dev/null +++ b/examples/svg2pdf/src/main.rs @@ -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> { + 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(()) +} diff --git a/metal/src/lib.rs b/metal/src/lib.rs index 13f157bc..41a11c80 100644 --- a/metal/src/lib.rs +++ b/metal/src/lib.rs @@ -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); diff --git a/pdf/Cargo.toml b/pdf/Cargo.toml new file mode 100644 index 00000000..d9b5ad70 --- /dev/null +++ b/pdf/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "pathfinder_pdf" +version = "0.1.0" +authors = ["Sebastian Köln "] +edition = "2018" + +[dependencies] +pathfinder_renderer = { path = "../renderer" } +pathfinder_geometry = { path = "../geometry" } +pathfinder_content = { path = "../content" } +deflate = "*" diff --git a/pdf/src/lib.rs b/pdf/src/lib.rs new file mode 100644 index 00000000..fcab6a75 --- /dev/null +++ b/pdf/src/lib.rs @@ -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(mut self, out: W) { + self.pdf.write_to(out); + } +} + +pub fn make_pdf(mut writer: W, scene: &Scene) { + let mut pdf = PdfBuilder::new(); + pdf.add_scene(scene); + pdf.write(writer); +} diff --git a/pdf/src/pdf.rs b/pdf/src/pdf.rs new file mode 100644 index 00000000..7de2ec3e --- /dev/null +++ b/pdf/src/pdf.rs @@ -0,0 +1,293 @@ +//! This is a heavily modified version of the pdfpdf crate by Benjamin Kimock (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 { + inner: T, + count: u64 +} +impl Counter { + pub fn new(inner: T) -> Counter { + Counter { + inner, + count: 0 + } + } + pub fn pos(&self) -> u64 { + self.count + } + pub fn into_inner(self) -> T { + self.inner + } +} +impl Write for Counter { + fn write(&mut self, buf: &[u8]) -> io::Result { + 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, + is_page: bool, + is_xobject: bool, + offset: Option, +} + +/// The top-level struct that represents a (partially) in-memory PDF file +pub struct Pdf { + page_buffer: Vec, + objects: Vec, + page_size: Option, + compression: Option, +} + +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, 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) { + 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(&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(()) + } +} diff --git a/renderer/src/scene.rs b/renderer/src/scene.rs index 328c3f82..f441d02f 100644 --- a/renderer/src/scene.rs +++ b/renderer/src/scene.rs @@ -198,8 +198,28 @@ impl Scene { writeln!(writer, "")?; 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 { + 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,