mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-28 12:27:11 -06:00
Found by comparing EPA's .doc templates with Word for Windows. Before: custom tab stops in .doc files were skipped, so centred titles, right-aligned dates and dot leaders set with tabs lost their alignment; and a section mark (0x0C) at a section's end became a page break inside a paragraph, so the file's sections (continuous breaks, footers) were lost and every section break started a new page. After: sprmPChgTabsPapx and sprmPChgTabs are read in styles and paragraphs (removed stops clear inherited ones; each added stop keeps its alignment and leader), and sprmPChgTabs' one-byte size is decoded per [MS-DOC] (it was read as two bytes, swallowing the sprms after it). A section mark at a section's end now ends that section's last paragraph, so the section's properties and footers are kept. Both EPA .doc templates in the comparison set now have Word's page count. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
105 lines
3.9 KiB
Rust
105 lines
3.9 KiB
Rust
//! Sprm decoding ([MS-DOC] §2.6). A Prl is a 2-byte opcode followed by an operand whose
|
|
//! size comes from the opcode's `spra` field (with two variable-length exceptions).
|
|
|
|
/// `sprmPChgTabs`: custom tab stops added and removed ([MS-DOC] §2.6.3, PChgTabsOperand).
|
|
pub(crate) const P_CHG_TABS: u16 = 0xC615;
|
|
/// `sprmPChgTabsPapx`: the same for paragraph and style formatting (PChgTabsPapxOperand).
|
|
pub(crate) const P_CHG_TABS_PAPX: u16 = 0xC60D;
|
|
/// `sprmTDefTable`: table cell borders and widths.
|
|
const T_DEF_TABLE: u16 = 0xD608;
|
|
|
|
/// One decoded Prl: an opcode plus its operand bytes.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub(crate) struct Prl<'a> {
|
|
pub(crate) op: u16,
|
|
pub(crate) operand: &'a [u8],
|
|
}
|
|
|
|
impl Prl<'_> {
|
|
/// Which kind of property this sprm modifies: 1 paragraph, 2 character, 3 picture,
|
|
/// 4 section, 5 table ([MS-DOC] Sprm.sgc).
|
|
pub(crate) fn sgc(&self) -> u8 {
|
|
((self.op >> 10) & 7) as u8
|
|
}
|
|
}
|
|
|
|
/// Iterate the Prls of a grpprl. Malformed tails are dropped.
|
|
pub(crate) fn iter(grpprl: &[u8]) -> impl Iterator<Item = Prl<'_>> {
|
|
let mut rest = grpprl;
|
|
std::iter::from_fn(move || {
|
|
if rest.len() < 2 {
|
|
return None;
|
|
}
|
|
let op = u16::from_le_bytes([rest[0], rest[1]]);
|
|
let after = &rest[2..];
|
|
// Operand size per spra, with the exceptions whose size byte lies.
|
|
let len = match (op >> 13, op) {
|
|
// PChgTabsOperand.cb is one byte, the size after it; 255 means "work it out" from the
|
|
// two counts: cb, cTabs, 4 bytes per removed stop, cTabs, 3 bytes per added stop.
|
|
(_, P_CHG_TABS) => match after.first().copied() {
|
|
Some(255) => after.get(1).and_then(|&del| {
|
|
let add = after.get(2 + 4 * del as usize).copied()?;
|
|
Some(1 + 1 + 4 * del as usize + 1 + 3 * add as usize)
|
|
}),
|
|
Some(cb) => Some(1 + cb as usize),
|
|
None => None,
|
|
},
|
|
// TDefTableOperand.cb counts the bytes after cb, incremented by 1, so the
|
|
// operand is one byte longer than cb.
|
|
(_, T_DEF_TABLE) => after.get(..2).map(|b| 1 + u16::from_le_bytes([b[0], b[1]]) as usize),
|
|
(0, _) | (1, _) => Some(1),
|
|
(2, _) | (4, _) | (5, _) => Some(2),
|
|
(3, _) => Some(4),
|
|
(7, _) => Some(3),
|
|
(6, _) => after.first().map(|&n| 1 + n as usize),
|
|
_ => None,
|
|
}?;
|
|
let operand = after.get(..len)?;
|
|
rest = &after[len.min(after.len())..];
|
|
Some(Prl { op, operand })
|
|
})
|
|
}
|
|
|
|
/// A ToggleOperand value: 0 = off, 1 = on, 0x80 = leave unchanged, 0x81 = invert.
|
|
pub(crate) enum Toggle {
|
|
Off,
|
|
On,
|
|
Keep,
|
|
Invert,
|
|
}
|
|
|
|
pub(crate) fn toggle(p: &Prl) -> Toggle {
|
|
match p.operand.first().copied().unwrap_or(0) {
|
|
0x81 => Toggle::Invert,
|
|
0x80 => Toggle::Keep,
|
|
0x00 => Toggle::Off,
|
|
_ => Toggle::On,
|
|
}
|
|
}
|
|
|
|
/// The isprm indices of Prm0 that map to sprms we understand ([MS-DOC] §2.9.215 table).
|
|
pub(crate) fn from_prm0(isprm: u8) -> Option<u16> {
|
|
Some(match isprm {
|
|
0x05 => 0x2461, // sprmPJc
|
|
0x07 => 0x2405, // sprmPFKeep
|
|
0x08 => 0x2406, // sprmPFKeepFollow
|
|
0x09 => 0x2407, // sprmPFPageBreakBefore
|
|
0x33 => 0x2435, // sprmPWidowControl (low byte of the operand is the value)
|
|
0x78 => 0x2640, // sprmPOutLvl
|
|
0x4D => 0x2A0C, // sprmCHighlight
|
|
0x55 => 0x0835, // sprmCFBold
|
|
0x56 => 0x0836, // sprmCFItalic
|
|
0x57 => 0x0837, // sprmCFStrike
|
|
0x58 => 0x0838, // sprmCFOutline
|
|
0x59 => 0x0839, // sprmCFShadow
|
|
0x5A => 0x083A, // sprmCFSmallCaps
|
|
0x5B => 0x083B, // sprmCFCaps
|
|
0x5C => 0x083C, // sprmCFVanish
|
|
0x5E => 0x2A3E, // sprmCKul
|
|
0x62 => 0x2A42, // sprmCIco
|
|
0x74 => 0x0854, // sprmCFImprint
|
|
0x75 => 0x0855, // sprmCFSpec
|
|
_ => return None,
|
|
})
|
|
}
|