Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 20 additions & 1 deletion include/skb_canvas.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ typedef struct skb_color_stop_t {
skb_color_t color;
} skb_color_stop_t;

/**
* Defines the supported blend modes when compositing layers
* Enum values match the CompositeMode values defined in
* https://learn.microsoft.com/en-us/typography/opentype/spec/colr#format-32-paintcomposite
*/
typedef enum {
SKB_BLEND_CLEAR = 0,
SKB_BLEND_SRC = 1,
SKB_BLEND_DEST = 2,
SKB_BLEND_SRC_OVER = 3,
SKB_BLEND_DEST_OVER = 4,
SKB_BLEND_SRC_IN = 5,
SKB_BLEND_DEST_IN = 6,
SKB_BLEND_SRC_OUT = 7,
SKB_BLEND_DEST_OUT = 8,
SKB_BLEND_SOFT_LIGHT = 20,
} skb_blend_mode_t;

/**
* Creates new canvas to draw to.
* The canvas is expected to be disposable. You create canvas, draw some shapes, and dispose it.
Expand Down Expand Up @@ -164,8 +182,9 @@ void skb_canvas_push_layer(skb_canvas_t* c);
/**
* Removes the image layer from the top of the layer stack and blends it over the previous image.
* @param c canvas to draw to
* @param mode the blend mode to use for compositing
*/
void skb_canvas_pop_layer(skb_canvas_t* c);
void skb_canvas_pop_layer(skb_canvas_t* c, skb_blend_mode_t mode);

/**
* Rasterizes the vector shape defined earlier into the top of the mask stack.
Expand Down
204 changes: 188 additions & 16 deletions src/skb_canvas.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

// The rasterizer is based on the old stb_truetype rasterizer.
#define SKB_SUBSAMPLES 5
Expand Down Expand Up @@ -618,27 +619,158 @@ void skb_canvas_push_layer(skb_canvas_t* c)
}
}

static void skb__blend_over(const skb_image_layer_t* src, skb_image_layer_t* dst, int32_t offset_x, int32_t offset_y, int32_t width, int32_t height)
#define DEFINE_BLEND_FUNC(blend_func_name, blend_row_func) \
static void blend_func_name(const skb_image_layer_t* src_img, skb_image_layer_t* dst_img, int32_t offset_x, int32_t offset_y, int32_t width, int32_t height) \
{ \
const int32_t src_stride = src_img->stride; \
const int32_t dst_stride = dst_img->stride; \
const skb_color_t* src = src_img->buffer + offset_x + (offset_y * src_stride); \
const skb_color_t* src_end = src + (height * src_stride); \
skb_color_t* dst = dst_img->buffer + offset_x + (offset_y * dst_stride); \
while(src < src_end) { \
blend_row_func(src, dst, width); \
src += src_stride; \
dst += dst_stride; \
} \
}

static inline void skb__blend_row_clear(const skb_color_t* src, skb_color_t* dst, int32_t width)
{
memset(dst, 0, width * sizeof(skb_color_t));
}
DEFINE_BLEND_FUNC(skb__blend_clear, skb__blend_row_clear)

static inline void skb__blend_row_src(const skb_color_t* src, skb_color_t* dst, int32_t width)
{
memcpy(dst, src, width * sizeof(skb_color_t));
}
DEFINE_BLEND_FUNC(skb__blend_src, skb__blend_row_src)

static inline void skb__blend_row_src_over(const skb_color_t* src, skb_color_t* dst, int32_t width)
{
const skb_color_t* row_end = src + width;
while(src < row_end) {
if (src->a == 255) {
*dst = *src;
} else if (src->a > 0) {
*dst = skb_color_blend(*dst, *src);
}
src++;
dst++;
}
}
DEFINE_BLEND_FUNC(skb__blend_src_over, skb__blend_row_src_over)

static inline void skb__blend_row_dest_over(const skb_color_t* src, skb_color_t* dst, int32_t width)
{
const skb_color_t* row_end = src + width;
while(src < row_end) {
if (dst->a == 0) {
*dst = *src;
} else if (dst->a < 255) {
*dst = skb_color_blend(*src, *dst);
}
src++;
dst++;
}
}
DEFINE_BLEND_FUNC(skb__blend_dest_over, skb__blend_row_dest_over)

static inline void skb__blend_row_src_in(const skb_color_t* src, skb_color_t* dst, int32_t width)
{
const skb_color_t* row_end = src + width;
while(src < row_end) {
*dst = skb_color_mul_alpha(*src, dst->a);
src++;
dst++;
}
}
DEFINE_BLEND_FUNC(skb__blend_src_in, skb__blend_row_src_in)

static inline void skb__blend_row_dest_in(const skb_color_t* src, skb_color_t* dst, int32_t width)
{
const skb_color_t* row_end = src + width;
while(src < row_end) {
*dst = skb_color_mul_alpha(*dst, src->a);
src++;
dst++;
}
}
DEFINE_BLEND_FUNC(skb__blend_dest_in, skb__blend_row_dest_in)

static inline void skb__blend_row_src_out(const skb_color_t* src, skb_color_t* dst, int32_t width)
{
const skb_color_t* row_end = src + width;
while(src < row_end) {
*dst = skb_color_mul_alpha(*src, 255 - dst->a);
src++;
dst++;
}
}
DEFINE_BLEND_FUNC(skb__blend_src_out, skb__blend_row_src_out)

static inline void skb__blend_row_dest_out(const skb_color_t* src, skb_color_t* dst, int32_t width)
{
const int32_t src_stride = src->stride;
const int32_t dst_stride = dst->stride;
const int32_t blend_w = width;
const skb_color_t* row_end = src + width;
while(src < row_end) {
*dst = skb_color_mul_alpha(*dst, 255 - src->a);
src++;
dst++;
}
}
DEFINE_BLEND_FUNC(skb__blend_dest_out, skb__blend_row_dest_out)

for (int32_t y = offset_y; y < (offset_y + height); y++) {
const skb_color_t* x_src = src->buffer + offset_x + (y * src_stride);
skb_color_t* x_dst = dst->buffer + offset_x + (y * dst_stride);
for (int32_t x = 0; x < blend_w; x++) {
if (x_src->a == 255)
*x_dst = *x_src;
else if (x_src->a > 0)
*x_dst = skb_color_blend(*x_dst, *x_src);
x_src++;
x_dst++;
static inline float skb__soft_light_single_channel(float s, float d) {
if (s <= 0.5f) {
return d - (1.0f - 2.0f * s) * d * (1.0f - d);
} else if (d <= 0.25f) {
return d + (2.0f * s - 1.0f) * d * ((16.0f * d - 12.0f) * d + 3.0f);
} else {
return d + (2.0f * s - 1.0f) * (sqrtf(d) - d);
}
}
static inline void skb__blend_row_soft_light(const skb_color_t* src, skb_color_t* dst, int32_t width)
{
const skb_color_t* row_end = src + width;
while(src < row_end) {
// TODO: optimize
if (src->a == 0) {
// Leave dst as-is
} else if (dst->a == 0) {
*dst = *src;
} else {
// Convert to float 0-1 and unpremultiply
float s_a = src->a / 255.0f;
float d_a = dst->a / 255.0f;
float s_r = (src->r / 255.0f) / s_a;
float s_g = (src->g / 255.0f) / s_a;
float s_b = (src->b / 255.0f) / s_a;
float d_r = (dst->r / 255.0f) / d_a;
float d_g = (dst->g / 255.0f) / d_a;
float d_b = (dst->b / 255.0f) / d_a;
// Soft light per channel
float sl_r = skb__soft_light_single_channel(s_r, d_r);
float sl_g = skb__soft_light_single_channel(s_g, d_g);
float sl_b = skb__soft_light_single_channel(s_b, d_b);
float sl_a = s_a + d_a - s_a * d_a;
// Composite with alpha
sl_r = (1 - d_a) * s_r * s_a + (1 - s_a) * d_r * d_a + s_a * d_a * sl_r;
sl_g = (1 - d_a) * s_g * s_a + (1 - s_a) * d_g * d_a + s_a * d_a * sl_g;
sl_b = (1 - d_a) * s_b * s_a + (1 - s_a) * d_b * d_a + s_a * d_a * sl_b;
// Re-premultiply
*dst = skb_rgba(
(uint8_t)(sl_r * 255.0f + 0.5f),
(uint8_t)(sl_g * 255.0f + 0.5f),
(uint8_t)(sl_b * 255.0f + 0.5f),
(uint8_t)(sl_a * 255.0f + 0.5f));
}
src++;
dst++;
}
}
DEFINE_BLEND_FUNC(skb__blend_soft_light, skb__blend_row_soft_light)

void skb_canvas_pop_layer(skb_canvas_t* c)
void skb_canvas_pop_layer(skb_canvas_t* c, skb_blend_mode_t mode)
{
if (c->layers_count <= 1) return; // First item is the final layer, do not remove.
c->layers_count--;
Expand All @@ -648,7 +780,47 @@ void skb_canvas_pop_layer(skb_canvas_t* c)
skb_image_layer_t* fg_layer = &c->layers[c->layers_count];
skb_mask_t* mask = &c->masks[c->masks_count-1];

skb__blend_over(fg_layer, bg_layer, mask->region.x, mask->region.y, mask->region.width, mask->region.height);
switch (mode) {
case SKB_BLEND_CLEAR:
skb__blend_clear(fg_layer, bg_layer, mask->region.x, mask->region.y, mask->region.width, mask->region.height);
break;
case SKB_BLEND_SRC:
skb__blend_src(fg_layer, bg_layer, mask->region.x, mask->region.y, mask->region.width, mask->region.height);
break;
case SKB_BLEND_DEST:
// Do nothing - leave dest as-is
break;
case SKB_BLEND_SRC_OVER:
skb__blend_src_over(fg_layer, bg_layer, mask->region.x, mask->region.y, mask->region.width, mask->region.height);
break;
case SKB_BLEND_DEST_OVER:
skb__blend_dest_over(fg_layer, bg_layer, mask->region.x, mask->region.y, mask->region.width, mask->region.height);
break;
case SKB_BLEND_SRC_IN:
skb__blend_src_in(fg_layer, bg_layer, mask->region.x, mask->region.y, mask->region.width, mask->region.height);
break;
case SKB_BLEND_DEST_IN:
skb__blend_dest_in(fg_layer, bg_layer, mask->region.x, mask->region.y, mask->region.width, mask->region.height);
break;
case SKB_BLEND_SRC_OUT:
skb__blend_src_out(fg_layer, bg_layer, mask->region.x, mask->region.y, mask->region.width, mask->region.height);
break;
case SKB_BLEND_DEST_OUT:
skb__blend_dest_out(fg_layer, bg_layer, mask->region.x, mask->region.y, mask->region.width, mask->region.height);
break;
case SKB_BLEND_SOFT_LIGHT:
skb__blend_soft_light(fg_layer, bg_layer, mask->region.x, mask->region.y, mask->region.width, mask->region.height);
break;
default:
if (mode < 0 || mode > 27) { // COLRv1: If an unrecognized value is encountered, COMPOSITE_CLEAR must be used.
skb_debug_log("Unrecognized value encountered for blend mode: %d; using CLEAR\n", mode);
skb__blend_clear(fg_layer, bg_layer, mask->region.x, mask->region.y, mask->region.width, mask->region.height);
} else {
skb_debug_log("Unsupported blend mode: %d; using SRC_OVER fallback\n", mode);
skb__blend_src_over(fg_layer, bg_layer, mask->region.x, mask->region.y, mask->region.width, mask->region.height);
}
break;
}

skb_canvas_pop_mask(c);
}
Expand Down
9 changes: 3 additions & 6 deletions src/skb_rasterizer.c
Original file line number Diff line number Diff line change
Expand Up @@ -761,11 +761,8 @@ static void skb__hb_pop_group (

skb_canvas_t* c = (skb_canvas_t*)paint_data;

if (mode != HB_PAINT_COMPOSITE_MODE_SRC_OVER) {
skb_debug_log("Unsupported blend mode: %d\n", mode);
}

skb_canvas_pop_layer(c);
// Note: Simple mode conversion possible because HB and SKB enum values both exactly match the COLRv1 spec.
skb_canvas_pop_layer(c, (skb_blend_mode_t)mode);
}

#define GRAST_MAX_COLOR_STOPS 64
Expand Down Expand Up @@ -1159,7 +1156,7 @@ static void skb__icon_draw_shape(skb_canvas_t* c, const skb_icon_t* icon, const
skb_canvas_fill_solid_color(c, color);
}

skb_canvas_pop_layer(c);
skb_canvas_pop_layer(c, SKB_BLEND_SRC_OVER);
}

for (int32_t i = 0; i < shape->children_count; i++)
Expand Down