Implement path stroking using the FreeType stroker.

I'm not too happy with this, as I discovered a segfault in FreeType that doesn't give me confidence
in this as a solution for the long term. Additionally, this exposes the problems in the partitioner
with lack of winding fill rule, proper handling of self-intersections, and splitting of paths at
their extrema. (I believe these problems should be fairly straightforward to handle, but we just
don't handle them yet.)
This commit is contained in:
Patrick Walton 2017-09-08 16:49:15 -07:00
parent 3e5b53f13c
commit c36896e337
6 changed files with 200 additions and 78 deletions

View File

@ -33,6 +33,9 @@ const BUILTIN_SVG_URI: string = "/svg/demo";
const DEFAULT_FILE: string = 'tiger';
/// The minimum size of a stroke.
const HAIRLINE_STROKE_WIDTH: number = 0.25;
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
none: NoAAStrategy,
ssaa: SSAAStrategy,
@ -56,13 +59,18 @@ interface AntialiasingStrategyTable {
ecaa: typeof ECAAStrategy;
}
interface PathInstance {
element: SVGPathElement;
stroke: number | 'fill';
}
class SVGDemoController extends DemoAppController<SVGDemoView> {
start() {
super.start();
this.svg = document.getElementById('pf-svg') as Element as SVGSVGElement;
this.pathElements = [];
this.pathInstances = [];
this.loadInitialFile();
}
@ -92,7 +100,7 @@ class SVGDemoController extends DemoAppController<SVGDemoView> {
this.svg.appendChild(kid);
// Scan for geometry elements.
this.pathElements.length = 0;
this.pathInstances.length = 0;
const queue: Array<Element> = [this.svg];
let element;
while ((element = queue.pop()) != null) {
@ -103,20 +111,30 @@ class SVGDemoController extends DemoAppController<SVGDemoView> {
kid = kid.previousSibling;
}
if (element instanceof SVGPathElement)
this.pathElements.push(element);
if (element instanceof SVGPathElement) {
const style = window.getComputedStyle(element);
if (style.fill !== 'none')
this.pathInstances.push({ element: element, stroke: 'fill' });
if (style.stroke !== 'none') {
this.pathInstances.push({
element: element,
stroke: parseInt(style.strokeWidth!),
});
}
}
}
// Extract, normalize, and transform the path data.
let pathData = [];
for (const element of this.pathElements) {
const request: any = { paths: [] };
for (const instance of this.pathInstances) {
const element = instance.element;
const svgCTM = element.getCTM();
const ctm = glmatrix.mat2d.fromValues(svgCTM.a, svgCTM.b,
svgCTM.c, svgCTM.d,
svgCTM.e, svgCTM.f);
glmatrix.mat2d.scale(ctm, ctm, [1.0, -1.0]);
pathData.push(element.getPathData({normalize: true}).map(segment => {
const segments = element.getPathData({normalize: true}).map(segment => {
const newValues = _.flatMap(_.chunk(segment.values, 2), coords => {
const point = glmatrix.vec2.create();
glmatrix.vec2.transformMat2d(point, coords, ctm);
@ -126,11 +144,16 @@ class SVGDemoController extends DemoAppController<SVGDemoView> {
type: segment.type,
values: newValues,
};
}));
}
});
// Build the partitioning request to the server.
const request = {paths: pathData.map(segments => ({segments: segments}))};
let kind;
if (instance.stroke === 'fill')
kind = 'Fill';
else
kind = { Stroke: Math.max(HAIRLINE_STROKE_WIDTH, instance.stroke) };
request.paths.push({ segments: segments, kind: kind });
}
// Make the request.
window.fetch(PARTITION_SVG_PATHS_ENDPOINT_URL, {
@ -157,13 +180,13 @@ class SVGDemoController extends DemoAppController<SVGDemoView> {
private meshesReceived() {
this.view.then(view => {
view.uploadPathMetadata(this.pathElements);
view.uploadPathMetadata(this.pathInstances);
view.attachMeshes([this.meshes]);
})
}
private svg: SVGSVGElement;
private pathElements: Array<SVGPathElement>;
private pathInstances: PathInstance[];
private meshes: PathfinderMeshData;
}
@ -192,18 +215,19 @@ class SVGDemoView extends PathfinderDemoView {
return this.destAllocatedSize;
}
uploadPathMetadata(elements: SVGPathElement[]) {
const pathColors = new Uint8Array(4 * (elements.length + 1));
const pathTransforms = new Float32Array(4 * (elements.length + 1));
for (let pathIndex = 0; pathIndex < elements.length; pathIndex++) {
uploadPathMetadata(instances: PathInstance[]) {
const pathColors = new Uint8Array(4 * (instances.length + 1));
const pathTransforms = new Float32Array(4 * (instances.length + 1));
for (let pathIndex = 0; pathIndex < instances.length; pathIndex++) {
const startOffset = (pathIndex + 1) * 4;
// Set color.
const style = window.getComputedStyle(elements[pathIndex]);
const fillColor: number[] =
style.fill === 'none' ? [0, 0, 0, 0] : parseColor(style.fill).rgba;
pathColors.set(fillColor.slice(0, 3), startOffset);
pathColors[startOffset + 3] = fillColor[3] * 255;
const style = window.getComputedStyle(instances[pathIndex].element);
const property = instances[pathIndex].stroke === 'fill' ? 'fill' : 'stroke';
const color: number[] =
style[property] === 'none' ? [0, 0, 0, 0] : parseColor(style[property]).rgba;
pathColors.set(color.slice(0, 3), startOffset);
pathColors[startOffset + 3] = color[3] * 255;
// TODO(pcwalton): Set transform.
pathTransforms.set([1, 1, 0, 0], startOffset);

View File

@ -4,6 +4,7 @@ version = "0.1.0"
authors = ["Patrick Walton <pcwalton@mimiga.net>"]
[dependencies]
app_units = "0.5"
base64 = "0.6"
bincode = "0.8"
env_logger = "0.3"

View File

@ -26,7 +26,8 @@ use bincode::Infinite;
use euclid::{Point2D, Size2D, Transform2D};
use pathfinder_font_renderer::{FontContext, FontInstanceKey, FontKey, GlyphKey};
use pathfinder_partitioner::partitioner::Partitioner;
use pathfinder_path_utils::{Endpoint, PathBuffer, Subpath, Transform2DPathStream};
use pathfinder_path_utils::stroke;
use pathfinder_path_utils::{PathBuffer, PathSegment, Transform2DPathStream};
use rocket::http::{ContentType, Status};
use rocket::request::Request;
use rocket::response::{NamedFile, Redirect, Responder, Response};
@ -227,6 +228,13 @@ struct PartitionSvgPathsRequest {
#[derive(Clone, Serialize, Deserialize)]
struct PartitionSvgPath {
segments: Vec<PartitionSvgPathSegment>,
kind: PartitionSvgPathKind,
}
#[derive(Clone, Copy, Serialize, Deserialize)]
enum PartitionSvgPathKind {
Fill,
Stroke(f32),
}
#[derive(Clone, Serialize, Deserialize)]
@ -457,32 +465,19 @@ fn partition_svg_paths(request: Json<PartitionSvgPathsRequest>)
let mut path_buffer = PathBuffer::new();
let mut paths = vec![];
for path in &request.paths {
let mut stream = vec![];
let first_subpath_index = path_buffer.subpaths.len() as u32;
let mut first_endpoint_index_in_subpath = path_buffer.endpoints.len();
for segment in &path.segments {
match segment.kind {
'M' => {
if first_endpoint_index_in_subpath < path_buffer.endpoints.len() {
path_buffer.subpaths.push(Subpath {
first_endpoint_index: first_endpoint_index_in_subpath as u32,
last_endpoint_index: path_buffer.endpoints.len() as u32,
});
first_endpoint_index_in_subpath = path_buffer.endpoints.len();
}
path_buffer.endpoints.push(Endpoint {
position: Point2D::new(segment.values[0] as f32, segment.values[1] as f32),
control_point_index: u32::MAX,
subpath_index: path_buffer.subpaths.len() as u32,
})
stream.push(PathSegment::MoveTo(Point2D::new(segment.values[0] as f32,
segment.values[1] as f32)))
}
'L' => {
path_buffer.endpoints.push(Endpoint {
position: Point2D::new(segment.values[0] as f32, segment.values[1] as f32),
control_point_index: u32::MAX,
subpath_index: path_buffer.subpaths.len() as u32,
})
stream.push(PathSegment::LineTo(Point2D::new(segment.values[0] as f32,
segment.values[1] as f32)))
}
'C' => {
// FIXME(pcwalton): Do real cubic-to-quadratic conversion.
@ -491,25 +486,24 @@ fn partition_svg_paths(request: Json<PartitionSvgPathsRequest>)
let control_point_1 = Point2D::new(segment.values[2] as f32,
segment.values[3] as f32);
let control_point = control_point_0.lerp(control_point_1, 0.5);
path_buffer.endpoints.push(Endpoint {
position: Point2D::new(segment.values[4] as f32, segment.values[5] as f32),
control_point_index: path_buffer.control_points.len() as u32,
subpath_index: path_buffer.subpaths.len() as u32,
});
path_buffer.control_points.push(control_point);
}
'Z' => {
path_buffer.subpaths.push(Subpath {
first_endpoint_index: first_endpoint_index_in_subpath as u32,
last_endpoint_index: path_buffer.endpoints.len() as u32,
});
first_endpoint_index_in_subpath = path_buffer.endpoints.len();
stream.push(PathSegment::CurveTo(control_point,
Point2D::new(segment.values[4] as f32,
segment.values[5] as f32)))
}
'Z' => stream.push(PathSegment::ClosePath),
_ => return Json(Err(PartitionSvgPathsError::UnknownSvgPathSegmentType)),
}
}
match path.kind {
PartitionSvgPathKind::Fill => path_buffer.add_stream(stream.into_iter()),
PartitionSvgPathKind::Stroke(stroke_width) => {
stroke::stroke(&mut path_buffer, stream.into_iter(), stroke_width)
}
}
let last_subpath_index = path_buffer.subpaths.len() as u32;
paths.push(SubpathRange {
start: first_subpath_index,
end: last_subpath_index,

View File

@ -97,29 +97,13 @@ impl FontContext {
self.load_glyph(font_instance, glyph_key).ok_or(()).map(|glyph_slot| {
unsafe {
GlyphOutline {
stream: OutlineStream::new(&(*glyph_slot).outline),
stream: OutlineStream::new(&(*glyph_slot).outline, 72.0),
phantom: PhantomData,
}
}
})
}
/*
pub fn push_glyph_outline(&self,
font_instance: &FontInstanceKey,
glyph_key: &GlyphKey,
path_buffer: &mut PathBuffer)
-> Result<(), ()> {
self.load_glyph(font_instance, glyph_key).ok_or(()).map(|glyph_slot| {
unsafe {
let outline = &(*glyph_slot).outline;
for contour_index in 0..outline.n_contours as u16 {
path_buffer.add_subpath_from_stream(OutlineIter::new(outline, contour_index))
}
}
})
}*/
fn load_glyph(&self, font_instance: &FontInstanceKey, glyph_key: &GlyphKey)
-> Option<FT_GlyphSlot> {
let face = match self.faces.get(&font_instance.font_key) {

View File

@ -9,31 +9,31 @@
// except according to those terms.
use euclid::Point2D;
use freetype_sys::{FT_Outline, FT_Vector};
use freetype_sys::{FT_Fixed, FT_Outline, FT_Pos, FT_Vector};
use PathSegment;
const FREETYPE_POINT_ON_CURVE: i8 = 0x01;
const DPI: f32 = 72.0;
pub struct OutlineStream<'a> {
outline: &'a FT_Outline,
point_index: u16,
contour_index: u16,
first_position_of_subpath: Point2D<f32>,
first_point_index_of_contour: bool,
dpi: f32,
}
impl<'a> OutlineStream<'a> {
#[inline]
pub unsafe fn new(outline: &FT_Outline) -> OutlineStream {
pub unsafe fn new(outline: &FT_Outline, dpi: f32) -> OutlineStream {
OutlineStream {
outline: outline,
point_index: 0,
contour_index: 0,
first_position_of_subpath: Point2D::zero(),
first_point_index_of_contour: true,
dpi: dpi,
}
}
@ -43,7 +43,7 @@ impl<'a> OutlineStream<'a> {
let point_offset = self.point_index as isize;
let position = ft_vector_to_f32(*self.outline.points.offset(point_offset));
let tag = *self.outline.tags.offset(point_offset);
(position * DPI, tag)
(position * self.dpi, tag)
}
}
}
@ -111,3 +111,16 @@ impl<'a> Iterator for OutlineStream<'a> {
fn ft_vector_to_f32(ft_vector: FT_Vector) -> Point2D<f32> {
Point2D::new(ft_vector.x as f32 / 64.0, ft_vector.y as f32 / 64.0)
}
#[inline]
pub fn f32_to_ft_vector(point: &Point2D<f32>) -> FT_Vector {
FT_Vector {
x: (point.x * 64.0).round() as FT_Pos,
y: (point.y * 64.0).round() as FT_Pos,
}
}
#[inline]
pub fn f32_to_26_6_ft_fixed(length: f32) -> FT_Fixed {
(length * 64.0).round() as FT_Fixed
}

View File

@ -8,4 +8,110 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
// TODO(pcwalton)
use freetype_sys::{FT_Init_FreeType, FT_Library, FT_Outline, FT_STROKER_LINECAP_BUTT, FT_Stroker};
use freetype_sys::{FT_STROKER_LINEJOIN_ROUND, FT_Stroker_BeginSubPath, FT_Stroker_ConicTo};
use freetype_sys::{FT_Stroker_Done, FT_Stroker_EndSubPath, FT_Stroker_Export};
use freetype_sys::{FT_Stroker_GetCounts, FT_Stroker_LineTo, FT_Stroker_New, FT_Stroker_Set};
use freetype_sys::{FT_UInt, FT_Vector};
use std::i16;
use freetype::{self, OutlineStream};
use {PathBuffer, PathSegment};
const EPSILON_POSITION_OFFSET: i64 = 8;
thread_local! {
pub static FREETYPE_LIBRARY: FT_Library = unsafe {
let mut library = 0 as FT_Library;
assert!(FT_Init_FreeType(&mut library) == 0);
library
};
}
pub fn stroke<I>(output: &mut PathBuffer, stream: I, stroke_width: f32)
where I: Iterator<Item = PathSegment> {
unsafe {
let mut stroker = 0 as FT_Stroker;
FREETYPE_LIBRARY.with(|&library| {
assert!(FT_Stroker_New(library, &mut stroker) == 0);
});
// TODO(pcwalton): Make line caps and line join customizable.
let stroke_width = freetype::f32_to_26_6_ft_fixed(stroke_width);
FT_Stroker_Set(stroker,
stroke_width,
FT_STROKER_LINECAP_BUTT,
FT_STROKER_LINEJOIN_ROUND,
0);
let mut first_position_in_subpath = None;
for segment in stream {
match segment {
PathSegment::MoveTo(position) => {
if first_position_in_subpath.is_some() {
assert!(FT_Stroker_EndSubPath(stroker) == 0);
}
let mut position = freetype::f32_to_ft_vector(&position);
first_position_in_subpath = Some(position);
assert!(FT_Stroker_BeginSubPath(stroker, &mut position, 1) == 0);
// FIXME(pcwalton): This is a really bad hack to guard against segfaults in
// FreeType when paths are empty (e.g. moveto plus closepath).
let mut epsilon_position = FT_Vector {
x: position.x + EPSILON_POSITION_OFFSET,
y: position.y,
};
assert!(FT_Stroker_LineTo(stroker, &mut epsilon_position) == 0);
}
PathSegment::LineTo(position) => {
let mut position = freetype::f32_to_ft_vector(&position);
assert!(FT_Stroker_LineTo(stroker, &mut position) == 0);
}
PathSegment::CurveTo(control_point_position, endpoint_position) => {
let mut control_point_position =
freetype::f32_to_ft_vector(&control_point_position);
let mut endpoint_position = freetype::f32_to_ft_vector(&endpoint_position);
assert!(FT_Stroker_ConicTo(stroker,
&mut control_point_position,
&mut endpoint_position) == 0);
}
PathSegment::ClosePath => {
if let Some(mut first_position_in_subpath) = first_position_in_subpath {
assert!(FT_Stroker_LineTo(stroker, &mut first_position_in_subpath) == 0);
assert!(FT_Stroker_EndSubPath(stroker) == 0);
}
first_position_in_subpath = None;
}
}
}
if first_position_in_subpath.is_some() {
assert!(FT_Stroker_EndSubPath(stroker) == 0)
}
let (mut anum_points, mut anum_contours) = (0, 0);
assert!(FT_Stroker_GetCounts(stroker, &mut anum_points, &mut anum_contours) == 0);
assert!(anum_points <= i16::MAX as FT_UInt && anum_contours <= i16::MAX as FT_UInt);
let mut outline_points = vec![FT_Vector { x: 0, y: 0 }; anum_points as usize];
let mut outline_tags = vec![0; anum_points as usize];
let mut outline_contours = vec![0; anum_contours as usize];
let mut outline = FT_Outline {
n_contours: 0,
n_points: 0,
points: outline_points.as_mut_ptr(),
tags: outline_tags.as_mut_ptr(),
contours: outline_contours.as_mut_ptr(),
flags: 0,
};
FT_Stroker_Export(stroker, &mut outline);
FT_Stroker_Done(stroker);
output.add_stream(OutlineStream::new(&outline, 1.0));
}
}