Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ project adheres to [Semantic Versioning](http://semver.org/).
* Fix error message HTTP response status code in image src setter
* `roundRect()` shape incorrect when radii were large relative to rectangle size (#2400)
* Reject loadImage when src is null or invalid (#2304)
* Add support for percentage in rgb and rgba parsing. Fix bug with parsing for alpha and percentages.
* Fix compilation on GCC 15 by including <cstdint> (#2545)

3.2.0
Expand Down
65 changes: 42 additions & 23 deletions src/color.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
#include <limits>
#include <map>
#include <string>

// Compatibility with Visual Studio versions prior to VS2015
#if defined(_MSC_VER) && _MSC_VER < 1900
#define snprintf _snprintf
Expand Down Expand Up @@ -153,14 +152,45 @@ wrap_float(T value, T limit) {
// return (value % limit + limit) % limit;
// }

/*
* Parse and clip a percentage value. Returns a float in the range [0, 1].
*/


static bool
check_percentage(const char* str) {
while (*str && *str != '%' && *str != ',' && *str != ' ' && *str != '/') ++str;
return *str == '%';
}

static bool
parse_clipped_percentage(const char** pStr, float *pFraction) {
float percentage;
bool result = parse_css_number(pStr,&percentage);
const char*& str = *pStr;
if (result) {
if (*str == '%') {
++str;
*pFraction = clip(percentage, 0.0f, 100.0f) / 100.0f;
return result;
}
}
return false;
}

/*
* Parse color channel value
*/

static bool
parse_rgb_channel(const char** pStr, uint8_t *pChannel) {
float f_channel;
if (parse_css_number(pStr, &f_channel)) {
bool percentage = check_percentage(*pStr);
if(percentage && parse_clipped_percentage(pStr, &f_channel)) {
int channel = (int) ceil(255 * f_channel);
*pChannel = channel;
return true;
} else if (parse_css_number(pStr, &f_channel)) {
int channel = (int) ceil(f_channel);
*pChannel = clip(channel, 0, 255);
return true;
Expand All @@ -182,24 +212,6 @@ parse_degrees(const char** pStr, float *pDegrees) {
return false;
}

/*
* Parse and clip a percentage value. Returns a float in the range [0, 1].
*/

static bool
parse_clipped_percentage(const char** pStr, float *pFraction) {
float percentage;
bool result = parse_css_number(pStr,&percentage);
const char*& str = *pStr;
if (result) {
if (*str == '%') {
++str;
*pFraction = clip(percentage, 0.0f, 100.0f) / 100.0f;
return result;
}
}
return false;
}

/*
* Macros to help with parsing inside rgba_from_*_string
Expand Down Expand Up @@ -231,13 +243,15 @@ parse_clipped_percentage(const char** pStr, float *pFraction) {
#define ALPHA(NAME) \
if (*str >= '1' && *str <= '9') { \
NAME = 0; \
float n = .1f; \
while(*str >='0' && *str <= '9') { \
NAME += (*str - '0') * n; \
NAME = NAME * 10 + (*str - '0'); \
str++; \
} \
while(*str == ' ')str++; \
if(*str != '%') { \
if(*str == '%') { \
NAME *= 0.01f; \
++str; \
} else { \
NAME = 1; \
} \
} else { \
Expand All @@ -253,6 +267,11 @@ parse_clipped_percentage(const char** pStr, float *pFraction) {
NAME += (*str++ - '0') * n; \
n *= .1f; \
} \
while(*str == ' ')str++; \
if(*str == '%') { \
NAME *= 0.01f; \
++str; \
} \
} \
} \
do {} while (0) // require trailing semicolon
Expand Down
21 changes: 21 additions & 0 deletions test/canvas.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,27 @@ describe('Canvas', function () {

ctx.fillStyle = 'rgb( 255, 300.09, 90, 40%)'
assert.equal('rgba(255, 255, 90, 0.40)', ctx.fillStyle)

ctx.fillStyle = 'rgb( 255, 100%, 50%, 60%)'
assert.equal('rgba(255, 255, 128, 0.60)', ctx.fillStyle)

ctx.fillStyle = 'rgb( 255, 20%, 90, 70%)'
assert.equal('rgba(255, 51, 90, 0.70)', ctx.fillStyle)

ctx.fillStyle = 'rgb( 100%, 300%, 90, 50%)'
assert.equal('rgba(255, 255, 90, 0.50)', ctx.fillStyle)

ctx.fillStyle = 'rgb( 100%, 300.09, 90%, 0)'
assert.equal('rgba(255, 255, 230, 0.00)', ctx.fillStyle)

ctx.fillStyle = 'rgb( 100%, 99%, 80% 50%)'
assert.equal('rgba(255, 253, 204, 0.50)', ctx.fillStyle)

ctx.fillStyle = 'rgb( 100%, 99.99%, 40% / 50%)'
Copy link

@yisibl yisibl Dec 25, 2025

Choose a reason for hiding this comment

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

To my knowledge, this syntax is not supported in CSS or browsers and requires wrapping with calc().

Or use spaces to separate them:

rgb( 100% 99.99% 40% / 50% )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To my understanding, the slash here is to separate the rgb from alpha. According to CSS specifications the commas are legacy format, but there is no mention of it being unsupported or deprecated.

https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/rgb

Copy link

Choose a reason for hiding this comment

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

Try~
image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I checked the MDN for color syntax, and yeah this makes sense. You cannot mix commas and / in the color property, however this was already the case in node-canvas, I just added a test for this.

@zbjornson @chearon is this something that should be fixed by adding formal syntax validation to rgb (and eventually other type) parsing?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be better not to, but it's probably not a big deal to parse the commas with the slash, since users will almost for sure be using one of the two syntaxes. Good to point it out though.

assert.equal('rgba(255, 255, 102, 0.50)', ctx.fillStyle)

ctx.fillStyle = 'rgb( 100%, 33.33%, 66.66%, 2%)'
assert.equal('rgba(255, 85, 170, 0.02)', ctx.fillStyle)
// hsl / hsla tests

ctx.fillStyle = 'hsl(0, 0%, 0%)'
Expand Down
Loading