Commit edd5ef64 authored by RazrFalcon's avatar RazrFalcon

Added initial filters support.

parent f8db2209
......@@ -29,6 +29,7 @@ This changelog also contains important changes in dependencies.
- (qt-api) SVG from QByteArray loading when data is invalid.
- (usvg) `display` attribute processing.
- (usvg) Recursive `mask` resolving.
- (svgdom) Namespaces resolving.
- (usvg) `inherit` attribute value resolving.
### Removed
......
......@@ -29,8 +29,10 @@ travis-ci = { repository = "RazrFalcon/resvg" }
[dependencies]
log = "0.4.5"
palette = { version = "0.4.1", default-features = false }
rgb = "0.8.9"
#usvg = "0.2"
usvg = { git = "https://github.com/RazrFalcon/usvg", rev = "14342a5" }
usvg = { git = "https://github.com/RazrFalcon/usvg", rev = "99bfc15" }
#usvg = { path = "../usvg" }
unicode-segmentation = "1.2.1"
......
......@@ -63,7 +63,7 @@ of the SVG Tiny 1.2 subset. In simple terms - it correctly renders only primitiv
*resvg* is aiming to support only the [static](http://www.w3.org/TR/SVG11/feature#SVG-static)
SVG subset. E.g. no `a`, `script`, `view`, `cursor` elements, no events and no animations.
Also, `filter`, `marker`, `textPath` and
Also, `marker`, `textPath` and
[embedded fonts](https://www.w3.org/TR/SVG11/feature#Font) are not yet implemented.
Results of the static subset of the [SVG test suite](https://www.w3.org/Graphics/SVG/Test/20110816/):
......
......@@ -4,8 +4,17 @@ List of features required from the 2D graphics library to implement a backend fo
- Composition modes:
- Clear
- Darken
- DestinationIn
- DestinationOut
- Lighten
- Multiply
- Screen
- SourceAtop
- SourceIn
- SourceOut
- SourceOver
- Xor
- Filling:
- With color
- With linear or radial gradient
......@@ -34,7 +43,7 @@ List of features required from the 2D graphics library to implement a backend fo
- Text
- Anti-aliasing
- Font properties:
- Family resolving (like `'Arial', monospace`)
- Family resolving (like `'Timer New Roman', monospace`)
- Style: *normal*, *italic* and *oblique*
- Weight
- Stretch
......
### Elements
- Filter based
- `feConvolveMatrix`
- `feDiffuseLighting`
- `feDisplacementMap`
- `feMorphology`
- `feSpecularLighting`
- `feDistantLight`
- `fePointLight`
- `feSpotLight`
- Font based
- `font`
- `glyph`
- `missing-glyph`
- `hkern`
- `vkern`
- `font-face`
- `font-face-src`
- `font-face-uri`
- `font-face-format`
- `font-face-name`
- `marker`
- `color-profile`
### Attributes
- [enable-background](https://www.w3.org/TR/SVG11/filters.html#EnableBackgroundProperty) (deprecated)
- [in](https://www.w3.org/TR/SVG11/filters.html#FilterPrimitiveInAttribute)
with `BackgroundImage`, `BackgroundAlpha`, `FillPaint`, `StrokePaint`
- `image-rendering`
- `shape-rendering`
- `text-rendering`
- clip (deprecated)
- color-interpolation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use std::cmp;
use std::rc::Rc;
// external
use cairo::{
self,
MatrixTrait,
PatternTrait,
};
use rgb::FromSlice;
use usvg;
use usvg::ColorInterpolation as ColorSpace;
// self
use super::prelude::*;
use backend_utils::filter::{
self,
Error,
Filter,
ImageExt,
};
type Image = filter::Image<cairo::ImageSurface>;
type FilterResult = filter::FilterResult<cairo::ImageSurface>;
pub fn apply(
filter: &usvg::Filter,
bbox: Rect,
ts: &usvg::Transform,
canvas: &mut cairo::ImageSurface,
) {
CairoFilter::apply(filter, bbox, ts, canvas);
}
impl ImageExt for cairo::ImageSurface {
fn width(&self) -> u32 {
self.get_width() as u32
}
fn height(&self) -> u32 {
self.get_height() as u32
}
fn clone_image(&self) -> Result<Self, Error> {
let new_image = create_image(self.width(), self.height())?;
let cr = cairo::Context::new(&new_image);
cr.set_source_surface(self.as_ref(), 0.0, 0.0);
cr.paint();
Ok(new_image)
}
fn clip_image(&mut self, region: ScreenRect) {
let cr = cairo::Context::new(self);
cr.set_source_rgba(0.0, 0.0, 0.0, 0.0);
cr.set_operator(cairo::Operator::Clear);
cr.rectangle(0.0, 0.0, self.width() as f64, region.y as f64);
cr.rectangle(0.0, 0.0, region.x as f64, self.height() as f64);
cr.rectangle(region.right() as f64, 0.0, self.width() as f64, self.height() as f64);
cr.rectangle(0.0, region.bottom() as f64, self.width() as f64, self.height() as f64);
cr.fill();
}
fn into_srgb(&mut self) {
let data = &mut self.get_data().unwrap();
from_premultiplied(data);
for p in data.as_bgra_mut() {
let linear_color: palette::LinSrgb = palette::LinSrgb::new(p.r, p.g, p.b).into_format();
let color = palette::Srgb::from_linear(linear_color).into_format();
p.r = color.red;
p.g = color.green;
p.b = color.blue;
}
into_premultiplied(data);
}
fn into_linear_rgb(&mut self) {
let data = &mut self.get_data().unwrap();
from_premultiplied(data);
for p in data.as_bgra_mut() {
let color = palette::Srgb::new(p.r, p.g, p.b)
.into_format::<f32>()
.into_linear()
.into_format();
p.r = color.red;
p.g = color.green;
p.b = color.blue;
}
into_premultiplied(data);
}
}
fn create_image(width: u32, height: u32) -> Result<cairo::ImageSurface, Error> {
cairo::ImageSurface::create(cairo::Format::ARgb32, width as i32, height as i32)
.map_err(|_| Error::AllocFailed)
}
fn copy_image(image: &cairo::ImageSurface, region: ScreenRect) -> Result<cairo::ImageSurface, Error> {
let x = cmp::max(0, region.x) as f64;
let y = cmp::max(0, region.y) as f64;
let new_image = create_image(region.width, region.height)?;
let cr = cairo::Context::new(&new_image);
cr.set_source_surface(&*image, -x, -y);
cr.paint();
Ok(new_image)
}
fn from_premultiplied(data: &mut [u8]) {
// https://www.cairographics.org/manual/cairo-Image-Surfaces.html#cairo-format-t
for p in data.as_bgra_mut() {
let a = p.a as f64 / 255.0;
p.b = (p.b as f64 / a + 0.5) as u8;
p.g = (p.g as f64 / a + 0.5) as u8;
p.r = (p.r as f64 / a + 0.5) as u8;
}
}
fn into_premultiplied(data: &mut [u8]) {
// https://www.cairographics.org/manual/cairo-Image-Surfaces.html#cairo-format-t
for p in data.as_bgra_mut() {
let a = p.a as f64 / 255.0;
p.b = (p.b as f64 * a + 0.5) as u8;
p.g = (p.g as f64 * a + 0.5) as u8;
p.r = (p.r as f64 * a + 0.5) as u8;
}
}
struct CairoFilter;
impl Filter<cairo::ImageSurface> for CairoFilter {
fn get_input(
input: &Option<usvg::FilterInput>,
region: ScreenRect,
results: &[FilterResult],
canvas: &cairo::ImageSurface,
) -> Result<Image, Error> {
match input {
Some(usvg::FilterInput::SourceGraphic) => {
let image = copy_image(canvas, region)?;
Ok(Image {
image: Rc::new(image),
region: ScreenRect::new(0, 0, region.width, region.height),
color_space: ColorSpace::SRGB,
})
}
Some(usvg::FilterInput::SourceAlpha) => {
let mut image = copy_image(canvas, region)?;
// Set RGB to black. Keep alpha as is.
for p in image.get_data().unwrap().chunks_mut(4) {
p[0] = 0;
p[1] = 0;
p[2] = 0;
}
Ok(Image {
image: Rc::new(image),
region: ScreenRect::new(0, 0, region.width, region.height),
color_space: ColorSpace::SRGB,
})
}
Some(usvg::FilterInput::Reference(ref name)) => {
if let Some(ref v) = results.iter().rev().find(|v| v.name == *name) {
Ok(v.image.clone())
} else {
warn!("Unknown filter primitive reference '{}'.", name);
Self::get_input(&Some(usvg::FilterInput::SourceGraphic), region, results, canvas)
}
}
Some(input) => {
warn!("Filter input '{}' is not supported.", input.to_string());
Self::get_input(&Some(usvg::FilterInput::SourceGraphic), region, results, canvas)
}
None => {
if let Some(ref v) = results.last() {
Ok(v.image.clone())
} else {
Self::get_input(&Some(usvg::FilterInput::SourceGraphic), region, results, canvas)
}
}
}
}
fn apply_blur(
fe: &usvg::FeGaussianBlur,
units: usvg::Units,
cs: ColorSpace,
bbox: Rect,
ts: &usvg::Transform,
input: Image,
) -> Result<Image, Error> {
let (std_dx, std_dy) = try_opt!(Self::resolve_std_dev(fe, units, bbox, ts), Ok(input));
let input = input.into_color_space(cs)?;
let mut buffer = input.take()?;
let (w, h) = (buffer.width(), buffer.height());
{
let data = &mut buffer.get_data().unwrap(); // TODO: remove unwrap()
from_premultiplied(data);
filter::blur::apply(data, w, h, std_dx, std_dy, 4);
into_premultiplied(data);
}
Ok(Image::from_image(buffer, cs))
}
fn apply_offset(
filter: &usvg::Filter,
fe: &usvg::FeOffset,
bbox: Rect,
ts: &usvg::Transform,
input: Image,
) -> Result<Image, Error> {
let (sx, sy) = ts.get_scale();
let (dx, dy) = if filter.primitive_units == usvg::Units::ObjectBoundingBox {
(fe.dx * sx * bbox.width, fe.dy * sy * bbox.height)
} else {
(fe.dx * sx, fe.dy * sy)
};
if dx.is_fuzzy_zero() && dy.is_fuzzy_zero() {
return Ok(input);
}
// TODO: do not use an additional buffer
let mut buffer = create_image(input.width(), input.height())?;
let cr = cairo::Context::new(&mut buffer);
cr.set_source_surface(input.as_ref(), dx, dy);
cr.paint();
Ok(Image::from_image(buffer, input.color_space))
}
fn apply_blend(
fe: &usvg::FeBlend,
cs: ColorSpace,
region: ScreenRect,
input1: Image,
input2: Image,
) -> Result<Image, Error> {
let input1 = input1.into_color_space(cs)?;
let input2 = input2.into_color_space(cs)?;
let mut buffer = create_image(region.width, region.height)?;
let cr = cairo::Context::new(&mut buffer);
cr.set_source_surface(input2.as_ref(), 0.0, 0.0);
cr.paint();
let operator = match fe.mode {
usvg::FeBlendMode::Normal => cairo::Operator::Over,
usvg::FeBlendMode::Multiply => cairo::Operator::Multiply,
usvg::FeBlendMode::Screen => cairo::Operator::Screen,
usvg::FeBlendMode::Darken => cairo::Operator::Darken,
usvg::FeBlendMode::Lighten => cairo::Operator::Lighten,
};
cr.set_operator(operator);
cr.set_source_surface(input1.as_ref(), 0.0, 0.0);
cr.paint();
Ok(Image::from_image(buffer, cs))
}
fn apply_composite(
fe: &usvg::FeComposite,
cs: ColorSpace,
region: ScreenRect,
input1: Image,
input2: Image,
) -> Result<Image, Error> {
let input1 = input1.into_color_space(cs)?;
let input2 = input2.into_color_space(cs)?;
let mut buffer = create_image(region.width, region.height)?;
if fe.operator == Operator::Arithmetic {
warn!("feComposite with 'arithmetic' operator is not supported.");
return Ok(Image::from_image(buffer, cs));
};
let cr = cairo::Context::new(&mut buffer);
cr.set_source_surface(input2.as_ref(), 0.0, 0.0);
cr.paint();
use usvg::FeCompositeOperator as Operator;
let operator = match fe.operator {
Operator::Over => cairo::Operator::Over,
Operator::In => cairo::Operator::In,
Operator::Out => cairo::Operator::Out,
Operator::Atop => cairo::Operator::Atop,
Operator::Xor => cairo::Operator::Xor,
Operator::Arithmetic => cairo::Operator::Over,
};
cr.set_operator(operator);
cr.set_source_surface(input1.as_ref(), 0.0, 0.0);
cr.paint();
Ok(Image::from_image(buffer, cs))
}
fn apply_merge(
fe: &usvg::FeMerge,
cs: ColorSpace,
region: ScreenRect,
results: &[FilterResult],
canvas: &cairo::ImageSurface,
) -> Result<Image, Error> {
let mut buffer = create_image(region.width, region.height)?;
let cr = cairo::Context::new(&mut buffer);
for input in &fe.inputs {
let input = Self::get_input(input, region, &results, canvas)?;
let input = input.into_color_space(cs)?;
cr.set_source_surface(input.as_ref(), 0.0, 0.0);
cr.paint();
}
Ok(Image::from_image(buffer, cs))
}
fn apply_flood(
fe: &usvg::FeFlood,
region: ScreenRect,
) -> Result<Image, Error> {
let buffer = create_image(region.width, region.height)?;
let cr = cairo::Context::new(&buffer);
cr.set_source_color(fe.color, fe.opacity);
cr.paint();
Ok(Image::from_image(buffer, ColorSpace::SRGB))
}
fn apply_tile(
input: Image,
region: ScreenRect,
) -> Result<Image, Error> {
let buffer = create_image(region.width, region.height)?;
let mut subregion = input.region;
subregion.x -= region.x;
subregion.y -= region.y;
let tile = copy_image(&input.image, subregion)?;
let brush_ts = usvg::Transform::new_translate(subregion.x as f64, subregion.y as f64);
let patt = cairo::SurfacePattern::create(&tile);
patt.set_extend(cairo::Extend::Repeat);
patt.set_filter(cairo::Filter::Best);
let cr = cairo::Context::new(&buffer);
let mut m: cairo::Matrix = brush_ts.to_native();
m.invert();
patt.set_matrix(m);
cr.set_source(&cairo::Pattern::SurfacePattern(patt));
cr.rectangle(0.0, 0.0, region.width as f64, region.height as f64);
cr.fill();
Ok(Image::from_image(buffer, ColorSpace::SRGB))
}
fn apply_to_canvas(
input: Image,
region: ScreenRect,
canvas: &mut cairo::ImageSurface,
) -> Result<(), Error> {
let input = input.into_color_space(ColorSpace::SRGB)?;
let cr = cairo::Context::new(canvas);
cr.set_operator(cairo::Operator::Clear);
cr.set_source_rgba(0.0, 0.0, 0.0, 0.0);
cr.paint();
cr.set_operator(cairo::Operator::Over);
cr.set_source_surface(input.as_ref(), region.x as f64, region.y as f64);
cr.paint();
Ok(())
}
}
......@@ -106,7 +106,7 @@ fn draw_raster(
surface_data[i + 0] = (((tb >> 8) + tb) >> 8) as u8;
surface_data[i + 1] = (((tg >> 8) + tg) >> 8) as u8;
surface_data[i + 2] = (((tr >> 8) + tr) >> 8) as u8;
surface_data[i + 3] = a as u8;
surface_data[i + 3] = a as u8; // TODO: is needed?
} else {
surface_data[i + 0] = img_pixels[idx + 2];
surface_data[i + 1] = img_pixels[idx + 1];
......
......@@ -40,6 +40,7 @@ macro_rules! try_create_surface {
mod clippath;
mod ext;
mod fill;
mod filter;
mod gradient;
mod image;
mod mask;
......@@ -294,7 +295,7 @@ fn render_group_impl(
cr: &cairo::Context,
) -> Option<Rect> {
let sub_surface = layers.get()?;
let sub_surface = sub_surface.borrow_mut();
let mut sub_surface = sub_surface.borrow_mut();
let sub_cr = cairo::Context::new(&*sub_surface);
sub_cr.set_matrix(cr.get_matrix());
......@@ -317,6 +318,15 @@ fn render_group_impl(
}
}
if let Some(ref id) = g.filter {
if let Some(filter_node) = node.tree().defs_by_id(id) {
if let usvg::NodeKind::Filter(ref filter) = *filter_node.borrow() {
let ts = usvg::Transform::from_native(&cr.get_matrix());
filter::apply(filter, bbox, &ts, &mut *sub_surface);
}
}
}
let curr_matrix = cr.get_matrix();
cr.set_matrix(cairo::Matrix::identity());
cr.set_source_surface(&*sub_surface, 0.0, 0.0);
......
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use std::cmp;
use std::rc::Rc;
// external
use qt;
use rgb::FromSlice;
use usvg;
use usvg::ColorInterpolation as ColorSpace;
// self
use super::prelude::*;
use backend_utils::filter::{
self,
Error,
Filter,
ImageExt,
};
type Image = filter::Image<qt::Image>;
type FilterResult = filter::FilterResult<qt::Image>;
pub fn apply(
filter: &usvg::Filter,
bbox: Rect,
ts: &usvg::Transform,
canvas: &mut qt::Image,
) {
QtFilter::apply(filter, bbox, ts, canvas);
}
impl ImageExt for qt::Image {
fn width(&self) -> u32 {
self.width()
}
fn height(&self) -> u32 {
self.height()
}
fn clone_image(&self) -> Result<Self, Error> {
self.try_clone().ok_or(Error::AllocFailed)
}
fn clip_image(&mut self, region: ScreenRect) {
let mut brush = qt::Brush::new();
brush.set_color(0, 0, 0, 0);
let mut p = qt::Painter::new(self);
p.set_composition_mode(qt::CompositionMode::Clear);
p.reset_pen();
p.set_brush(brush);
p.draw_rect(0.0, 0.0, self.width() as f64, region.y as f64);
p.draw_rect(0.0, 0.0, region.x as f64, self.height() as f64);
p.draw_rect(region.right() as f64, 0.0, self.width() as f64, self.height() as f64);
p.draw_rect(0.0, region.bottom() as f64, self.width() as f64, self.height() as f64);
}
fn into_srgb(&mut self) {
for p in self.data_mut().as_rgba_mut() {
let linear_color: palette::LinSrgb = palette::LinSrgb::new(p.r, p.g, p.b).into_format();
let color = palette::Srgb::from_linear(linear_color).into_format();
p.r = color.red;
p.g = color.green;
p.b = color.blue;
}
}
fn into_linear_rgb(&mut self) {
for p in self.data_mut().as_rgba_mut() {
let color = palette::Srgb::new(p.r, p.g, p.b)
.into_format::<f32>()
.into_linear()
.into_format();
p.r = color.red;
p.g = color.green;
p.b = color.blue;
}
}
}
fn create_image(width: u32, height: u32) -> Result<qt::Image, Error> {
let mut image = qt::Image::new_rgba(width, height).ok_or(Error::AllocFailed)?;
image.fill(0, 0, 0, 0);
Ok(image)
}
fn copy_image(image: &qt::Image, region: ScreenRect) -> Result<qt::Image, Error> {
let x = cmp::max(0, region.x) as u32;
let y = cmp::max(0, region.y) as u32;
image.copy(x, y, region.width, region.height).ok_or(Error::AllocFailed)
}
struct QtFilter;
impl Filter<qt::Image> for QtFilter {
fn get_input(
input: &Option<usvg::FilterInput>,
region: ScreenRect,
results: &[FilterResult],
canvas: &qt::Image,
) -> Result<Image, Error> {
match input {
Some(usvg::FilterInput::SourceGraphic) => {
let image = copy_image(canvas, region)?;
let image = image.to_rgba().ok_or(Error::AllocFailed)?; // TODO: optional
Ok(Image {
image: Rc::new(image),
region: ScreenRect::new(0, 0, region.width, region.height),
color_space: ColorSpace::SRGB,
})
}
Some(usvg::FilterInput::SourceAlpha) => {
let image = copy_image(canvas, region)?;
let mut image = image.to_rgba().ok_or(Error::AllocFailed)?; // TODO: optional
// Set RGB to black. Keep alpha as is.
for p in image.data_mut().chunks_mut(4) {
p[0] = 0;
p[1] = 0;
p[2] = 0;
}
Ok(Image {
image: Rc::new(image),
region: ScreenRect::new(0, 0, region.width, region.height),
color_space: ColorSpace::SRGB,
})
}
Some(usvg::FilterInput::Reference(ref name)) => {
if let Some(ref v) = results.iter().rev().find(|v| v.name == *name) {
Ok(v.image.clone())
} else {
warn!("Unknown filter primitive reference '{}'.", name);
Self::get_input(&Some(usvg::FilterInput::SourceGraphic), region, results, canvas)
}
}
Some(input) => {
warn!("Filter input '{}' is not supported.", input.to_string());
Self::get_input(&Some(usvg::FilterInput::SourceGraphic), region, results, canvas)
}
None => {
if let Some(ref v) = results.last() {
Ok(v.image.clone())
} else {
Self::get_input(&Some(usvg::FilterInput::SourceGraphic), region, results, canvas)
}
}
}
}
fn apply_blur(
fe: &usvg::FeGaussianBlur,
units: usvg::Units,
cs: ColorSpace,