Use librsvg/Cairo/Pixman to render SVG reference images.

The scale and colors aren't correct yet, but this is a start.
This commit is contained in:
Patrick Walton 2017-12-21 17:20:01 -08:00
parent c2d89aba91
commit b6236ac835
3 changed files with 133 additions and 32 deletions

View File

@ -41,7 +41,11 @@ const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
xcaa: AdaptiveMonochromeXCAAStrategy,
};
const RENDER_REFERENCE_URI: string = "/render-reference";
const RENDER_REFERENCE_URIS: PerTestType<string> = {
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<ReferenceTestView> {
textRun: TextRun | null;
svgLoader: SVGLoader;
builtinSvgName: string | null;
referenceCanvas: HTMLCanvasElement;
@ -162,7 +167,7 @@ class ReferenceTestAppController extends DemoAppController<ReferenceTestView> {
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<ReferenceTestView> {
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<ReferenceTestView> {
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<ReferenceTestView> {
}
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<ReferenceTestView> {
// 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<ReferenceTestView> {
}
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<ReferenceTestView> {
subpixel: !!row.Subpixel,
});
}
return fontNames.map(fontName => {
return {
font: fontName,
@ -412,11 +419,11 @@ class ReferenceTestAppController extends DemoAppController<ReferenceTestView> {
});
}
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<ReferenceTestView> {
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<ReferenceTestView> {
});
}
private loadReference(): Promise<void> {
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<void> {
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);
});
}
}

View File

@ -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"

View File

@ -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<Arc<Vec<u8>>, FontErr
}
}
// Fetches the SVG data.
fn svg_data_from_request(builtin_svg_name: &str) -> Result<Arc<Vec<u8>>, 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<PartitionSvgPathsRequest>)
})
}
#[post("/render-reference", format = "application/json", data = "<request>")]
fn render_reference(request: Json<RenderReferenceRequest>)
-> Result<ReferenceImage, FontError> {
#[post("/render-reference/text", format = "application/json", data = "<request>")]
fn render_reference_text(request: Json<RenderTextReferenceRequest>)
-> Result<ReferenceImage, FontError> {
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<RenderReferenceRequest>)
// 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<RenderReferenceRequest>)
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<RenderReferenceRequest>)
Ok(reference_image)
}
#[post("/render-reference/svg", format = "application/json", data = "<request>")]
fn render_reference_svg(request: Json<RenderSvgReferenceRequest>)
-> Result<ReferenceImage, SvgError> {
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<NamedFile> {
@ -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,