mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 04:50:18 +00:00
Place: Word .doc styles are resolved once and damaged property tables are refused
Each style's formatting is now worked out once, on top of the style it is based on, and each property run's formatting once, so a document with many hidden paragraphs or long style chains no longer resolves the chains again for every paragraph and run. Character styles are applied as a change to the paragraph's formatting. A bin table whose file positions do not rise, a property page listed twice, pages whose runs overlap, and styles based on each other in a loop are refused as damaged, as MS-DOC requires (PlcBteChpx, ChpxFkp, StdfBase). Property runs refer to their page instead of copying it.
This commit is contained in:
@@ -39,7 +39,8 @@
|
||||
//! other stories, tracked changes and tables. A stream larger than [`MAX_STREAM`] bytes, a main
|
||||
//! text longer than [`text::MAX_TEXT`] characters, more than [`MAX_PIECES`] pieces or
|
||||
//! [`MAX_PAGES`] property pages, and placed type of more than [`MAX_PARAGRAPHS`] paragraphs or
|
||||
//! [`MAX_RUNS`] runs are refused, and so is a structure that lies outside its stream.
|
||||
//! [`MAX_RUNS`] runs are refused, and so are a structure that lies outside its stream, property
|
||||
//! pages whose runs are listed twice or overlap, and styles based on each other in a loop.
|
||||
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
@@ -71,8 +72,6 @@ const MAX_PIECES: usize = 100_000;
|
||||
const MAX_PAGES: usize = 16_384;
|
||||
/// The most styles read from the style sheet (`Stshif.cstd` is below 0x0FFE).
|
||||
const MAX_STYLES: usize = 0x0FFE;
|
||||
/// The most styles one style's chain of `istdBase` takes in.
|
||||
const MAX_BASED_ON: usize = 32;
|
||||
/// The most fonts read from the font table (SttbfFfn's `cData` is at most 0x7FF0).
|
||||
const MAX_FONTS: usize = 0x7FF0;
|
||||
/// The most bytes of a font name kept.
|
||||
@@ -359,17 +358,21 @@ enum Fkp {
|
||||
}
|
||||
|
||||
/// The file positions `start..end` and the properties a property page gives them.
|
||||
struct Run {
|
||||
struct Run<'a> {
|
||||
start: u32,
|
||||
end: u32,
|
||||
/// The paragraph style (Papx only).
|
||||
istd: u16,
|
||||
grpprl: Vec<u8>,
|
||||
/// The properties, in their property page.
|
||||
grpprl: &'a [u8],
|
||||
}
|
||||
|
||||
/// The runs of the property pages the bin table `plc` (PlcBteChpx or PlcBtePapx) points to, in
|
||||
/// the WordDocument stream `wd`, ordered by file position.
|
||||
fn runs(wd: &[u8], plc: &[u8], kind: Fkp, cmd: &str) -> Result<Vec<Run>> {
|
||||
/// the WordDocument stream `wd`, ordered by file position. The bin table's file positions rise
|
||||
/// ("MUST be sorted in ascending order", and a PlcBteChpx "MUST NOT contain duplicate stream
|
||||
/// offsets"), and so do the runs of its pages one after another: a page listed twice, or pages
|
||||
/// whose runs overlap, are damaged.
|
||||
fn runs<'a>(wd: &'a [u8], plc: &[u8], kind: Fkp, cmd: &str) -> Result<Vec<Run<'a>>> {
|
||||
let broken = || damaged(cmd, "its character or paragraph properties are damaged");
|
||||
if plc.len() < 4 || !(plc.len() - 4).is_multiple_of(8) {
|
||||
return Err(broken());
|
||||
@@ -378,19 +381,28 @@ fn runs(wd: &[u8], plc: &[u8], kind: Fkp, cmd: &str) -> Result<Vec<Run>> {
|
||||
if n > MAX_PAGES {
|
||||
return Err(bad(cmd, format!("the Word document has more than {MAX_PAGES} pages of properties, too many to place")));
|
||||
}
|
||||
let mut out = vec![];
|
||||
let fcs = (0..=n).map(|i| u32_at(plc, 4 * i)).collect::<Option<Vec<_>>>().ok_or_else(broken)?;
|
||||
if fcs.windows(2).any(|w| w.first() >= w.get(1)) {
|
||||
return Err(broken());
|
||||
}
|
||||
let mut out: Vec<Run<'a>> = vec![];
|
||||
for i in 0..n {
|
||||
// PnFkpChpx and PnFkpPapx: the page number in bits 0 to 21.
|
||||
let pn = u32_at(plc, 4 * (n + 1) + 4 * i).ok_or_else(broken)? & 0x003F_FFFF;
|
||||
let page = bytes_at(wd, pn as usize * 512, 512).ok_or_else(broken)?;
|
||||
let first = out.len();
|
||||
page_runs(page, kind, &mut out).ok_or_else(broken)?;
|
||||
// A page's own runs rise (rgfc); its first starts at or after the end of the page before.
|
||||
let before = first.checked_sub(1).and_then(|k| out.get(k)).map(|r| r.end);
|
||||
if before.zip(out.get(first)).is_some_and(|(end, r)| r.start < end) {
|
||||
return Err(broken());
|
||||
}
|
||||
}
|
||||
out.sort_by_key(|r| r.start);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The runs of one 512-byte property page; None when it is damaged.
|
||||
fn page_runs(page: &[u8], kind: Fkp, out: &mut Vec<Run>) -> Option<()> {
|
||||
fn page_runs<'a>(page: &'a [u8], kind: Fkp, out: &mut Vec<Run<'a>>) -> Option<()> {
|
||||
let count = usize::from(*page.get(511)?);
|
||||
let most = if kind == Fkp::Chpx { 0x65 } else { 0x1D };
|
||||
if count == 0 || count > most {
|
||||
@@ -402,10 +414,10 @@ fn page_runs(page: &[u8], kind: Fkp, out: &mut Vec<Run>) -> Option<()> {
|
||||
let entry = 4 * (count + 1) + if kind == Fkp::Chpx { i } else { 13 * i };
|
||||
let offset = usize::from(*page.get(entry)?) * 2;
|
||||
let (istd, grpprl) = match (offset, kind) {
|
||||
(0, _) => (0, vec![]),
|
||||
(0, _) => (0, &[][..]),
|
||||
(_, Fkp::Chpx) => {
|
||||
let cb = usize::from(*page.get(offset)?);
|
||||
(0, bytes_at(page, offset + 1, cb).filter(|_| offset + 1 + cb <= 511)?.to_vec())
|
||||
(0, bytes_at(page, offset + 1, cb).filter(|_| offset + 1 + cb <= 511)?)
|
||||
}
|
||||
(_, Fkp::Papx) => {
|
||||
// PapxInFkp: 2 × cb − 1 bytes, or (cb = 0) 2 × cb′ bytes after cb′.
|
||||
@@ -414,9 +426,11 @@ fn page_runs(page: &[u8], kind: Fkp, out: &mut Vec<Run>) -> Option<()> {
|
||||
cb => (offset + 1, 2 * cb - 1),
|
||||
};
|
||||
let grp = bytes_at(page, at, len).filter(|_| at + len <= 511 && len >= 2)?;
|
||||
(u16_at(grp, 0)?, grp.get(2..)?.to_vec())
|
||||
(u16_at(grp, 0)?, grp.get(2..)?)
|
||||
}
|
||||
};
|
||||
// rgfc "MUST be sorted in ascending order and MUST NOT contain duplicates": each run ends
|
||||
// where the next starts.
|
||||
if end <= start {
|
||||
return None;
|
||||
}
|
||||
@@ -473,10 +487,14 @@ struct Style {
|
||||
chpx: Vec<u8>,
|
||||
}
|
||||
|
||||
/// The style sheet: its styles by istd and its default font.
|
||||
/// The style sheet: each style's formatting with that of the styles it is based on, by istd, and
|
||||
/// its default font.
|
||||
#[derive(Default)]
|
||||
struct Sheet {
|
||||
styles: Vec<Option<Style>>,
|
||||
/// Paragraph styles: their paragraph formatting and their text's.
|
||||
paragraph: Vec<Option<(Para, Chars)>>,
|
||||
/// Character styles: what they do to the formatting of the text they are applied to.
|
||||
character: Vec<Option<CharsChange>>,
|
||||
/// `Stshif.ftcAsci`.
|
||||
font: Option<u16>,
|
||||
}
|
||||
@@ -500,23 +518,74 @@ impl Sheet {
|
||||
// LPStd records start on even offsets.
|
||||
at += 2 + usize::from(len) + usize::from(len) % 2;
|
||||
}
|
||||
Ok(Self { styles, font })
|
||||
let root = (Para::default(), Chars { font, ..Chars::default() });
|
||||
let paragraph = resolve(&styles, 1, root, |(mut para, chars), s| {
|
||||
para.apply(&s.papx);
|
||||
(para, chars.with(&CharsChange::of(&s.chpx)))
|
||||
})
|
||||
.ok_or_else(broken)?;
|
||||
let character = resolve(&styles, 2, CharsChange::default(), |change, s| change.then(&CharsChange::of(&s.chpx))).ok_or_else(broken)?;
|
||||
Ok(Self { paragraph, character, font })
|
||||
}
|
||||
|
||||
/// Style `istd` of kind `stk` and the styles it is based on, the first of the chain first.
|
||||
fn chain(&self, istd: u16, stk: u8) -> Vec<&Style> {
|
||||
let mut seen: Vec<u16> = vec![];
|
||||
let mut chain = vec![];
|
||||
let mut next = istd;
|
||||
while next != NO_STYLE && !seen.contains(&next) && seen.len() < MAX_BASED_ON {
|
||||
let Some(s) = self.styles.get(usize::from(next)).and_then(Option::as_ref).filter(|s| s.stk == stk) else { break };
|
||||
seen.push(next);
|
||||
chain.push(s);
|
||||
next = s.base;
|
||||
}
|
||||
chain.reverse();
|
||||
chain
|
||||
/// Paragraph style `istd`'s paragraph formatting and its text's; the document defaults when
|
||||
/// it is no paragraph style.
|
||||
fn paragraph_style(&self, istd: u16) -> (Para, Chars) {
|
||||
self.paragraph.get(usize::from(istd)).copied().flatten().unwrap_or((Para::default(), Chars { font: self.font, ..Chars::default() }))
|
||||
}
|
||||
|
||||
/// What character style `istd` does to text; nothing when it is no character style.
|
||||
fn character_style(&self, istd: u16) -> CharsChange {
|
||||
self.character.get(usize::from(istd)).copied().flatten().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The formatting of each style of `styles` of kind `stk` with that of the styles it is based on:
|
||||
/// `root` with `own` applied for each style of its chain of `istdBase`, the first of the chain
|
||||
/// first. Each style's formatting is worked out once, on that of the style it is based on. A chain
|
||||
/// ends at a style of another kind or none. None when `istdBase` makes a loop (which it "MUST
|
||||
/// NOT", StdfBase).
|
||||
fn resolve<T: Copy>(styles: &[Option<Style>], stk: u8, root: T, own: impl Fn(T, &Style) -> T) -> Option<Vec<Option<T>>> {
|
||||
let of_kind = |istd: usize| styles.get(istd).and_then(Option::as_ref).filter(|s| s.stk == stk);
|
||||
let mut done: Vec<Option<T>> = vec![None; styles.len()];
|
||||
let mut walking = vec![false; styles.len()];
|
||||
for istd in 0..styles.len() {
|
||||
// The styles from `istd` to the first one worked out, `istd` first.
|
||||
let mut walk: Vec<usize> = vec![];
|
||||
let mut value = root;
|
||||
let mut next = istd;
|
||||
while let Some(s) = of_kind(next) {
|
||||
if let Some(v) = done.get(next).copied().flatten() {
|
||||
value = v;
|
||||
break;
|
||||
}
|
||||
if walking.get(next).copied().unwrap_or_default() {
|
||||
return None;
|
||||
}
|
||||
walk.push(next);
|
||||
if let Some(w) = walking.get_mut(next) {
|
||||
*w = true;
|
||||
}
|
||||
if s.base == NO_STYLE {
|
||||
break;
|
||||
}
|
||||
next = usize::from(s.base);
|
||||
}
|
||||
for &i in &walk {
|
||||
if let Some(w) = walking.get_mut(i) {
|
||||
*w = false;
|
||||
}
|
||||
}
|
||||
for &i in walk.iter().rev() {
|
||||
if let Some(s) = of_kind(i) {
|
||||
value = own(value, s);
|
||||
}
|
||||
if let Some(slot) = done.get_mut(i) {
|
||||
*slot = Some(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(done)
|
||||
}
|
||||
|
||||
/// A style definition (STD, §2.9.258) whose Stdf takes `base_size` bytes; None when it is damaged.
|
||||
@@ -646,51 +715,154 @@ const ICO: [[u8; 3]; 16] = [
|
||||
];
|
||||
|
||||
impl Chars {
|
||||
/// The character properties of `grpprl` applied over this formatting; a toggle operand of
|
||||
/// 0x80 or 0x81 takes or reverses the value of `style`, the formatting its styles give.
|
||||
fn apply(&mut self, grpprl: &[u8], style: &Chars) {
|
||||
/// This formatting with `change` applied.
|
||||
fn with(self, change: &CharsChange) -> Chars {
|
||||
let c = change;
|
||||
Chars {
|
||||
font: c.font.or(self.font),
|
||||
half_points: c.half_points.unwrap_or(self.half_points),
|
||||
bold: c.bold.on(self.bold),
|
||||
italic: c.italic.on(self.italic),
|
||||
caps: c.caps.on(self.caps),
|
||||
small_caps: c.small_caps.on(self.small_caps),
|
||||
strike: c.strike.on(self.strike),
|
||||
dstrike: c.dstrike.on(self.dstrike),
|
||||
hidden: c.hidden.on(self.hidden),
|
||||
underline: c.underline.unwrap_or(self.underline),
|
||||
script: c.script.unwrap_or(self.script),
|
||||
shift: c.shift.unwrap_or(self.shift),
|
||||
color: c.color.unwrap_or(self.color),
|
||||
deleted: c.deleted.on(self.deleted),
|
||||
inserted: c.inserted.on(self.inserted),
|
||||
special: c.special.on(self.special),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What character properties do to a property that is on or off.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
enum Toggle {
|
||||
#[default]
|
||||
Keep,
|
||||
/// The reverse of the value the properties are applied over (toggle operand 0x81).
|
||||
Reverse,
|
||||
Set(bool),
|
||||
}
|
||||
|
||||
impl Toggle {
|
||||
/// The value this gives a property that was `value`.
|
||||
fn on(self, value: bool) -> bool {
|
||||
match self {
|
||||
Toggle::Keep => value,
|
||||
Toggle::Reverse => !value,
|
||||
Toggle::Set(v) => v,
|
||||
}
|
||||
}
|
||||
|
||||
/// This, then `later`.
|
||||
fn then(self, later: Toggle) -> Toggle {
|
||||
match (self, later) {
|
||||
(t, Toggle::Keep) => t,
|
||||
(_, Toggle::Set(v)) => Toggle::Set(v),
|
||||
(Toggle::Keep, Toggle::Reverse) => Toggle::Reverse,
|
||||
(Toggle::Reverse, Toggle::Reverse) => Toggle::Keep,
|
||||
(Toggle::Set(v), Toggle::Reverse) => Toggle::Set(!v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a list of character properties does to the formatting it is applied over: each property
|
||||
/// it sets (None keeps the property as it was).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
struct CharsChange {
|
||||
font: Option<u16>,
|
||||
half_points: Option<u16>,
|
||||
bold: Toggle,
|
||||
italic: Toggle,
|
||||
caps: Toggle,
|
||||
small_caps: Toggle,
|
||||
strike: Toggle,
|
||||
dstrike: Toggle,
|
||||
hidden: Toggle,
|
||||
underline: Option<bool>,
|
||||
script: Option<Script>,
|
||||
shift: Option<i16>,
|
||||
color: Option<Option<[u8; 3]>>,
|
||||
deleted: Toggle,
|
||||
inserted: Toggle,
|
||||
special: Toggle,
|
||||
}
|
||||
|
||||
impl CharsChange {
|
||||
/// What the character properties of `grpprl` do; a toggle operand of 0x80 or 0x81 takes or
|
||||
/// reverses the value of the formatting they are applied over.
|
||||
fn of(grpprl: &[u8]) -> Self {
|
||||
let mut c = Self::default();
|
||||
each_prl(grpprl, |sprm, op| {
|
||||
let byte = op.first().copied();
|
||||
let word = u16_at(op, 0);
|
||||
let toggle = |state: &mut bool, of_style: bool| match byte {
|
||||
Some(0) => *state = false,
|
||||
Some(1) => *state = true,
|
||||
Some(0x80) => *state = of_style,
|
||||
Some(0x81) => *state = !of_style,
|
||||
let toggle = |state: &mut Toggle| match byte {
|
||||
Some(0) => *state = Toggle::Set(false),
|
||||
Some(1) => *state = Toggle::Set(true),
|
||||
Some(0x80) => *state = Toggle::Keep,
|
||||
Some(0x81) => *state = Toggle::Reverse,
|
||||
_ => {}
|
||||
};
|
||||
match sprm {
|
||||
0x0800 => toggle(&mut self.deleted, style.deleted),
|
||||
0x0801 => toggle(&mut self.inserted, style.inserted),
|
||||
0x0835 => toggle(&mut self.bold, style.bold),
|
||||
0x0836 => toggle(&mut self.italic, style.italic),
|
||||
0x0837 => toggle(&mut self.strike, style.strike),
|
||||
0x083A => toggle(&mut self.small_caps, style.small_caps),
|
||||
0x083B => toggle(&mut self.caps, style.caps),
|
||||
0x083C => toggle(&mut self.hidden, style.hidden),
|
||||
0x0855 => toggle(&mut self.special, style.special),
|
||||
0x2A53 => toggle(&mut self.dstrike, style.dstrike),
|
||||
0x2A3E => self.underline = byte.is_some_and(|k| k != 0),
|
||||
0x2A42 => self.color = byte.and_then(|i| usize::from(i).checked_sub(1)).and_then(|i| ICO.get(i)).copied(),
|
||||
0x4A43 => self.half_points = word.filter(|v| (2..=3276).contains(v)).unwrap_or(self.half_points),
|
||||
0x4845 => self.shift = word.map_or(self.shift, |v| v as i16),
|
||||
0x0800 => toggle(&mut c.deleted),
|
||||
0x0801 => toggle(&mut c.inserted),
|
||||
0x0835 => toggle(&mut c.bold),
|
||||
0x0836 => toggle(&mut c.italic),
|
||||
0x0837 => toggle(&mut c.strike),
|
||||
0x083A => toggle(&mut c.small_caps),
|
||||
0x083B => toggle(&mut c.caps),
|
||||
0x083C => toggle(&mut c.hidden),
|
||||
0x0855 => toggle(&mut c.special),
|
||||
0x2A53 => toggle(&mut c.dstrike),
|
||||
0x2A3E => c.underline = Some(byte.is_some_and(|k| k != 0)),
|
||||
0x2A42 => c.color = Some(byte.and_then(|i| usize::from(i).checked_sub(1)).and_then(|i| ICO.get(i)).copied()),
|
||||
0x4A43 => c.half_points = word.filter(|v| (2..=3276).contains(v)).or(c.half_points),
|
||||
0x4845 => c.shift = word.map(|v| v as i16).or(c.shift),
|
||||
0x2A48 => {
|
||||
self.script = match byte {
|
||||
c.script = Some(match byte {
|
||||
Some(1) => Script::Super,
|
||||
Some(2) => Script::Sub,
|
||||
_ => Script::Normal,
|
||||
}
|
||||
})
|
||||
}
|
||||
0x4A4F => self.font = word.or(self.font),
|
||||
0x4A4F => c.font = word.or(c.font),
|
||||
// COLORREF: red, green, blue, then 0xFF for the automatic color.
|
||||
0x6870 => {
|
||||
if let [r, g, b, auto] = *op {
|
||||
self.color = (auto != 0xFF).then_some([r, g, b]);
|
||||
c.color = Some((auto != 0xFF).then_some([r, g, b]));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
c
|
||||
}
|
||||
|
||||
/// This, then `later`.
|
||||
fn then(&self, later: &CharsChange) -> CharsChange {
|
||||
CharsChange {
|
||||
font: later.font.or(self.font),
|
||||
half_points: later.half_points.or(self.half_points),
|
||||
bold: self.bold.then(later.bold),
|
||||
italic: self.italic.then(later.italic),
|
||||
caps: self.caps.then(later.caps),
|
||||
small_caps: self.small_caps.then(later.small_caps),
|
||||
strike: self.strike.then(later.strike),
|
||||
dstrike: self.dstrike.then(later.dstrike),
|
||||
hidden: self.hidden.then(later.hidden),
|
||||
underline: later.underline.or(self.underline),
|
||||
script: later.script.or(self.script),
|
||||
shift: later.shift.or(self.shift),
|
||||
color: later.color.or(self.color),
|
||||
deleted: self.deleted.then(later.deleted),
|
||||
inserted: self.inserted.then(later.inserted),
|
||||
special: self.special.then(later.special),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,6 +1037,9 @@ impl<'a> Reader<'a> {
|
||||
/// The text `text`, with the character properties of `chpx` and the paragraph properties of
|
||||
/// `papx`.
|
||||
fn read(&mut self, text: &[Char], chpx: &[Run], papx: &[Run]) {
|
||||
// Each property run's formatting, worked out the first time the text reaches it.
|
||||
let mut paragraphs: Vec<Option<(Para, Chars)>> = vec![None; papx.len()];
|
||||
let mut changes: Vec<Option<CharsChange>> = vec![None; chpx.len()];
|
||||
let mut start = 0;
|
||||
while start < text.len() && !self.full {
|
||||
// A paragraph runs to its mark: a paragraph or cell mark, or a page or section break at
|
||||
@@ -876,14 +1051,21 @@ impl<'a> Reader<'a> {
|
||||
let end = text.get(start..).and_then(|rest| rest.iter().position(ends)).map_or(text.len(), |at| start + at + 1);
|
||||
let segment = text.get(start..end).unwrap_or_default();
|
||||
let mark_fc = segment.last().map(|&(_, fc, _)| fc).unwrap_or_default();
|
||||
let (para, base) = self.paragraph(run_at(papx, mark_fc).and_then(|i| papx.get(i)));
|
||||
let run = run_at(papx, mark_fc).and_then(|i| Some((papx.get(i)?, paragraphs.get_mut(i)?)));
|
||||
let (para, base) = match run {
|
||||
Some((r, slot)) => *slot.get_or_insert_with(|| self.paragraph(Some(r))),
|
||||
None => self.paragraph(None),
|
||||
};
|
||||
self.para = para;
|
||||
let mut cached: Option<(Option<usize>, Chars)> = None;
|
||||
for (k, ch) in segment.iter().enumerate() {
|
||||
let run = run_at(chpx, ch.1);
|
||||
let chars = match cached {
|
||||
Some((r, chars)) if r == run => chars,
|
||||
_ => self.chars(base, run.and_then(|i| chpx.get(i)).map_or(&[][..], |r| r.grpprl.as_slice())),
|
||||
_ => match run.and_then(|i| Some((chpx.get(i)?, changes.get_mut(i)?))) {
|
||||
Some((r, slot)) => base.with(slot.get_or_insert_with(|| self.change(r.grpprl))),
|
||||
None => base,
|
||||
},
|
||||
};
|
||||
cached = Some((run, chars));
|
||||
self.character(ch.0, chars, k + 1 == segment.len() && ends(ch));
|
||||
@@ -895,37 +1077,25 @@ impl<'a> Reader<'a> {
|
||||
/// A paragraph's formatting and its text's formatting before character styles and direct
|
||||
/// formatting, from its properties `run`.
|
||||
fn paragraph(&self, run: Option<&Run>) -> (Para, Chars) {
|
||||
let mut para = Para::default();
|
||||
let mut chars = Chars { font: self.sheet.font, ..Chars::default() };
|
||||
for s in self.sheet.chain(run.map_or(0, |r| r.istd), 1) {
|
||||
para.apply(&s.papx);
|
||||
let style = chars;
|
||||
chars.apply(&s.chpx, &style);
|
||||
}
|
||||
let (mut para, chars) = self.sheet.paragraph_style(run.map_or(0, |r| r.istd));
|
||||
if let Some(r) = run {
|
||||
para.apply(&r.grpprl);
|
||||
para.apply(r.grpprl);
|
||||
}
|
||||
(para, chars)
|
||||
}
|
||||
|
||||
/// The formatting of text whose paragraph gives `base` and whose properties are `grpprl`: its
|
||||
/// character style (`sprmCIstd`), then its direct formatting.
|
||||
fn chars(&self, base: Chars, grpprl: &[u8]) -> Chars {
|
||||
let mut style = base;
|
||||
/// What the character properties `grpprl` do to the formatting their paragraph gives: their
|
||||
/// character style (`sprmCIstd`), then their direct formatting.
|
||||
fn change(&self, grpprl: &[u8]) -> CharsChange {
|
||||
let mut style = CharsChange::default();
|
||||
each_prl(grpprl, |sprm, op| {
|
||||
if sprm == 0x4A30
|
||||
&& let Some(istd) = u16_at(op, 0)
|
||||
{
|
||||
style = base;
|
||||
for s in self.sheet.chain(istd, 2) {
|
||||
let before = style;
|
||||
style.apply(&s.chpx, &before);
|
||||
}
|
||||
style = self.sheet.character_style(istd);
|
||||
}
|
||||
});
|
||||
let mut chars = style;
|
||||
chars.apply(grpprl, &style);
|
||||
chars
|
||||
style.then(&CharsChange::of(grpprl))
|
||||
}
|
||||
|
||||
/// Character `c` of the main text in formatting `chars`; `mark`: it ends its paragraph's
|
||||
|
||||
@@ -503,6 +503,8 @@ struct Word97 {
|
||||
ccp_extra: u32,
|
||||
/// A Clx to write instead of the pieces'.
|
||||
clx: Option<Vec<u8>>,
|
||||
/// Changes the character properties' bin table: its file positions, then its page numbers.
|
||||
chpx_bin: Option<fn(&mut Vec<u32>)>,
|
||||
}
|
||||
|
||||
/// One 512-byte property page of `runs` (file positions and properties), written to `wd`; its
|
||||
@@ -578,7 +580,8 @@ impl Word97 {
|
||||
_ => runs.push((fc, fc + w, key, props)),
|
||||
}
|
||||
}
|
||||
// Pages of contiguous runs, 16 at most.
|
||||
// Pages of contiguous runs, 16 at most, in file order.
|
||||
runs.sort_by_key(|r| r.0);
|
||||
let mut groups: Vec<Vec<(u32, u32, Vec<u8>)>> = vec![];
|
||||
for (start, end, _, props) in runs {
|
||||
match groups.last_mut() {
|
||||
@@ -589,7 +592,11 @@ impl Word97 {
|
||||
let mut fcs: Vec<u32> = groups.iter().map(|g| g[0].0).collect();
|
||||
fcs.extend(groups.last().map(|g| g.last().unwrap().1));
|
||||
let pns: Vec<u32> = groups.iter().map(|g| property_page(g, papx, &mut wd)).collect();
|
||||
bins.push([fcs, pns].concat().into_iter().flat_map(u32::to_le_bytes).collect::<Vec<u8>>());
|
||||
let mut bin = [fcs, pns].concat();
|
||||
if let Some(edit) = self.chpx_bin.filter(|_| !papx) {
|
||||
edit(&mut bin);
|
||||
}
|
||||
bins.push(bin.into_iter().flat_map(u32::to_le_bytes).collect::<Vec<u8>>());
|
||||
}
|
||||
let mut cps = vec![0u32];
|
||||
for (text, _) in &self.pieces {
|
||||
@@ -835,11 +842,66 @@ fn malformed_doc_fails_cleanly() {
|
||||
let r = place(&mut s, "a.doc", &Word97 { pieces: vec![("a\rb\u{13} PAGE c\r", true)], ..Word97::default() }.build()).unwrap();
|
||||
assert_eq!(placed(&s, &r).0, "a\nb");
|
||||
assert_eq!(r["warnings"], json!(["the Word document's fields without a result that could be read were left out"]));
|
||||
// A bin table whose file positions don't rise, and one that lists a property page twice.
|
||||
let e = place(&mut s, "a.doc", &Word97 { chpx_bin: Some(|b| b.swap(0, 1)), ..words() }.build()).unwrap_err();
|
||||
assert!(e.contains("properties are damaged"), "{e}");
|
||||
let twice = |b: &mut Vec<u32>| *b = vec![b[0], b[1], b[1] + 1, b[2], b[2]];
|
||||
let e = place(&mut s, "a.doc", &Word97 { chpx_bin: Some(twice), ..words() }.build()).unwrap_err();
|
||||
assert!(e.contains("properties are damaged"), "{e}");
|
||||
// Styles based on each other.
|
||||
let styles = vec![Some((1, 1, vec![], vec![])), Some((1, 0, vec![], vec![]))];
|
||||
let e = place(&mut s, "a.doc", &Word97 { styles, ..words() }.build()).unwrap_err();
|
||||
assert!(e.contains("style sheet is damaged"), "{e}");
|
||||
let r = place(&mut s, "a.doc", &words().build()).unwrap();
|
||||
assert_eq!(placed(&s, &r).0, "words");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), 5, "failed places leave no undo step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_styles_are_worked_out_once_however_many_paragraphs_use_them() {
|
||||
// Two chains of 32 styles, each with 21,000 character properties: paragraph styles 0 to 31
|
||||
// (the first 12 pt) and character styles 32 to 63 (the first italic).
|
||||
let filler = prls(&[(0x0835, &[0])]).repeat(21_000);
|
||||
let mut styles = vec![];
|
||||
for (stk, first, size) in [(1, 0u16, (0x4A43, &[24u8, 0][..])), (2, 32, (0x0836, &[1][..]))] {
|
||||
for i in first..first + 32 {
|
||||
let mut chpx = filler.clone();
|
||||
if i == first {
|
||||
chpx.extend(prls(&[size]));
|
||||
}
|
||||
styles.push(Some((stk, if i == first { 0x0FFF } else { i - 1 }, vec![], chpx)));
|
||||
}
|
||||
}
|
||||
// 20,000 hidden paragraphs in the last paragraph style with the last character style, then one
|
||||
// shown.
|
||||
let text: &'static str = format!("{}x\r", "\r".repeat(20_000)).leak();
|
||||
let doc = Word97 {
|
||||
pieces: vec![(text, true)],
|
||||
chpx: vec![(0, 20_000, prls(&[(0x4A30, &[63, 0]), (0x083C, &[1])])), (20_000, 20_002, prls(&[(0x4A30, &[63, 0])]))],
|
||||
papx: vec![(20_001, 31, vec![])],
|
||||
styles,
|
||||
..Word97::default()
|
||||
}
|
||||
.build();
|
||||
let mut s = session();
|
||||
let started = std::time::Instant::now();
|
||||
let r = s.execute("file.place", &json!({"name": "a.doc", "dataBase64": vectorcraft_format::base64_encode(&doc)})).unwrap();
|
||||
let took = started.elapsed();
|
||||
let n = s.doc().unwrap().doc.node(NodeId(r["ids"][0].as_u64().unwrap())).unwrap().clone();
|
||||
let NodeKind::Text(t) = &n.kind else { panic!("not type") };
|
||||
assert_eq!((t.runs.len(), t.runs[0].text.as_str(), t.runs[0].style.font_style.as_str(), t.runs[0].style.size), (1, "x", "Italic", 12.0));
|
||||
assert!(took < std::time::Duration::from_secs(10), "placing took {took:?}");
|
||||
// A chain of 34 paragraph styles, each based on the next; the last is 12 pt. Styles 31 and 0
|
||||
// both take its size.
|
||||
let mut styles: Vec<_> = (0..33u16).map(|i| Some((1, i + 1, vec![], vec![]))).collect();
|
||||
styles.push(Some((1, 0x0FFF, vec![], prls(&[(0x4A43, &[24, 0])]))));
|
||||
let doc = Word97 { pieces: vec![("a\rb\r", true)], papx: vec![(1, 31, vec![]), (3, 0, vec![])], styles, ..Word97::default() }.build();
|
||||
let r = s.execute("file.place", &json!({"name": "a.doc", "dataBase64": vectorcraft_format::base64_encode(&doc)})).unwrap();
|
||||
let n = s.doc().unwrap().doc.node(NodeId(r["ids"][0].as_u64().unwrap())).unwrap().clone();
|
||||
let NodeKind::Text(t) = &n.kind else { panic!("not type") };
|
||||
assert_eq!(t.runs.iter().map(|r| (r.text.as_str(), r.style.size)).collect::<Vec<_>>(), [("a\nb", 12.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn place_reads_what_open_reads_and_text_files() {
|
||||
use cmd::fileio::{DOC_EXTS, DOCX_EXTS, OPEN_EXTS, PLACE_EXTS, RTF_EXTS, TEXT_EXTS};
|
||||
|
||||
+3
-2
@@ -3900,8 +3900,9 @@ are left out, and `warnings` names each kind of content left out or changed once
|
||||
tracked changes, tables, and the other stories the document has); a field with no result is left out with its own
|
||||
warning. Documents saved by Word 6.0 or Word 95, encrypted or password-protected documents and documents last saved with
|
||||
fast save are refused, and so are a stream larger than 64 MiB, a document with more than 4 MiB of text, 100,000 pieces,
|
||||
16,384 pages of properties or 100,000 paragraphs or runs, one whose 16-bit text holds half of a surrogate pair, and one
|
||||
whose structures lie outside their streams.
|
||||
16,384 pages of properties or 100,000 paragraphs or runs, one whose 16-bit text holds half of a surrogate pair, one
|
||||
whose structures lie outside their streams, one whose property pages are out of order, listed twice or overlap, and one
|
||||
whose styles are based on each other in a loop.
|
||||
`file.place` answers `format: "doc"`.
|
||||
|
||||
## PDF presets
|
||||
|
||||
Reference in New Issue
Block a user