mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-26 11:27:10 -06:00
519 lines
21 KiB
Rust
519 lines
21 KiB
Rust
//! Tables: the table sprms (sgc 5) carried by a row-end (TTP) mark's grpprl, and the
|
|
//! assembly of cell-ended paragraphs into [`Table`] blocks ([MS-DOC] §2.6.3, §2.4.3).
|
|
|
|
use std::sync::Arc;
|
|
|
|
use wordcraft_doc::Rgb;
|
|
use wordcraft_doc::para::Paragraph;
|
|
use wordcraft_doc::props::{Align, Border, BorderStyle, Borders, CellProps, HeightRule, RowProps, TableProps, VAlign, VMerge};
|
|
use wordcraft_doc::table::{Cell, Row, Table};
|
|
use wordcraft_doc::{Block, Blocks, para_block};
|
|
|
|
use crate::fmt;
|
|
use crate::sprm::{self, Prl};
|
|
|
|
// Paragraph sprms for table membership.
|
|
const P_IN_TABLE: u16 = 0x2416;
|
|
const P_TTP: u16 = 0x2417;
|
|
/// `sprmPItap`: table depth (we only model depth 1; deeper rows fall back to paragraphs).
|
|
const P_ITAP: u16 = 0x6649;
|
|
|
|
// Table sprms ([MS-DOC] §2.6.3).
|
|
const T_JC: u16 = 0x5400;
|
|
const T_JC_90: u16 = 0x548A;
|
|
const T_DXA_LEFT: u16 = 0x9601;
|
|
const T_DXA_GAP_HALF: u16 = 0x9602;
|
|
const T_TABLE_HEADER: u16 = 0x3404;
|
|
const T_TABLE_BORDERS_80: u16 = 0xD605;
|
|
const T_ROW_HEIGHT: u16 = 0x9407;
|
|
const T_DEF_TABLE: u16 = 0xD608;
|
|
const T_DEF_TABLE_SHD_80: u16 = 0xD609;
|
|
/// `sprmTDefTableShd` / `Shd2nd` / `Shd3rd`: 10-byte `Shd`s for cells 0-21, 22-43, 44-62.
|
|
const T_DEF_TABLE_SHD: u16 = 0xD612;
|
|
const T_DEF_TABLE_SHD_2ND: u16 = 0xD616;
|
|
const T_DEF_TABLE_SHD_3RD: u16 = 0xD60C;
|
|
/// `sprmTDefTableShdRaw` / `Raw2nd` / `Raw3rd`: the same `Shd` arrays over the same cell
|
|
/// ranges, which Word 2007+ writes next to the above.
|
|
const T_DEF_TABLE_SHD_RAW: u16 = 0xD670;
|
|
const T_DEF_TABLE_SHD_RAW_2ND: u16 = 0xD671;
|
|
const T_DEF_TABLE_SHD_RAW_3RD: u16 = 0xD672;
|
|
const T_CANT_SPLIT_90: u16 = 0x3403;
|
|
const T_MERGE: u16 = 0x5624;
|
|
const T_VERT_MERGE: u16 = 0xD62B;
|
|
const T_VERT_ALIGN: u16 = 0xD62C;
|
|
const T_SET_SHD: u16 = 0xD62D;
|
|
const T_CANT_SPLIT: u16 = 0x3466;
|
|
const T_CELL_WIDTH: u16 = 0xD635;
|
|
|
|
/// Most grid columns we build from a `sprmTDefTable` (the model's own cap is 63; one more
|
|
/// edge is fine but merged rows can sum spans slightly over, so clamp when assembling).
|
|
const MAX_COLS: usize = 63;
|
|
|
|
/// What a paragraph mark says about its table context, from its PAPX.
|
|
#[derive(Default)]
|
|
pub(crate) struct RowInfo {
|
|
pub(crate) in_table: bool,
|
|
pub(crate) ttp: bool,
|
|
/// The mark itself was 0x07 (a cell end), set by the walker.
|
|
pub(crate) cell_mark: bool,
|
|
/// Table depth from `sprmPItap` (0 or 1 supported; deeper tables degrade to paragraphs).
|
|
pub(crate) depth: u32,
|
|
/// Cell-edge positions (points) from `sprmTDefTable` — [MS-DOC] defines them per row,
|
|
/// so each row keeps its own; ascending in well-formed rows, the first edge usually 0
|
|
/// (the row origin). Rows without a `sprmTDefTable` keep this empty.
|
|
pub(crate) edges: Vec<f32>,
|
|
height: Option<f32>,
|
|
height_rule: HeightRule,
|
|
header: bool,
|
|
cant_split: bool,
|
|
align: Option<Align>,
|
|
indent: Option<f32>,
|
|
borders: Option<Borders>,
|
|
/// Horizontal merges: (itcFirst, itcLim) cell-index ranges.
|
|
merges: Vec<(u8, u8)>,
|
|
/// Per-cell overrides keyed by (pre-merge) cell index.
|
|
vmerge: Vec<(u8, VMerge)>,
|
|
/// Later entries win: the `Shd` arrays override the `Shd80` fallback before them, and an
|
|
/// automatic `Shd` (`None`) clears it.
|
|
shading: Vec<(u8, Option<Rgb>)>,
|
|
widths: Vec<(u8, f32)>,
|
|
width_pcts: Vec<(u8, f32)>,
|
|
valign: Vec<(u8, VAlign)>,
|
|
}
|
|
|
|
/// Read the table context out of a paragraph mark's PAPX grpprl.
|
|
impl RowInfo {
|
|
/// The row height from `sprmTDyaRowHeight`, for tests.
|
|
#[cfg(test)]
|
|
pub(crate) fn height(&self) -> Option<f32> {
|
|
self.height
|
|
}
|
|
|
|
/// The fill cell `i` ends up with (the last shading entry for it wins), for tests.
|
|
#[cfg(test)]
|
|
pub(crate) fn shading_of(&self, i: u8) -> Option<Rgb> {
|
|
self.shading.iter().rfind(|(k, _)| *k == i).and_then(|(_, s)| *s)
|
|
}
|
|
|
|
/// The width cell `i` ends up with, for tests.
|
|
#[cfg(test)]
|
|
pub(crate) fn width_of(&self, i: u8) -> Option<f32> {
|
|
self.widths.iter().rfind(|(k, _)| *k == i).map(|(_, w)| *w)
|
|
}
|
|
|
|
/// The width percentage cell `i` ends up with, for tests.
|
|
#[cfg(test)]
|
|
pub(crate) fn width_pct_of(&self, i: u8) -> Option<f32> {
|
|
self.width_pcts.iter().rfind(|(k, _)| *k == i).map(|(_, w)| *w)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn decode(papx: &[u8]) -> RowInfo {
|
|
let mut r = RowInfo::default();
|
|
let mut gap_half: Option<i16> = None;
|
|
let mut dxa_left: Option<i16> = None;
|
|
for prl in sprm::iter(papx) {
|
|
// The membership sprms are paragraph sprms (sgc 1); the rest here are table sprms
|
|
// (sgc 5), so match opcodes directly.
|
|
match prl.op {
|
|
P_IN_TABLE => r.in_table = prl.operand.first().copied().unwrap_or(0) != 0,
|
|
P_TTP => r.ttp = prl.operand.first().copied().unwrap_or(0) != 0,
|
|
P_ITAP => r.depth = i32_of(&prl).unwrap_or(0).max(0) as u32,
|
|
T_DEF_TABLE => r.edges = def_table_edges(prl.operand),
|
|
T_TABLE_HEADER => r.header = prl.operand.first().copied().unwrap_or(0) != 0,
|
|
T_CANT_SPLIT | T_CANT_SPLIT_90 => r.cant_split = prl.operand.first().copied().unwrap_or(0) != 0,
|
|
T_ROW_HEIGHT => {
|
|
if let Some(v) = s16(&prl) {
|
|
if v < 0 {
|
|
r.height_rule = HeightRule::Exact;
|
|
r.height = Some((-(v as f32) / 20.0).clamp(0.0, 1584.0));
|
|
} else {
|
|
r.height_rule = HeightRule::AtLeast;
|
|
r.height = Some((v as f32 / 20.0).clamp(0.0, 1584.0));
|
|
}
|
|
}
|
|
}
|
|
T_JC | T_JC_90 => {
|
|
r.align = match u16_of(&prl).unwrap_or(0) {
|
|
1 => Some(Align::Center),
|
|
2 => Some(Align::Right),
|
|
_ => Some(Align::Left),
|
|
};
|
|
}
|
|
T_DXA_LEFT => dxa_left = s16(&prl),
|
|
T_DXA_GAP_HALF => gap_half = s16(&prl),
|
|
T_TABLE_BORDERS_80 => r.borders = table_borders(prl.operand),
|
|
T_MERGE => {
|
|
if let [a, b, ..] = prl.operand
|
|
&& b > a
|
|
{
|
|
r.merges.push((*a, *b));
|
|
}
|
|
}
|
|
T_VERT_MERGE => {
|
|
// cb, itc, flag: 1 = continues the merge from above, 3 = restarts it.
|
|
if let [_, itc, flag, ..] = prl.operand {
|
|
let m = match flag {
|
|
3 => VMerge::Restart,
|
|
1 => VMerge::Continue,
|
|
_ => VMerge::None,
|
|
};
|
|
if m != VMerge::None {
|
|
r.vmerge.push((*itc, m));
|
|
}
|
|
}
|
|
}
|
|
T_SET_SHD => {
|
|
if let Some(rgb) = prl.operand.get(3..).and_then(shd_rgb)
|
|
&& let [a, b, ..] = prl.operand.get(1..3).unwrap_or(&[])
|
|
{
|
|
for c in *a..(*b).min(MAX_COLS as u8) {
|
|
r.shading.push((c, Some(rgb)));
|
|
}
|
|
}
|
|
}
|
|
T_DEF_TABLE_SHD_80 => {
|
|
let cells = prl.operand.get(1..).unwrap_or(&[]);
|
|
for (i, b) in cells.as_chunks::<2>().0.iter().take(MAX_COLS).enumerate() {
|
|
if let Some(rgb) = shd80_rgb(b) {
|
|
r.shading.push((i as u8, Some(rgb)));
|
|
}
|
|
}
|
|
}
|
|
T_DEF_TABLE_SHD | T_DEF_TABLE_SHD_RAW | T_DEF_TABLE_SHD_2ND | T_DEF_TABLE_SHD_RAW_2ND | T_DEF_TABLE_SHD_3RD | T_DEF_TABLE_SHD_RAW_3RD => {
|
|
// Each array covers its own cell range (0-21, 22-43, 44-62); extra entries
|
|
// are ignored so an over-long array can't override the next range.
|
|
let (first, lim) = match prl.op {
|
|
T_DEF_TABLE_SHD_2ND | T_DEF_TABLE_SHD_RAW_2ND => (22, 44),
|
|
T_DEF_TABLE_SHD_3RD | T_DEF_TABLE_SHD_RAW_3RD => (44, MAX_COLS),
|
|
_ => (0, 22),
|
|
};
|
|
let cells = prl.operand.get(1..).unwrap_or(&[]);
|
|
let raw = matches!(prl.op, T_DEF_TABLE_SHD_RAW | T_DEF_TABLE_SHD_RAW_2ND | T_DEF_TABLE_SHD_RAW_3RD);
|
|
for (i, b) in cells.as_chunks::<10>().0.iter().take(lim - first).enumerate() {
|
|
// In the Raw arrays a nil Shd (ipat 0xFFFF) means "as the table style
|
|
// shades it", so it leaves any earlier fill alone.
|
|
if raw && b.get(8..10) == Some(&[0xFF, 0xFF]) {
|
|
continue;
|
|
}
|
|
r.shading.push(((first + i) as u8, shd_rgb(b)));
|
|
}
|
|
}
|
|
T_CELL_WIDTH => {
|
|
// Operand: cb (1 byte), itcFirst (1 byte), itcLim (1 byte), ftsWidth (1 byte), wWidth (2 bytes).
|
|
// [MS-DOC] §2.9.290 sprmTCellWidth (0xD635).
|
|
// ftsWidth: 3 = twips (dxa), 2 = percentage (50ths of a percent).
|
|
if let [_, a, b, fts, w0, w1, ..] = prl.operand {
|
|
if *fts == 3 {
|
|
let w = (i16::from_le_bytes([*w0, *w1]) as f32 / 20.0).clamp(0.0, 1584.0);
|
|
for c in *a..(*b).min(MAX_COLS as u8) {
|
|
r.widths.push((c, w));
|
|
}
|
|
} else if *fts == 2 {
|
|
let pct = (u16::from_le_bytes([*w0, *w1]) as f32 / 50.0).clamp(0.0, 100.0);
|
|
for c in *a..(*b).min(MAX_COLS as u8) {
|
|
r.width_pcts.push((c, pct));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
T_VERT_ALIGN => {
|
|
if let [_, a, b, va, ..] = prl.operand {
|
|
let v = match va {
|
|
1 => VAlign::Center,
|
|
2 => VAlign::Bottom,
|
|
_ => VAlign::Top,
|
|
};
|
|
for c in *a..*b {
|
|
r.valign.push((c, v));
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
// The table indent is the row origin minus half the cell gap.
|
|
if let Some(l) = dxa_left {
|
|
let gap = gap_half.unwrap_or(0) as f32 / 20.0;
|
|
r.indent = Some(((l as f32 / 20.0) - gap).clamp(-1584.0, 1584.0));
|
|
}
|
|
r
|
|
}
|
|
|
|
/// A finished paragraph plus the bookkeeping the block assembly needs.
|
|
pub(crate) struct ParaOut {
|
|
pub(crate) para: Paragraph,
|
|
/// CP just past the terminating mark.
|
|
pub(crate) end_cp: u32,
|
|
/// Table context from the mark's PAPX (empty default = not in a table).
|
|
pub(crate) row: RowInfo,
|
|
}
|
|
|
|
/// Group consecutive in-table paragraphs into [`Table`] blocks; the rest pass through.
|
|
/// Rows that never close (or tables nested deeper than we model) degrade to paragraphs so
|
|
/// no text is lost.
|
|
pub(crate) fn assemble(paras: Vec<ParaOut>) -> Blocks {
|
|
let mut out: Blocks = Vec::new();
|
|
let mut rows: Vec<Row> = Vec::new();
|
|
let mut cells: Vec<Cell> = Vec::new();
|
|
let mut cell: Blocks = Vec::new();
|
|
let mut grid: Vec<f32> = Vec::new();
|
|
let mut props = TableProps::default();
|
|
let mut open = false;
|
|
let mut depth_seen = 0u32;
|
|
|
|
fn flush_cell(cells: &mut Vec<Cell>, cell: &mut Blocks) {
|
|
let mut blocks = std::mem::take(cell);
|
|
if blocks.is_empty() {
|
|
blocks.push(para_block(Paragraph::new()));
|
|
}
|
|
cells.push(Cell { props: CellProps { span: 1, ..Default::default() }, blocks, controls: Default::default() });
|
|
}
|
|
fn build_row(mut cells: Vec<Cell>, info: &RowInfo) -> Row {
|
|
// Per-cell overrides first (sprm cell indices count pre-merge cells), then merges.
|
|
for (i, c) in cells.iter_mut().enumerate() {
|
|
let i = i as u8;
|
|
if let Some((_, m)) = info.vmerge.iter().find(|(k, _)| *k == i) {
|
|
c.props.vmerge = *m;
|
|
}
|
|
if let Some((_, w)) = info.widths.iter().rfind(|(k, _)| *k == i) {
|
|
c.props.width = Some(*w);
|
|
}
|
|
if let Some((_, pct)) = info.width_pcts.iter().rfind(|(k, _)| *k == i) {
|
|
c.props.width_pct = Some(*pct);
|
|
}
|
|
if let Some((_, s)) = info.shading.iter().rfind(|(k, _)| *k == i) {
|
|
c.props.shading = *s;
|
|
}
|
|
if let Some((_, v)) = info.valign.iter().rfind(|(k, _)| *k == i) {
|
|
c.props.valign = Some(*v);
|
|
}
|
|
}
|
|
for &(a, b) in info.merges.iter().take(MAX_COLS) {
|
|
let a = a as usize;
|
|
let b = (b as usize).min(cells.len());
|
|
if a < b && b <= cells.len() && b - a > 1 {
|
|
let removed: Vec<Cell> = cells.drain(a + 1..b).collect();
|
|
if let Some(first) = cells.get_mut(a) {
|
|
first.props.span = ((b - a) as u32).min(63);
|
|
for c in removed {
|
|
first.blocks.extend(c.blocks.into_iter().filter(|x| x.as_para().is_none_or(|p| !p.is_empty())));
|
|
}
|
|
if first.blocks.len() > 1 && first.blocks.first().and_then(|x| x.as_para()).is_some_and(Paragraph::is_empty) {
|
|
first.blocks.remove(0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Row {
|
|
props: RowProps {
|
|
height: info.height,
|
|
height_rule: info.height_rule,
|
|
header: info.header,
|
|
cant_split: info.cant_split,
|
|
..Default::default()
|
|
},
|
|
cells,
|
|
controls: Default::default(),
|
|
}
|
|
}
|
|
fn flush_table(out: &mut Blocks, rows: &mut Vec<Row>, cells: &mut Vec<Cell>, cell: &mut Blocks, grid: &[f32], props: &TableProps, nested: bool) {
|
|
if !cells.is_empty() || !cell.is_empty() {
|
|
flush_cell(cells, cell);
|
|
// A stray trailing row (no TTP of its own): size its cells from the grid.
|
|
let mut stray = build_row(std::mem::take(cells), &RowInfo::default());
|
|
for (i, c) in stray.cells.iter_mut().enumerate() {
|
|
if c.props.width.is_none() && c.props.span == 1 {
|
|
c.props.width = grid.get(i).copied();
|
|
}
|
|
}
|
|
rows.push(stray);
|
|
}
|
|
if rows.is_empty() {
|
|
return;
|
|
}
|
|
if nested {
|
|
// Unclosed or nested deeper than we model: keep the text, drop the table shape.
|
|
for r in rows.iter() {
|
|
for c in &r.cells {
|
|
out.extend(c.blocks.iter().cloned());
|
|
}
|
|
}
|
|
} else {
|
|
out.push(Arc::new(Block::Table(Table {
|
|
props: props.clone(),
|
|
grid: grid.to_vec(),
|
|
rows: std::mem::take(rows),
|
|
controls: Default::default(),
|
|
})));
|
|
}
|
|
rows.clear();
|
|
}
|
|
|
|
for p in paras {
|
|
let info = p.row;
|
|
let is_table = info.in_table || info.ttp || info.cell_mark;
|
|
if !is_table {
|
|
if open {
|
|
flush_table(&mut out, &mut rows, &mut cells, &mut cell, &grid, &props, depth_seen > 1);
|
|
grid.clear();
|
|
// Table props are per-table (first row carrying the sprm wins within the
|
|
// table): reset the accumulator so one table's borders, align, indent and
|
|
// cell margins never leak into the next (the repro's first table wrote
|
|
// Word's default 108-twip gap, and every later table — including the
|
|
// change history with its own 40-twip gap — inherited 5.4 pt margins).
|
|
props = TableProps::default();
|
|
open = false;
|
|
depth_seen = 0;
|
|
}
|
|
out.push(para_block(p.para));
|
|
continue;
|
|
}
|
|
if !open {
|
|
open = true;
|
|
}
|
|
depth_seen = depth_seen.max(info.depth);
|
|
if grid.is_empty() {
|
|
// Column widths from the table's first `sprmTDefTable`.
|
|
grid = info.edges.windows(2).take(MAX_COLS).map(|w| (w[1] - w[0]).max(0.0)).collect();
|
|
}
|
|
if info.borders.is_some() && props.borders.is_none() {
|
|
props.borders = info.borders;
|
|
}
|
|
if props.align.is_none() {
|
|
props.align = info.align;
|
|
}
|
|
if props.indent.is_none() {
|
|
props.indent = info.indent;
|
|
}
|
|
if info.cell_mark {
|
|
// A row-end (TTP) mark terminates the row. Word writes rows as N content
|
|
// cells each ended by their own cell mark, followed by an empty row-end mark
|
|
// that carries the row's TDefTable (the 3GPP change history: 8 content marks
|
|
// + an empty TTP whose TDefTable defines itcMac = 8). When that mark ends no
|
|
// content of its own and the TDefTable already has a column per flushed cell,
|
|
// it is a pure row terminator, not the end of a phantom (N+1)th cell — such a
|
|
// cell has no column (width None) and shifts every real cell's span one grid
|
|
// column over. A TTP mark with content, or one short of the TDefTable's
|
|
// columns, still ends a real cell (rows without a TDefTable keep today's
|
|
// shape: the count is unknowable, so the mark always flushes).
|
|
let row_end_only = info.ttp && p.para.text.is_empty() && cell.is_empty() && !info.edges.is_empty() && cells.len() + 1 >= info.edges.len();
|
|
// ...but a row-end mark with no cells before it still makes a (one-cell) row.
|
|
let row_end_only = row_end_only && !cells.is_empty();
|
|
if !p.para.text.is_empty() {
|
|
cell.push(para_block(p.para));
|
|
}
|
|
if !row_end_only {
|
|
flush_cell(&mut cells, &mut cell);
|
|
}
|
|
if info.ttp {
|
|
let row = build_row(std::mem::take(&mut cells), &info);
|
|
rows.push(row);
|
|
}
|
|
} else {
|
|
cell.push(para_block(p.para));
|
|
}
|
|
}
|
|
if open {
|
|
flush_table(&mut out, &mut rows, &mut cells, &mut cell, &grid, &props, depth_seen > 1);
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Cell-edge positions (points) from a `sprmTDefTable` operand: cb u16, itcMac u8, then
|
|
/// itcMac+1 twip boundaries followed by the TC80s (skipped). Edges are returned as stored:
|
|
/// per-row, in operand order; sorting and capping happen when the table grid is assembled.
|
|
fn def_table_edges(operand: &[u8]) -> Vec<f32> {
|
|
let itc = operand.get(2).copied().unwrap_or(0) as usize;
|
|
if itc == 0 {
|
|
return Vec::new();
|
|
}
|
|
// itcMac is a u8, so this allocation stays tiny; the loop stops at the operand's end.
|
|
let mut edges = Vec::with_capacity((itc + 1).min(256));
|
|
for i in 0..=itc {
|
|
match operand.get(3 + i * 2..3 + i * 2 + 2) {
|
|
Some(b) => edges.push(i16::from_le_bytes([b[0], b[1]]) as f32 / 20.0),
|
|
None => break,
|
|
}
|
|
}
|
|
edges
|
|
}
|
|
|
|
/// Table borders from `sprmTTableBorders80`: cb (0x18) then six Brc80s in the order top,
|
|
/// left, bottom, right, inside-horizontal, inside-vertical.
|
|
fn table_borders(operand: &[u8]) -> Option<Borders> {
|
|
let b = |i: usize| operand.get(1 + i * 4..1 + i * 4 + 4).and_then(brc80);
|
|
Some(Borders { top: b(0), left: b(1), bottom: b(2), right: b(3), between: b(4), inside_v: b(5) })
|
|
}
|
|
|
|
/// One Brc80: width in 1/8 pt, type, palette colour, space/flags byte.
|
|
fn brc80(b: &[u8]) -> Option<Border> {
|
|
let [w, ty, ico, flags] = b else { return None };
|
|
let style = match ty {
|
|
0x00 => BorderStyle::None,
|
|
0x02 | 0x15 => BorderStyle::Thick,
|
|
0x03 => BorderStyle::Double,
|
|
0x04 | 0x05 => BorderStyle::Dotted,
|
|
0x06 | 0x07 => BorderStyle::Dashed,
|
|
0x08 | 0x09 => BorderStyle::DotDash,
|
|
0x0A | 0x18 => BorderStyle::Wave,
|
|
0x11 => BorderStyle::Triple,
|
|
_ => BorderStyle::Single,
|
|
};
|
|
if *ty == 0 && *w == 0 {
|
|
return None;
|
|
}
|
|
Some(Border { style, width: (*w as f32 / 8.0).clamp(0.25, 12.0), color: fmt::ico(*ico), space: (flags & 0x1F) as f32 })
|
|
}
|
|
|
|
/// Background colour from a 10-byte `Shd`: cvFore, cvBack (COLORREFs), ipat. Solid patterns
|
|
/// show the foreground; clear (0) and the other patterns show the background (Word writes a
|
|
/// plain cell fill as clear with cvBack); nil (0xFFFF) and automatic colours are no fill.
|
|
fn shd_rgb(b: &[u8]) -> Option<Rgb> {
|
|
let cv = |b: &[u8]| -> Option<Rgb> {
|
|
match b {
|
|
[_, _, _, 0xFF] => None, // fAuto
|
|
[r, g, bl, _] => Some(Rgb(*r, *g, *bl)),
|
|
_ => None,
|
|
}
|
|
};
|
|
let ipat = b.get(8..10).map(|x| u16::from_le_bytes([x[0], x[1]]))?;
|
|
match ipat {
|
|
0xFFFF => None,
|
|
1 => cv(b.get(0..4)?),
|
|
_ => cv(b.get(4..8)?),
|
|
}
|
|
}
|
|
|
|
/// Background colour from a 2-byte `Shd80` (icoFore, icoBack, ipat bit-fields). As in
|
|
/// [`shd_rgb`], solid shows the foreground and clear (0) the background, which is how Word
|
|
/// 97 writes a plain fill; ico 0 is automatic (no fill).
|
|
fn shd80_rgb(b: &[u8]) -> Option<Rgb> {
|
|
let v = u16::from_le_bytes([*b.first()?, *b.get(1)?]);
|
|
let fore = v & 0x1F;
|
|
let back = (v >> 5) & 0x1F;
|
|
let ipat = v >> 10;
|
|
if fore == 0x1F && back == 0x1F {
|
|
return None;
|
|
}
|
|
fmt::ico(if ipat == 1 { fore as u8 } else { back as u8 })
|
|
}
|
|
|
|
fn u16_of(p: &Prl) -> Option<u16> {
|
|
match p.operand {
|
|
[b0, b1, ..] => Some(u16::from_le_bytes([*b0, *b1])),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn s16(p: &Prl) -> Option<i16> {
|
|
u16_of(p).map(|v| v as i16)
|
|
}
|
|
|
|
fn i32_of(p: &Prl) -> Option<i32> {
|
|
match p.operand {
|
|
[b0, b1, b2, b3, ..] => Some(i32::from_le_bytes([*b0, *b1, *b2, *b3])),
|
|
_ => None,
|
|
}
|
|
}
|