From b6236ac83586421666b530e3807f76a21f6475e8 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Thu, 21 Dec 2017 17:20:01 -0800 Subject: [PATCH] Use librsvg/Cairo/Pixman to render SVG reference images. The scale and colors aren't correct yet, but this is a start. --- demo/client/src/reference-test.ts | 65 ++++++++++++++------- demo/server/Cargo.toml | 5 ++ demo/server/src/main.rs | 95 +++++++++++++++++++++++++++---- 3 files changed, 133 insertions(+), 32 deletions(-) diff --git a/demo/client/src/reference-test.ts b/demo/client/src/reference-test.ts index 9ae7c869..34fa41ac 100644 --- a/demo/client/src/reference-test.ts +++ b/demo/client/src/reference-test.ts @@ -41,7 +41,11 @@ const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = { xcaa: AdaptiveMonochromeXCAAStrategy, }; -const RENDER_REFERENCE_URI: string = "/render-reference"; +const RENDER_REFERENCE_URIS: PerTestType = { + font: "/render-reference/text", + svg: "/render-reference/svg", +}; + const TEST_DATA_URI: string = "/test-data/reference-test-text.csv"; const SSIM_TOLERANCE: number = 0.01; @@ -95,6 +99,7 @@ class ReferenceTestAppController extends DemoAppController { textRun: TextRun | null; svgLoader: SVGLoader; + builtinSvgName: string | null; referenceCanvas: HTMLCanvasElement; @@ -162,7 +167,7 @@ class ReferenceTestAppController extends DemoAppController { this.referenceRendererSelect = unwrapNull(document.getElementById('pf-font-reference-renderer')) as HTMLSelectElement; this.referenceRendererSelect.addEventListener('change', () => { - this.view.then(view => this.runSingleTest()); + this.view.then(view => this.runSingleTest(view)); }, false); super.start(); @@ -177,13 +182,13 @@ class ReferenceTestAppController extends DemoAppController { this.fontSizeInput = unwrapNull(document.getElementById('pf-font-size')) as HTMLInputElement; this.fontSizeInput.addEventListener('change', () => { - this.view.then(view => this.runSingleTest()); + this.view.then(view => this.runSingleTest(view)); }, false); this.characterInput = unwrapNull(document.getElementById('pf-character')) as HTMLInputElement; this.characterInput.addEventListener('change', () => { - this.view.then(view => this.runSingleTest()); + this.view.then(view => this.runSingleTest(view)); }, false); this.aaLevelGroup = unwrapNull(document.getElementById('pf-aa-level-group')) as @@ -244,7 +249,7 @@ class ReferenceTestAppController extends DemoAppController { this.loadInitialFile(this.builtinFileURI); } - runNextTestIfNecessary(tests: ReferenceTestGroup[]): void { + runNextTestIfNecessary(view: ReferenceTestView, tests: ReferenceTestGroup[]): void { if (this.currentTestGroupIndex == null || this.currentTestCaseIndex == null || this.currentGlobalTestCaseIndex == null) { return; @@ -266,7 +271,7 @@ class ReferenceTestAppController extends DemoAppController { } this.loadFontForTestGroupIfNecessary(tests).then(() => { - this.setOptionsForCurrentTest(tests).then(() => this.runSingleTest()); + this.setOptionsForCurrentTest(tests).then(() => this.runSingleTest(view)); }); } @@ -328,7 +333,7 @@ class ReferenceTestAppController extends DemoAppController { // Don't automatically run the test unless this is a custom test. if (this.currentGlobalTestCaseIndex == null) - this.runSingleTest(); + this.view.then(view => this.runSingleTest(view)); } private textFileLoaded(fileData: ArrayBuffer, builtinName: string | null): void { @@ -337,6 +342,7 @@ class ReferenceTestAppController extends DemoAppController { } private svgFileLoaded(fileData: ArrayBuffer, builtinName: string | null): void { + this.builtinSvgName = builtinName; this.svgLoader = new SVGLoader; this.svgLoader.loadFile(fileData); } @@ -381,6 +387,7 @@ class ReferenceTestAppController extends DemoAppController { subpixel: !!row.Subpixel, }); } + return fontNames.map(fontName => { return { font: fontName, @@ -412,11 +419,11 @@ class ReferenceTestAppController extends DemoAppController { }); } - private runSingleTest(): void { + private runSingleTest(view: ReferenceTestView): void { if (this.currentTestType === 'font') this.setUpTextRun(); - this.loadReference().then(() => this.loadRendering()); + this.loadReference(view).then(() => this.loadRendering()); } private runTests(): void { @@ -428,7 +435,7 @@ class ReferenceTestAppController extends DemoAppController { this.currentGlobalTestCaseIndex = 0; this.loadFontForTestGroupIfNecessary(tests).then(() => { - this.setOptionsForCurrentTest(tests).then(() => this.runSingleTest()); + this.setOptionsForCurrentTest(tests).then(() => this.runSingleTest(view)); }); }); }); @@ -514,18 +521,32 @@ class ReferenceTestAppController extends DemoAppController { }); } - private loadReference(): Promise { - const request = { - face: { - Builtin: unwrapNull(this.font).builtinFontName, - }, - fontIndex: 0, - glyph: this.glyphStore.glyphIDs[0], - pointSize: this.currentFontSize, - renderer: this.currentReferenceRenderer, - }; + private loadReference(view: ReferenceTestView): Promise { + let request; + switch (this.currentTestType) { + case 'font': + request = { + face: { + Builtin: unwrapNull(this.font).builtinFontName, + }, + fontIndex: 0, + glyph: this.glyphStore.glyphIDs[0], + pointSize: this.currentFontSize, + renderer: this.currentReferenceRenderer, + }; + break; + case 'svg': + // TODO(pcwalton): Custom SVGs. + // TODO(pcwalton): Detect scale. + request = { + name: unwrapNull(this.builtinSvgName), + renderer: 'pixman', + scale: 1.0, + }; + break; + } - return window.fetch(RENDER_REFERENCE_URI, { + return window.fetch(RENDER_REFERENCE_URIS[this.currentTestType], { body: JSON.stringify(request), headers: {'Content-Type': 'application/json'} as any, method: 'POST', @@ -630,7 +651,7 @@ class ReferenceTestView extends DemoView { const differenceImage = generateDifferenceImage(referenceImage, renderedImage); this.appController.recordSSIMResult(tests, ssimResult); this.appController.drawDifferenceImage(differenceImage); - this.appController.runNextTestIfNecessary(tests); + this.appController.runNextTestIfNecessary(this, tests); }); } } diff --git a/demo/server/Cargo.toml b/demo/server/Cargo.toml index 7485e0c5..16326eac 100644 --- a/demo/server/Cargo.toml +++ b/demo/server/Cargo.toml @@ -16,10 +16,15 @@ lru-cache = "0.1" rocket = "0.3" rocket_codegen = "0.3" rocket_contrib = "0.3" +rsvg = "0.3" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" +[dependencies.cairo-rs] +version = "0.3" +features = ["png"] + [dependencies.fontsan] git = "https://github.com/servo/fontsan.git" diff --git a/demo/server/src/main.rs b/demo/server/src/main.rs index 848229c8..545133fd 100644 --- a/demo/server/src/main.rs +++ b/demo/server/src/main.rs @@ -13,6 +13,7 @@ extern crate app_units; extern crate base64; +extern crate cairo; extern crate env_logger; extern crate euclid; extern crate fontsan; @@ -23,6 +24,7 @@ extern crate pathfinder_partitioner; extern crate pathfinder_path_utils; extern crate rocket; extern crate rocket_contrib; +extern crate rsvg; #[macro_use] extern crate lazy_static; @@ -30,7 +32,8 @@ extern crate lazy_static; extern crate serde_derive; use app_units::Au; -use euclid::{Point2D, Transform2D}; +use cairo::{Format, ImageSurface}; +use euclid::{Point2D, Size2D, Transform2D}; use image::{DynamicImage, ImageBuffer, ImageFormat, ImageRgba8}; use lru_cache::LruCache; use pathfinder_font_renderer::{FontContext, FontInstance, FontKey, GlyphImage}; @@ -46,6 +49,7 @@ use rocket::http::{ContentType, Header, Status}; use rocket::request::Request; use rocket::response::{NamedFile, Redirect, Responder, Response}; use rocket_contrib::json::Json; +use rsvg::{Handle, HandleExt}; use std::fs::File; use std::io::{self, Cursor, Read}; use std::ops::Range; @@ -130,15 +134,21 @@ enum FontRequestFace { } #[derive(Clone, Copy, Serialize, Deserialize)] -enum ReferenceRenderer { +enum ReferenceTextRenderer { #[serde(rename = "freetype")] FreeType, #[serde(rename = "core-graphics")] CoreGraphics, } +#[derive(Clone, Copy, Serialize, Deserialize)] +enum ReferenceSvgRenderer { + #[serde(rename = "pixman")] + Pixman, +} + #[derive(Clone, Serialize, Deserialize)] -struct RenderReferenceRequest { +struct RenderTextReferenceRequest { face: FontRequestFace, #[serde(rename = "fontIndex")] font_index: u32, @@ -146,7 +156,14 @@ struct RenderReferenceRequest { #[serde(rename = "pointSize")] point_size: f64, - renderer: ReferenceRenderer, + renderer: ReferenceTextRenderer, +} + +#[derive(Clone, Serialize, Deserialize)] +struct RenderSvgReferenceRequest { + name: String, + scale: f64, + renderer: ReferenceSvgRenderer, } #[derive(Clone, Copy, Serialize, Deserialize)] @@ -172,6 +189,13 @@ enum FontError { Unimplemented, } +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +enum SvgError { + UnknownBuiltinSvg, + LoadingFailed, + ImageWritingFailed, +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize)] enum PartitionSvgPathsError { UnknownSvgPathCommandType, @@ -327,6 +351,21 @@ fn otf_data_from_request(face: &FontRequestFace) -> Result>, FontErr } } +// Fetches the SVG data. +fn svg_data_from_request(builtin_svg_name: &str) -> Result>, SvgError> { + // Read in the builtin SVG. + match BUILTIN_SVGS.iter().filter(|& &(name, _)| name == builtin_svg_name).next() { + Some(&(_, path)) => { + let mut data = vec![]; + File::open(path).expect("Couldn't find builtin SVG!") + .read_to_end(&mut data) + .expect("Couldn't read builtin SVG!"); + Ok(Arc::new(data)) + } + None => return Err(SvgError::UnknownBuiltinSvg), + } +} + #[cfg(target_os = "macos")] fn rasterize_glyph_with_core_graphics(font_key: &FontKey, font_index: u32, @@ -536,9 +575,9 @@ fn partition_svg_paths(request: Json) }) } -#[post("/render-reference", format = "application/json", data = "")] -fn render_reference(request: Json) - -> Result { +#[post("/render-reference/text", format = "application/json", data = "")] +fn render_reference_text(request: Json) + -> Result { let font_key = FontKey::new(); let otf_data = try!(otf_data_from_request(&request.face)); let font_instance = FontInstance { @@ -549,7 +588,7 @@ fn render_reference(request: Json) // Rasterize the glyph using the right rasterizer. let glyph_image = match request.renderer { - ReferenceRenderer::FreeType => { + ReferenceTextRenderer::FreeType => { let mut font_context = try!(FontContext::new().map_err(|_| FontError::FontLoadingFailed)); try!(font_context.add_font_from_memory(&font_key, otf_data, request.font_index) @@ -559,7 +598,7 @@ fn render_reference(request: Json) true) .map_err(|_| FontError::RasterizationFailed)) } - ReferenceRenderer::CoreGraphics => { + ReferenceTextRenderer::CoreGraphics => { try!(rasterize_glyph_with_core_graphics(&font_key, request.font_index, otf_data, @@ -579,6 +618,41 @@ fn render_reference(request: Json) Ok(reference_image) } +#[post("/render-reference/svg", format = "application/json", data = "")] +fn render_reference_svg(request: Json) + -> Result { + let svg_data = try!(svg_data_from_request(&request.name)); + let svg_string = String::from_utf8_lossy(&*svg_data); + let svg_handle = try!(Handle::new_from_str(&svg_string).map_err(|_| SvgError::LoadingFailed)); + + let svg_dimensions = svg_handle.get_dimensions(); + let mut image_size = Size2D::new(svg_dimensions.width as f64, svg_dimensions.height as f64); + image_size = (image_size * request.scale).ceil(); + + // Rasterize the SVG using the appropriate rasterizer. + let mut surface = ImageSurface::create(Format::ARgb32, + image_size.width as i32, + image_size.height as i32).unwrap(); + + { + let cairo_context = cairo::Context::new(&surface); + cairo_context.scale(request.scale, request.scale); + svg_handle.render_cairo(&cairo_context); + } + + let image_data = (*surface.get_data().unwrap()).to_vec(); + let image_buffer = match ImageBuffer::from_raw(image_size.width as u32, + image_size.height as u32, + image_data) { + None => return Err(SvgError::ImageWritingFailed), + Some(image_buffer) => image_buffer, + }; + + Ok(ReferenceImage { + image: ImageRgba8(image_buffer), + }) +} + // Static files #[get("/")] fn static_index() -> io::Result { @@ -715,7 +789,8 @@ fn main() { rocket.mount("/", routes![ partition_font, partition_svg_paths, - render_reference, + render_reference_text, + render_reference_svg, static_index, static_demo_text, static_demo_svg,