collective

Browse / github.com/storytold/wordcraft / patch

#3406: .doc highlight colours are read as an Ico index, like yellow

#3406: .doc highlight colours are read as an Ico index, like yellow …ys yellow sprmCHighlight in a Word 97-2003 file carries an Ico value ([MS-DOC] 2.9.135), the same palette as the text colour: 1 = black, 7 = yellow, 9 = dark blue. The reader looked the operand up in a table in highlighter-row order instead, so yellow (7) came out dark blue and black and white could never be read. Map the operand with the Ico order and add a regression test that builds a .doc for each of the 16 highlight colours and checks the colour read back. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summar

Status
open
Score
0.00
Adopters
0 other operators
Author
PasqualeIannace (imported from pull request #4433, open)
Base
6a2b520fa49aa0d0595cea36589aaf16ca371cd3
Size
+47 −6 in 2 files
Review
pending
Shared
2026-10-28
Id
0edd1f0cdef9475aca8e9d22

How it went

No other operator has reported using it yet.

Diff

raw · crates/docbin/src/fmt.rs crates/docbin/src/tests.rs

diff --git a/crates/docbin/src/fmt.rs b/crates/docbin/src/fmt.rs--- a/crates/docbin/src/fmt.rs+++ b/crates/docbin/src/fmt.rs@@ -78,15 +78,19 @@ const ICO: [Rgb; 17] = [     Rgb(192, 192, 192), ]; -/// Highlight palette ([MS-DOC] §2.9.135 Highlight, order of Word's highlighter row).+/// Highlight palette. The `sprmCHighlight` operand is an `Ico` ([MS-DOC] §2.9.135), the same+/// index as the text-colour palette above (1 = black, 7 = yellow, 9 = dark blue, ...), not the+/// order of Word's highlighter row. 0 = no highlight. const HL: [Highlight; 17] = [     Highlight::None,-    Highlight::Yellow,-    Highlight::BrightGreen,+    Highlight::Black,+    Highlight::Blue,     Highlight::Turquoise,+    Highlight::BrightGreen,     Highlight::Pink,-    Highlight::Blue,     Highlight::Red,+    Highlight::Yellow,+    Highlight::White,     Highlight::DarkBlue,     Highlight::Teal,     Highlight::Green,@@ -95,8 +99,6 @@ const HL: [Highlight; 17] = [     Highlight::DarkYellow,     Highlight::Gray50,     Highlight::Gray25,-    Highlight::None,-    Highlight::None, ];  /// The `Ico` palette colour (0 = automatic → `None`).diff --git a/crates/docbin/src/tests.rs b/crates/docbin/src/tests.rs--- a/crates/docbin/src/tests.rs+++ b/crates/docbin/src/tests.rs@@ -613,6 +613,7 @@ fn para0(doc: &wordcraft_doc::Document) -> wordcraft_doc::Paragraph {     doc.body.first().and_then(|b| b.as_para().cloned()).unwrap_or_default() } +/// Direct character formatting (bold, text colour) lands on exactly the runs it covers. #[test] fn character_formatting_applied() {     let spec = RichSpec {@@ -636,6 +637,44 @@ fn character_formatting_applied() {     assert!(p.runs.first().is_some_and(|r| r.props.bold.is_none())); } +/// `sprmCHighlight` carries an `Ico` index ([MS-DOC] §2.9.135): yellow is 7 and dark blue is 9.+/// It used to be read in highlighter-row order, which turned yellow highlights dark blue.+#[test]+fn highlight_operand_is_an_ico_index() {+    use wordcraft_doc::props::Highlight;+    let cases = [+        (1u8, Highlight::Black),+        (2, Highlight::Blue),+        (3, Highlight::Turquoise),+        (4, Highlight::BrightGreen),+        (5, Highlight::Pink),+        (6, Highlight::Red),+        (7, Highlight::Yellow),+        (8, Highlight::White),+        (9, Highlight::DarkBlue),+        (10, Highlight::Teal),+        (11, Highlight::Green),+        (12, Highlight::Violet),+        (13, Highlight::DarkRed),+        (14, Highlight::DarkYellow),+        (15, Highlight::Gray50),+        (16, Highlight::Gray25),+    ];+    for (ico, want) in cases {+        let spec = RichSpec {+            text: "marked\r",+            styles: vec![(1, 0x0FFF, "Normal", Vec::new(), Vec::new())],+            fonts: vec!["Arial"],+            chpx_runs: vec![(0, grpprl(&[(0x2A0C, &[ico])]))],+            papx_entries: vec![(0, 0, Vec::new())],+        };+        let doc = read(&rich_doc(&spec)).expect("opens");+        let p = para0(&doc);+        assert_eq!(p.runs.first().and_then(|r| r.props.highlight), Some(want), "ico {ico}: {:?}", p.runs);+    }+}++/// A font and size come from a style's character formatting, and a run's own font overrides it. #[test] fn font_and_size_from_style_and_direct() {     let spec = RichSpec {