Skip to content
Draft
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Add support for CSS gradients as background images

## PlutoBook 0.19.0 (2026-08-04)

- Fix several security vulnerabilities
Expand Down
13 changes: 12 additions & 1 deletion FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- [Fonts](#fonts)
- [Color](#color)
- [Backgrounds and Borders](#backgrounds-and-borders)
- [Gradients](#gradients)
- [Outlines](#outlines)
- [Box Model](#box-model)
- [Box Sizing](#box-sizing)
Expand Down Expand Up @@ -36,10 +37,20 @@ PlutoBook also provides partial support for [CSS Color Module Level 4](https://w

## Backgrounds and Borders

PlutoBook implements the [CSS Backgrounds and Borders Module Level 3](https://www.w3.org/TR/css-backgrounds-3), providing control over the decoration of the border area and the background of the content, padding, and border areas. It supports properties like `background-color` for solid fills and `background-image` for adding images, along with `background-repeat`, `background-attachment`, `background-position`, `background-clip`, `background-origin`, and `background-size` for controlling placement and scaling. The `background` shorthand is fully supported, but multiple background layers are not yet supported.
PlutoBook implements the [CSS Backgrounds and Borders Module Level 3](https://www.w3.org/TR/css-backgrounds-3), providing control over the decoration of the border area and the background of the content, padding, and border areas. It supports properties like `background-color` for solid fills and `background-image` for adding images and [gradients](#gradients), along with `background-repeat`, `background-attachment`, `background-position`, `background-clip`, `background-origin`, and `background-size` for controlling placement and scaling. The `background` shorthand is fully supported, but multiple background layers are not yet supported.

For borders, PlutoBook handles `border-color`, `border-style`, and `border-width`, which can be combined using the `border` shorthand. Side-specific border properties such as `border-top`, `border-right`, `border-bottom`, and `border-left` are also supported for precise control of each edge. The shape of corners can be adjusted using `border-radius`, allowing smooth rounding for any or all corners.

## Gradients

PlutoBook supports CSS gradients as defined in the [CSS Images Module Level 3](https://www.w3.org/TR/css-images-3), which can be used anywhere an image is expected, such as `background-image` and `list-style-image`. All six gradient functions are available: `linear-gradient()`, `radial-gradient()`, `conic-gradient()`, and their `repeating-` counterparts.

A linear gradient may be given an explicit `<angle>` or a `to <side-or-corner>` keyword such as `to top right`. A radial gradient accepts an ending shape (`circle` or `ellipse`), a size given either as one of the `closest-side`, `closest-corner`, `farthest-side`, and `farthest-corner` keywords or as explicit radii, and a center position through `at <position>`. A conic gradient accepts a starting angle through `from <angle>` and a center position through `at <position>`.

Color stops support implicit positions, which spread evenly between their positioned neighbours, the two-position syntax (`red 20% 40%`), and transition hints (`red, 20%, blue`). Since a gradient is resolved against the style of the element using it, `currentColor` and font relative lengths such as `em` inside a gradient behave as expected. Gradients have no intrinsic dimensions, so they take the size of the area they are painted into and respond to `background-size`, `background-position`, and `background-repeat`.

Gradients are emitted as PDF shadings rather than as bitmaps, so they stay resolution independent. PDF has no conic shading, so a conic gradient is approximated with a mesh of patches; the alternative of sampling the sweep into a bitmap can be selected at runtime with `plutobook::setConicGradientRendering()`, or `plutobook_set_conic_gradient_rendering()` from the C API.

## Outlines

PlutoBook supports outlines as defined in the [CSS Basic User Interface Module Level 3](https://www.w3.org/TR/css-ui-3). This includes `outline-color`, `outline-style`, `outline-width`, and `outline-offset`, allowing authors to draw a line around elements without affecting their box dimensions or layout flow. You can specify an outline’s color, style, and width using `outline-color`, `outline-style`, and `outline-width`, or conveniently set them all at once with the `outline` shorthand. The distance between the outline and the element’s border can be adjusted with `outline-offset`. Just like borders, outlines respect `border-radius`, so corners can be rounded consistently. Unlike borders, outlines do not take up space or influence the element’s size, making them useful for adding emphasis or separation without changing layout.
Expand Down
29 changes: 29 additions & 0 deletions include/plutobook.h
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,35 @@ PLUTOBOOK_API void plutobook_set_http_max_redirects(int amount);
*/
PLUTOBOOK_API void plutobook_set_http_timeout(int timeout);

/**
* @brief Defines how a CSS conic gradient is rendered.
*
* Neither cairo nor PDF has a conic shading, so the sweep has to be
* approximated one way or another.
*/
typedef enum _plutobook_conic_gradient_rendering {
PLUTOBOOK_CONIC_GRADIENT_RENDERING_MESH,
PLUTOBOOK_CONIC_GRADIENT_RENDERING_RASTER
} plutobook_conic_gradient_rendering_t;

/**
* @brief Selects the method used to render conic gradients.
*
* If not set, conic gradients are rendered with
* PLUTOBOOK_CONIC_GRADIENT_RENDERING_MESH, which keeps them resolution
* independent in the PDF output. PLUTOBOOK_CONIC_GRADIENT_RENDERING_RASTER
* trades that for an exact sweep, at the cost of an embedded bitmap.
*
* @param rendering The rendering method to use.
*/
PLUTOBOOK_API void plutobook_set_conic_gradient_rendering(plutobook_conic_gradient_rendering_t rendering);

/**
* @brief Returns the method used to render conic gradients.
* @return The rendering method currently in use.
*/
PLUTOBOOK_API plutobook_conic_gradient_rendering_t plutobook_get_conic_gradient_rendering(void);

/**
* @brief Defines the different media types used for CSS @media queries.
*/
Expand Down
29 changes: 29 additions & 0 deletions include/plutobook.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,35 @@ class PLUTOBOOK_API DefaultResourceFetcher final : public ResourceFetcher {
*/
DefaultResourceFetcher* defaultResourceFetcher();

/**
* @brief Defines how a CSS conic gradient is rendered.
*
* Neither cairo nor PDF has a conic shading, so the sweep has to be
* approximated one way or another.
*/
enum class ConicGradientRendering {
Mesh, ///< Approximate the sweep with a mesh of patches, which stays vectorial.
Raster ///< Sample the sweep into a bitmap.
};

/**
* @brief Selects the method used to render conic gradients.
*
* If not set, conic gradients are rendered with ConicGradientRendering::Mesh,
* which keeps them resolution independent in the PDF output.
* ConicGradientRendering::Raster trades that for an exact sweep, at the cost of
* an embedded bitmap.
*
* @param rendering The rendering method to use.
*/
PLUTOBOOK_API void setConicGradientRendering(ConicGradientRendering rendering);

/**
* @brief Returns the method used to render conic gradients.
* @return The rendering method currently in use.
*/
PLUTOBOOK_API ConicGradientRendering conicGradientRendering();

/**
* @brief The OutputStream is an abstract base class for writing data to an output stream.
*/
Expand Down
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ plutobook_sources = [
'source/layout/textbox.cpp',

'source/resource/fontresource.cpp',
'source/resource/gradientimage.cpp',
'source/resource/imageresource.cpp',
'source/resource/resource.cpp',
'source/resource/textresource.cpp',
Expand Down
229 changes: 229 additions & 0 deletions source/cssparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1918,10 +1918,239 @@ RefPtr<CSSValue> CSSParser::consumeUrlOrNone(CSSTokenStream& input)
return consumeUrl(input);
}

bool CSSParser::consumeLinearGradientPrelude(CSSTokenStream& input, RefPtr<CSSValue>& angle, RefPtr<CSSValue>& direction)
{
if((angle = consumeAngle(input)))
return true;
if(!consumeIdentIncludingWhitespace(input, "to"))
return true;
static constexpr CSSIdentValueEntry horizontalTable[] = {
{"left", CSSValueID::Left},
{"right", CSSValueID::Right}
};

static constexpr CSSIdentValueEntry verticalTable[] = {
{"top", CSSValueID::Top},
{"bottom", CSSValueID::Bottom}
};

RefPtr<CSSValue> horizontal;
RefPtr<CSSValue> vertical;
do {
if(horizontal == nullptr && (horizontal = consumeIdent(input, horizontalTable)))
continue;
if(vertical == nullptr && (vertical = consumeIdent(input, verticalTable)))
continue;
break;
} while(true);

if(horizontal == nullptr && vertical == nullptr)
return false;
if(horizontal == nullptr)
horizontal = CSSIdentValue::create(CSSValueID::Center);
if(vertical == nullptr)
vertical = CSSIdentValue::create(CSSValueID::Center);
direction = CSSPairValue::create(m_heap, std::move(horizontal), std::move(vertical));
return true;
}

bool CSSParser::consumeRadialGradientPrelude(CSSTokenStream& input, RefPtr<CSSValue>& shape, RefPtr<CSSValue>& size, RefPtr<CSSValue>& position)
{
static constexpr CSSIdentValueEntry shapeTable[] = {
{"circle", CSSValueID::Circle},
{"ellipse", CSSValueID::Ellipse}
};

static constexpr CSSIdentValueEntry sizeTable[] = {
{"closest-side", CSSValueID::ClosestSide},
{"closest-corner", CSSValueID::ClosestCorner},
{"farthest-side", CSSValueID::FarthestSide},
{"farthest-corner", CSSValueID::FarthestCorner}
};

do {
if(shape == nullptr && (shape = consumeIdent(input, shapeTable)))
continue;
if(size == nullptr) {
if((size = consumeIdent(input, sizeTable)))
continue;
if(auto first = consumeLengthOrPercent(input, false, false)) {
if(auto second = consumeLengthOrPercent(input, false, false)) {
size = CSSPairValue::create(m_heap, std::move(first), std::move(second));
} else {
size = std::move(first);
}

continue;
}
}

break;
} while(true);

if(size && !is<CSSIdentValue>(*size)) {
auto ellipse = is<CSSPairValue>(*size);
if(shape == nullptr) {
shape = CSSIdentValue::create(ellipse ? CSSValueID::Ellipse : CSSValueID::Circle);
} else if(ellipse != (shape->id() == CSSValueID::Ellipse)) {
return false;
}

// A circle radius must be a length, percentages resolve against no single axis.
if(!ellipse && is<CSSPercentValue>(*size)) {
return false;
}
}

if(consumeIdentIncludingWhitespace(input, "at")) {
position = consumePositionCoordinate(input);
if(position == nullptr) {
return false;
}
}

return true;
}

bool CSSParser::consumeConicGradientPrelude(CSSTokenStream& input, RefPtr<CSSValue>& angle, RefPtr<CSSValue>& position)
{
if(consumeIdentIncludingWhitespace(input, "from")) {
angle = consumeAngle(input);
if(angle == nullptr) {
return false;
}
}

if(consumeIdentIncludingWhitespace(input, "at")) {
position = consumePositionCoordinate(input);
if(position == nullptr) {
return false;
}
}

return true;
}

RefPtr<CSSValue> CSSParser::consumeGradientStopPosition(CSSTokenStream& input, CSSGradientType gradientType)
{
if(gradientType == CSSGradientType::Conic) {
if(auto value = consumeAngle(input))
return value;
// A unitless zero is accepted wherever an angle is expected.
if(input->type() == CSSToken::Type::Number && input->number() == 0.f) {
input.consumeIncludingWhitespace();
return CSSAngleValue::create(m_heap, 0.f, CSSAngleValue::Units::Degrees);
}

return consumePercent(input, true);
}

return consumeLengthOrPercent(input, true, false);
}

bool CSSParser::consumeGradientStops(CSSTokenStream& input, CSSGradientType gradientType, CSSGradientStopList& stops)
{
do {
auto color = consumeColor(input);
if(color == nullptr) {
// A color hint must sit between two color stops.
if(stops.empty())
return false;
auto hint = consumeGradientStopPosition(input, gradientType);
if(hint == nullptr)
return false;
stops.emplace_back(nullptr, std::move(hint));
if(!input.consumeCommaIncludingWhitespace())
return false;
color = consumeColor(input);
if(color == nullptr) {
return false;
}
}

if(auto first = consumeGradientStopPosition(input, gradientType)) {
auto second = consumeGradientStopPosition(input, gradientType);
stops.emplace_back(color, std::move(first));
if(second) {
stops.emplace_back(std::move(color), std::move(second));
}
} else {
stops.emplace_back(std::move(color), nullptr);
}
} while(input.consumeCommaIncludingWhitespace());

// A lone color stop is allowed and paints a solid color.
return true;
}

RefPtr<CSSValue> CSSParser::consumeGradient(CSSTokenStream& input, CSSGradientType gradientType, bool repeating)
{
assert(input->type() == CSSToken::Type::Function);
CSSTokenStreamGuard guard(input);
auto block = input.consumeBlock();
block.consumeWhitespace();

RefPtr<CSSValue> angle;
RefPtr<CSSValue> direction;
RefPtr<CSSValue> shape;
RefPtr<CSSValue> size;
RefPtr<CSSValue> position;
switch(gradientType) {
case CSSGradientType::Linear:
if(!consumeLinearGradientPrelude(block, angle, direction))
return nullptr;
break;
case CSSGradientType::Radial:
if(!consumeRadialGradientPrelude(block, shape, size, position))
return nullptr;
break;
case CSSGradientType::Conic:
if(!consumeConicGradientPrelude(block, angle, position))
return nullptr;
break;
}

if(angle || direction || shape || size || position) {
if(!block.consumeCommaIncludingWhitespace()) {
return nullptr;
}
}

CSSGradientStopList stops(m_heap);
if(!consumeGradientStops(block, gradientType, stops))
return nullptr;
if(!block.empty())
return nullptr;
input.consumeWhitespace();
guard.release();
return CSSGradientValue::create(m_heap, gradientType, repeating, std::move(angle), std::move(direction),
std::move(shape), std::move(size), std::move(position), std::move(stops));
}

RefPtr<CSSValue> CSSParser::consumeImage(CSSTokenStream& input)
{
if(auto token = consumeUrlToken(input))
return CSSImageValue::create(m_heap, m_context.completeUrl(token->data()));
if(input->type() == CSSToken::Type::Function) {
struct CSSGradientEntry {
CSSGradientType gradientType;
bool repeating;
};

static constexpr CSSIdentEntry<CSSGradientEntry> table[] = {
{"linear-gradient", {CSSGradientType::Linear, false}},
{"radial-gradient", {CSSGradientType::Radial, false}},
{"conic-gradient", {CSSGradientType::Conic, false}},
{"repeating-linear-gradient", {CSSGradientType::Linear, true}},
{"repeating-radial-gradient", {CSSGradientType::Radial, true}},
{"repeating-conic-gradient", {CSSGradientType::Conic, true}}
};

if(auto entry = matchIdent(table, input->data())) {
return consumeGradient(input, entry->gradientType, entry->repeating);
}
}

return nullptr;
}

Expand Down
7 changes: 7 additions & 0 deletions source/cssparser.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ class CSSParser {
RefPtr<CSSValue> consumeUrlOrNone(CSSTokenStream& input);
RefPtr<CSSValue> consumeImage(CSSTokenStream& input);
RefPtr<CSSValue> consumeImageOrNone(CSSTokenStream& input);
RefPtr<CSSValue> consumeGradient(CSSTokenStream& input, CSSGradientType gradientType, bool repeating);
RefPtr<CSSValue> consumeGradientStopPosition(CSSTokenStream& input, CSSGradientType gradientType);
RefPtr<CSSValue> consumeColor(CSSTokenStream& input);
RefPtr<CSSValue> consumeRgb(CSSTokenStream& input);
RefPtr<CSSValue> consumeHsl(CSSTokenStream& input);
Expand Down Expand Up @@ -163,6 +165,11 @@ class CSSParser {
RefPtr<CSSValue> consumePaintOrder(CSSTokenStream& input);
RefPtr<CSSValue> consumeLonghand(CSSTokenStream& input, CSSPropertyID id);

bool consumeLinearGradientPrelude(CSSTokenStream& input, RefPtr<CSSValue>& angle, RefPtr<CSSValue>& direction);
bool consumeRadialGradientPrelude(CSSTokenStream& input, RefPtr<CSSValue>& shape, RefPtr<CSSValue>& size, RefPtr<CSSValue>& position);
bool consumeConicGradientPrelude(CSSTokenStream& input, RefPtr<CSSValue>& angle, RefPtr<CSSValue>& position);
bool consumeGradientStops(CSSTokenStream& input, CSSGradientType gradientType, CSSGradientStopList& stops);

bool consumeFlex(CSSTokenStream& input, CSSPropertyList& properties, bool important);
bool consumeBackground(CSSTokenStream& input, CSSPropertyList& properties, bool important);
bool consumeColumns(CSSTokenStream& input, CSSPropertyList& properties, bool important);
Expand Down
Loading