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:
parent
3e5b53f13c
commit
c36896e337
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue