Skip to content

Export fill and border colors with an alpha channel as opaque RGB - #48

Open
rtaph wants to merge 1 commit into
pteridin:mainfrom
rtaph:fix/opaque-fill-and-border-colors
Open

rtaph wants to merge 1 commit into
pteridin:mainfrom
rtaph:fix/opaque-fill-and-border-colors

Conversation

@rtaph

@rtaph rtaph commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Fixes #47

The problem

wb_apply_cell_styles() and wb_apply_border() pass flextable's colour strings straight to openxlsx2::wb_color(). R writes a colour with an alpha channel as #RRGGBBAA, but Excel reads eight hex digits as AARRGGBB, so the alpha byte lands in the red channel and the colour shifts by one byte. A translucent yellow such as #EDB50199 (adjustcolor("#edb501", 0.6)) exports as purple; a half-transparent black border exports as dark blue. Nothing errors, the colours are just wrong.

Font colours do not suffer from this because wb_apply_content() already normalises them through prepare_color(). Fills and borders skipped that step.

The fix

  • Fills: the resolved background colour in wb_apply_cell_styles() goes through a new prepare_fill_color(), a thin wrapper around prepare_color() that keeps "transparent" as "transparent" (where prepare_color() returns NA), so the existing != "transparent" guard keeps working.
  • Borders: the four border.color.* columns in wb_apply_border() go through prepare_color() after the existing transparent -> black substitution.

Both use grDevices::col2rgb() under the hood, so named colours, #RRGGBB and #RRGGBBAA all reduce to the opaque #RRGGBB that wb_color() turns into FFRRGGBB. Excel has no translucent solid fills, so dropping the alpha is the only faithful option; it matches what font colours already do.

Tests

Added a regression test in test-wb_apply_cell_styles.R that exports a fill and a border with alpha and asserts the opaque FFEDB501 / FF000000 in the style XML, that a named colour (orange) is unchanged, and that an unfilled cell stays unfilled. It fails on main (Expected style_of("A2", "fill") to match "rgb=\"FFEDB501\"", actual rgb="EDB50199") and passes with the change. The full suite is green locally, with only the pre-existing CRAN-gated and flexlsxtestdir skips.

NAMESPACE already imports dplyr::coalesce, so no roxygen run is needed for the new helper.

I added a NEWS bullet under a development heading; happy to drop or reword it.

Note: This fix was produced with the help of Claude

wb_apply_cell_styles() and wb_apply_border() passed flextable's color
strings straight to openxlsx2::wb_color(). R writes a color with an alpha
channel as #RRGGBBAA, but Excel reads eight hex digits as AARRGGBB, so
the alpha byte became the red channel: a translucent yellow such as
"#EDB50199" (adjustcolor("#edb501", 0.6)) exported as purple, and a
half-transparent black border as dark blue.

Font colors were already normalised through prepare_color(); fills and
borders now go the same way. prepare_fill_color() wraps it so that
"transparent" survives as "transparent" for the existing no-fill check.

Adds a regression test asserting the opaque FFRRGGBB value for a fill
and a border with alpha, and that a named color and an unfilled cell are
unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fill and border colors with an alpha channel (#RRGGBBAA) export as the wrong color

1 participant