Skip to content

Add support for rendering images #919

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ smallvec = "1.15.0"
static_assertions = "1.1.0"
thiserror = "2.0.12"
oxipng = { version = "9.1.4", default-features = false }
png = "0.17.16"

# The below crates are experimental!
vello_api = { path = "sparse_strips/vello_api", default-features = false }
Expand All @@ -119,7 +120,6 @@ image = { version = "0.25.6", default-features = false }
# Used for examples
clap = "4.5.35"
anyhow = "1.0.97"
png = "0.17.16"
pollster = "0.4.0"
web-time = "1.1.0"
wgpu-profiler = "0.21.0"
Expand Down
1 change: 1 addition & 0 deletions sparse_strips/vello_api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ publish = false

[dependencies]
peniko = { workspace = true }
png = { workspace = true }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need this? Could we move the responsibility of preparing the image data to the client? If it still provides value, perhaps we could consider making it an optional feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that loading png images is such a common operation, I think it would be good to keep this. But I will definitely make it an optional feature.


[features]
default = ["std"]
Expand Down
2 changes: 2 additions & 0 deletions sparse_strips/vello_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@

#![forbid(unsafe_code)]
#![no_std]
extern crate alloc;

pub use peniko;
pub use peniko::color;
pub use peniko::kurbo;
pub mod execute;
pub mod glyph;
pub mod paint;
pub mod pixmap;
27 changes: 26 additions & 1 deletion sparse_strips/vello_api/src/paint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

//! Types for paints.

use alloc::sync::Arc;
use crate::kurbo::Affine;
use peniko::color::{AlphaColor, PremulRgba8, Srgb};
use peniko::{ColorStops, GradientKind};
use peniko::{ColorStops, GradientKind, ImageQuality};
use crate::pixmap::Pixmap;

/// A paint that needs to be resolved via its index.
// In the future, we might add additional flags, that's why we have
Expand Down Expand Up @@ -82,13 +84,30 @@ impl Gradient {
}
}

/// An image.
#[derive(Debug, Clone)]
pub struct Image {
/// The underlying pixmap of the image.
pub pixmap: Arc<Pixmap>,
/// Extend mode in the horizontal direction.
pub x_extend: peniko::Extend,
/// Extend mode in the vertical direction.
pub y_extend: peniko::Extend,
/// Hint for desired rendering quality.
pub quality: ImageQuality,
/// A transform to apply to the image.
pub transform: Affine,
}

/// A kind of paint that can be used for filling and stroking shapes.
#[derive(Debug, Clone)]
pub enum PaintType {
/// A solid color.
Solid(AlphaColor<Srgb>),
/// A gradient.
Gradient(Gradient),
/// An image.
Image(Image),
}

impl From<AlphaColor<Srgb>> for PaintType {
Expand All @@ -102,3 +121,9 @@ impl From<Gradient> for PaintType {
Self::Gradient(value)
}
}

impl From<Image> for PaintType {
fn from(value: Image) -> Self {
Self::Image(value)
}
}
125 changes: 125 additions & 0 deletions sparse_strips/vello_api/src/pixmap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! A simple pixmap type.

use alloc::vec;
use alloc::vec::Vec;

/// A pixmap backed by u8.
#[derive(Debug, Clone)]
pub struct Pixmap {
/// Width of the pixmap in pixels.
pub width: u16,
/// Height of the pixmap in pixels.
pub height: u16,
/// Buffer of the pixmap in RGBA format.
pub buf: Vec<u8>,
}

impl Pixmap {
/// Create a new pixmap with the given width and height in pixels.
pub fn new(width: u16, height: u16) -> Self {
let buf = vec![0; width as usize * height as usize * 4];
Self { width, height, buf }
}

/// Return the width of the pixmap.
pub fn width(&self) -> u16 {
self.width
}

/// Return the height of the pixmap.
pub fn height(&self) -> u16 {
self.height
}

/// Apply an alpha value to the whole pixmap.
pub fn multiply_alpha(&mut self, alpha: u8) {
for comp in self.data_mut() {
*comp = ((alpha as u16 * *comp as u16) / 255) as u8;
}
}

/// Create a pixmap from a PNG file.
pub fn from_png(data: &[u8]) -> Result<Self, png::DecodingError> {
let mut decoder = png::Decoder::new(data);
decoder.set_transformations(png::Transformations::ALPHA);

let mut reader = decoder.read_info()?;
let mut img_data = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut img_data)?;

let data = match info.color_type {
// We set a transformation to always convert to alpha.
png::ColorType::Rgb => unreachable!(),
png::ColorType::Grayscale => unreachable!(),
// I believe the above transformation also expands indexed images.
png::ColorType::Indexed => unreachable!(),
png::ColorType::Rgba => img_data,
png::ColorType::GrayscaleAlpha => {
let mut rgba_data = Vec::with_capacity(img_data.len() * 2);
for slice in img_data.chunks(2) {
let gray = slice[0];
let alpha = slice[1];
rgba_data.push(gray);
rgba_data.push(gray);
rgba_data.push(gray);
rgba_data.push(alpha);
}

rgba_data
}
};

let premultiplied = data.chunks_exact(4).flat_map(|d| {
let alpha = d[3] as u16;
let premultiply = |e: u8| {
((e as u16 * alpha) / 255) as u8
};

if alpha == 0 {
[0, 0, 0, 0]
} else {
[premultiply(d[0]), premultiply(d[1]), premultiply(d[2]), d[3]]
}
}).collect::<Vec<_>>();

Ok(Self {
width: info.width as u16,
height: info.height as u16,
buf: premultiplied,
})
}

/// Returns a reference to the underlying data as premultiplied RGBA8.
pub fn data(&self) -> &[u8] {
&self.buf
}

/// Returns a mutable reference to the underlying data as premultiplied RGBA8.
pub fn data_mut(&mut self) -> &mut [u8] {
&mut self.buf
}

/// Sample a pixel from the pixmap.
#[inline(always)]
pub fn sample(&self, x: u16, y: u16) -> &[u8] {
let idx = 4 * (self.width as usize * y as usize + x as usize);
&self.buf[idx..][..4]
}

/// Convert from premultiplied to separate alpha.
///
/// Not fast, but useful for saving to PNG etc.
pub fn unpremultiply(&mut self) {
for rgba in self.buf.chunks_exact_mut(4) {
let alpha = 255.0 / rgba[3] as f32;
if alpha != 0.0 {
rgba[0] = (rgba[0] as f32 * alpha).round().min(255.0) as u8;
rgba[1] = (rgba[1] as f32 * alpha).round().min(255.0) as u8;
rgba[2] = (rgba[2] as f32 * alpha).round().min(255.0) as u8;
}
}
}
}
87 changes: 71 additions & 16 deletions sparse_strips/vello_common/src/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
use crate::color::Srgb;
use crate::color::palette::css::BLACK;
use crate::kurbo::{Affine, Point, Vec2};
use crate::peniko::{ColorStop, Extend, GradientKind};
use crate::peniko::{ColorStop, Extend, GradientKind, ImageQuality};
use smallvec::SmallVec;
use std::borrow::Cow;
use std::f32::consts::PI;
use std::iter;
use vello_api::paint::{Gradient, IndexedPaint, Paint};
use std::sync::Arc;
use vello_api::paint::{Gradient, Image, IndexedPaint, Paint};
use crate::encode::private::Sealed;
use crate::pixmap::Pixmap;

const DEGENERATE_THRESHOLD: f32 = 1.0e-6;
const NUDGE_VAL: f32 = 1.0e-7;
Expand Down Expand Up @@ -209,20 +212,7 @@ impl EncodeExt for Gradient {
// the above approach of incrementally updating the position, we need to calculate
// how the x/y unit vectors are affected by the transform, and then use this as the
// step delta for a step in the x/y direction.
let (x_advance, y_advance) = {
let scale_skew_transform = {
let c = transform.as_coeffs();
Affine::new([c[0], c[1], c[2], c[3], 0.0, 0.0])
};

let x_advance = scale_skew_transform * Point::new(1.0, 0.0);
let y_advance = scale_skew_transform * Point::new(0.0, 1.0);

(
Vec2::new(x_advance.x, x_advance.y),
Vec2::new(y_advance.x, y_advance.y),
)
};
let (x_advance, y_advance) = x_y_advances(&transform);

let encoded = EncodedGradient {
kind,
Expand Down Expand Up @@ -425,11 +415,57 @@ fn encode_stops(stops: &[ColorStop], start: f32, end: f32, pad: bool) -> Vec<Gra
}
}

fn x_y_advances(transform: &Affine) -> (Vec2, Vec2) {
let scale_skew_transform = {
let c = transform.as_coeffs();
Affine::new([c[0], c[1], c[2], c[3], 0.0, 0.0])
};

let x_advance = scale_skew_transform * Point::new(1.0, 0.0);
let y_advance = scale_skew_transform * Point::new(0.0, 1.0);

(
Vec2::new(x_advance.x, x_advance.y),
Vec2::new(y_advance.x, y_advance.y),
)
}

impl Sealed for Image {}

impl EncodeExt for Image {
fn encode_into(&self, paints: &mut Vec<EncodedPaint>) -> Paint {
let idx = paints.len();

let transform = self.transform.inverse();
// TODO: This is somewhat expensive for large images, maybe it's not worth optimizing
// non-opaque images in the first place..
let has_opacities = self.pixmap.data().chunks(4).any(|c| c[3] != 255);

let (x_advance, y_advance) = x_y_advances(&transform);

let encoded = EncodedImage {
pixmap: self.pixmap.clone(),
extends: (self.x_extend, self.y_extend),
quality: self.quality,
has_opacities,
transform,
x_advance,
y_advance,
};

paints.push(EncodedPaint::Image(encoded));

Paint::Indexed(IndexedPaint::new(idx))
}
}

/// An encoded paint.
#[derive(Debug)]
pub enum EncodedPaint {
/// An encoded gradient.
Gradient(EncodedGradient),
/// An encoded image.
Image(EncodedImage),
}

impl From<EncodedGradient> for EncodedPaint {
Expand All @@ -438,6 +474,25 @@ impl From<EncodedGradient> for EncodedPaint {
}
}

/// An encoded image.
#[derive(Debug)]
pub struct EncodedImage {
/// The underlying pixmap of the image.
pub pixmap: Arc<Pixmap>,
/// The extends in the horizontal and vertical direction.
pub extends: (Extend, Extend),
/// The rendering quality of the image.
pub quality: ImageQuality,
/// Whether the image has opacities.
pub has_opacities: bool,
/// A transform to apply to the image.
pub transform: Affine,
/// The advance in image coordinates for one step in the x direction.
pub x_advance: Vec2,
/// The advance in image coordinates for one step in the y direction.
pub y_advance: Vec2,
}

/// Computed properties of a linear gradient.
#[derive(Debug)]
pub struct LinearKind {
Expand Down
1 change: 0 additions & 1 deletion sparse_strips/vello_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ pub mod encode;
pub mod flatten;
pub mod glyph;
pub mod pico_svg;
pub mod pixmap;
pub mod strip;
pub mod tile;

Expand Down
42 changes: 0 additions & 42 deletions sparse_strips/vello_common/src/pixmap.rs

This file was deleted.

Loading
Loading