bare-x-to-pdf 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -131,10 +131,14 @@ Documents are often untrusted, so the engine caps what a small file can make it
131
131
  lay out. A document past a hard limit is rejected with an error. One past a soft limit still
132
132
  converts, with the value clamped.
133
133
 
134
- Rejected:
134
+ Rejected, with `err.code` set to `'LIMIT'` so an app can tell these apart from a damaged
135
+ file:
135
136
 
136
137
  - An archive part larger than 128 MB uncompressed.
137
138
  - XML nested more than 512 elements deep.
139
+ - An XML part with more than 1,000,000 elements.
140
+ - A document whose content needs more than 64 MB of Typst source to lay out, about 8,000
141
+ pages of dense text.
138
142
  - A spreadsheet that would print more than 30,000 cells.
139
143
 
140
144
  Clamped:
@@ -152,6 +156,19 @@ Clamped:
152
156
 
153
157
  Spreadsheet pages with no content are not printed, as in LibreOffice Calc.
154
158
 
159
+ ## Long documents
160
+
161
+ Typst keeps every page in memory until a document is laid out, about 1 MB per page of dense
162
+ text. A document whose content needs more than 2 MB of Typst source, about 270 pages, is
163
+ laid out in chunks of that size instead, each ending where a page starts with a new
164
+ paragraph or table, and the chunks are joined into one PDF. Pages, page numbers, totals,
165
+ headers, footers and footnote numbers come out as in a single pass, and memory stays near
166
+ what one chunk needs: a 5,900-page document peaks at about 510 MB instead of 6 GB.
167
+
168
+ A chunked PDF is not tagged, since the tags of separate chunks cannot be joined, so it
169
+ carries no structure for screen readers. Presentations with speaker notes are always laid
170
+ out in a single pass.
171
+
155
172
  ## Development
156
173
 
157
174
  The platform packages in `npm/` are npm workspaces, and each declares its own `os` and `cpu`
package/binding.c CHANGED
@@ -196,8 +196,14 @@ simple_converter__error(js_env_t *env, simple_converter_job_t *job) {
196
196
  }
197
197
  assert(err == 0);
198
198
 
199
+ js_value_t *code = NULL;
200
+ if (job->status == SC_STATUS_LIMIT) {
201
+ err = js_create_string_utf8(env, (utf8_t *) "LIMIT", (size_t) -1, &code);
202
+ assert(err == 0);
203
+ }
204
+
199
205
  js_value_t *error;
200
- err = js_create_error(env, NULL, message, &error);
206
+ err = js_create_error(env, code, message, &error);
201
207
  assert(err == 0);
202
208
 
203
209
  return error;
@@ -8,6 +8,14 @@
8
8
  extern "C" {
9
9
  #endif
10
10
 
11
+ enum {
12
+ SC_STATUS_OK = 0,
13
+ SC_STATUS_ERROR = 1,
14
+ SC_STATUS_PANIC = 2,
15
+ SC_STATUS_INVALID = 3,
16
+ SC_STATUS_LIMIT = 4,
17
+ };
18
+
11
19
  typedef struct {
12
20
  uint8_t *data;
13
21
  size_t len;
package/core/src/error.rs CHANGED
@@ -1,21 +1,38 @@
1
1
  use std::fmt;
2
2
 
3
3
  #[derive(Debug, Clone)]
4
- pub struct Error(String);
4
+ pub struct Error {
5
+ message: String,
6
+ limit: bool,
7
+ }
5
8
 
6
9
  impl Error {
7
10
  pub fn new(message: impl Into<String>) -> Self {
8
- Error(message.into())
11
+ Error {
12
+ message: message.into(),
13
+ limit: false,
14
+ }
15
+ }
16
+
17
+ pub fn limit(message: impl Into<String>) -> Self {
18
+ Error {
19
+ message: message.into(),
20
+ limit: true,
21
+ }
9
22
  }
10
23
 
11
24
  pub fn message(&self) -> &str {
12
- &self.0
25
+ &self.message
26
+ }
27
+
28
+ pub fn is_limit(&self) -> bool {
29
+ self.limit
13
30
  }
14
31
  }
15
32
 
16
33
  impl fmt::Display for Error {
17
34
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18
- f.write_str(&self.0)
35
+ f.write_str(&self.message)
19
36
  }
20
37
  }
21
38
 
@@ -23,18 +40,18 @@ impl std::error::Error for Error {}
23
40
 
24
41
  impl From<std::io::Error> for Error {
25
42
  fn from(e: std::io::Error) -> Self {
26
- Error(format!("io: {e}"))
43
+ Error::new(format!("io: {e}"))
27
44
  }
28
45
  }
29
46
 
30
47
  impl From<zip::result::ZipError> for Error {
31
48
  fn from(e: zip::result::ZipError) -> Self {
32
- Error(format!("zip: {e}"))
49
+ Error::new(format!("zip: {e}"))
33
50
  }
34
51
  }
35
52
 
36
53
  impl From<quick_xml::Error> for Error {
37
54
  fn from(e: quick_xml::Error) -> Self {
38
- Error(format!("xml: {e}"))
55
+ Error::new(format!("xml: {e}"))
39
56
  }
40
57
  }
package/core/src/lib.rs CHANGED
@@ -157,7 +157,11 @@ pub unsafe extern "C" fn sc_convert(
157
157
  }
158
158
  Ok(Err(e)) => {
159
159
  *error = Buffer::from_vec(e.message().as_bytes().to_vec());
160
- 1
160
+ if e.is_limit() {
161
+ 4
162
+ } else {
163
+ 1
164
+ }
161
165
  }
162
166
  Err(payload) => {
163
167
  let message = payload
@@ -2,6 +2,7 @@ use crate::error::Error;
2
2
  use std::io::Read;
3
3
 
4
4
  pub const MAX_XML_DEPTH: usize = 512;
5
+ pub const MAX_XML_ELEMENTS: usize = 1_000_000;
5
6
  pub const MAX_FORMULA_DEPTH: usize = 256;
6
7
  pub const MAX_ZIP_ENTRY_BYTES: u64 = 128 * 1024 * 1024;
7
8
  pub const MAX_TABLE_SPAN: usize = 1000;
@@ -19,6 +20,19 @@ pub const MAX_SHEET_CELLS: u64 = 30_000;
19
20
  pub const MIN_PAGE_SIDE: f64 = 72.0;
20
21
  pub const MAX_PAGE_SIDE: f64 = 14_400.0;
21
22
  pub const MAX_LENGTH: f64 = 1_000_000.0;
23
+ pub const MAX_LAYOUT_SOURCE_BYTES: usize = 64 * 1024 * 1024;
24
+
25
+ pub fn check_layout_source(bytes: usize) -> Result<(), Error> {
26
+ if bytes > MAX_LAYOUT_SOURCE_BYTES {
27
+ let mb = |n: usize| n.div_ceil(1024 * 1024);
28
+ return Err(Error::limit(format!(
29
+ "document is too large to convert: its content needs {} MB of layout source, more than the {} MB limit",
30
+ mb(bytes),
31
+ mb(MAX_LAYOUT_SOURCE_BYTES)
32
+ )));
33
+ }
34
+ Ok(())
35
+ }
22
36
 
23
37
  pub fn span(value: usize) -> usize {
24
38
  value.clamp(1, MAX_TABLE_SPAN)
@@ -36,7 +50,7 @@ pub fn read_entry(reader: impl Read) -> Result<Vec<u8>, Error> {
36
50
  let mut data = Vec::new();
37
51
  reader.take(MAX_ZIP_ENTRY_BYTES + 1).read_to_end(&mut data)?;
38
52
  if data.len() as u64 > MAX_ZIP_ENTRY_BYTES {
39
- return Err(Error::new(format!("a part of the document is larger than {} MB uncompressed", MAX_ZIP_ENTRY_BYTES / (1024 * 1024))));
53
+ return Err(Error::limit(format!("a part of the document is larger than {} MB uncompressed", MAX_ZIP_ENTRY_BYTES / (1024 * 1024))));
40
54
  }
41
55
  Ok(data)
42
56
  }
@@ -0,0 +1,275 @@
1
+ use super::emit::{self, Chunk};
2
+ use super::fonts::FontSet;
3
+ use super::{export, layout, merge};
4
+ use crate::error::Error;
5
+ use crate::model::Document;
6
+ use std::num::NonZeroUsize;
7
+ use std::ops::Range;
8
+ use typst::foundations::{Label, NativeElement, Selector, Value};
9
+ use typst::introspection::{Introspector, MetadataElem};
10
+ use typst::layout::PageRanges;
11
+ use typst::model::FootnoteElem;
12
+ use typst::utils::PicoStr;
13
+ use typst_layout::PagedDocument;
14
+ use typst_pdf::PdfOptions;
15
+
16
+ pub const CHUNK_SOURCE_BYTES: usize = 2 * 1024 * 1024;
17
+ const TOP_TOLERANCE: f64 = 0.5;
18
+
19
+ type Cursor = (usize, usize);
20
+
21
+ type Sink<'a> = dyn FnMut(&PagedDocument, usize) -> Result<(), Error> + 'a;
22
+
23
+ pub fn render(doc: &Document, fonts: &FontSet, sizes: &[Vec<usize>], uses_total_pages: bool) -> Result<Vec<u8>, Error> {
24
+ render_in_chunks(doc, fonts, sizes, uses_total_pages, CHUNK_SOURCE_BYTES)
25
+ }
26
+
27
+ fn render_in_chunks(doc: &Document, fonts: &FontSet, sizes: &[Vec<usize>], uses_total_pages: bool, budget: usize) -> Result<Vec<u8>, Error> {
28
+ let mut pdfs = Vec::new();
29
+ paginate(doc, fonts, sizes, uses_total_pages, budget, &mut |paged, keep| {
30
+ let options = PdfOptions {
31
+ tagged: false,
32
+ page_ranges: Some(PageRanges::new(vec![NonZeroUsize::new(1)..=NonZeroUsize::new(keep)])),
33
+ ..PdfOptions::default()
34
+ };
35
+ pdfs.push(export(paged, &options)?);
36
+ Ok(())
37
+ })?;
38
+ merge::merge(&pdfs)
39
+ }
40
+
41
+ fn paginate(doc: &Document, fonts: &FontSet, sizes: &[Vec<usize>], uses_total_pages: bool, budget: usize, sink: &mut Sink) -> Result<(), Error> {
42
+ let total_pages = if uses_total_pages {
43
+ Some(run(doc, fonts, sizes, None, budget, &mut |_, _| Ok(()))?)
44
+ } else {
45
+ None
46
+ };
47
+ run(doc, fonts, sizes, total_pages, budget, sink)?;
48
+ Ok(())
49
+ }
50
+
51
+ fn run(doc: &Document, fonts: &FontSet, sizes: &[Vec<usize>], total_pages: Option<u64>, budget: usize, sink: &mut Sink) -> Result<u64, Error> {
52
+ let mut last_number = 0;
53
+ let mut chunk = Chunk {
54
+ total_pages,
55
+ ..Chunk::default()
56
+ };
57
+ let mut cursor: Cursor = (0, 0);
58
+ while cursor.0 < sizes.len() {
59
+ let mut span = budget;
60
+ let (paged, keep, resume) = loop {
61
+ let (parts, next) = take(sizes, cursor, span);
62
+ chunk.parts = parts;
63
+ let paged = layout(emit::emit_chunk(doc, fonts, &chunk), fonts.clone())?;
64
+ let count = paged.pages().len();
65
+ if next.0 >= sizes.len() {
66
+ break (paged, count, next);
67
+ }
68
+ if let Some((keep, resume)) = find_cut(&paged, doc, cursor) {
69
+ break (paged, keep, resume);
70
+ }
71
+ drop(paged);
72
+ typst::comemo::evict(0);
73
+ span = span.saturating_mul(2);
74
+ };
75
+ if keep > 0 {
76
+ sink(&paged, keep)?;
77
+ last_number = paged.pages()[keep - 1].number;
78
+ chunk.page_number = Some(last_number + 1);
79
+ }
80
+ chunk.footnotes += footnotes_on(&paged, keep);
81
+ chunk.page_offset += keep;
82
+ chunk.resumed = true;
83
+ drop(paged);
84
+ typst::comemo::evict(0);
85
+ cursor = resume;
86
+ }
87
+ Ok(last_number)
88
+ }
89
+
90
+ fn take(sizes: &[Vec<usize>], cursor: Cursor, budget: usize) -> (Vec<(usize, Range<usize>)>, Cursor) {
91
+ let (mut section, mut block) = cursor;
92
+ let mut parts = Vec::new();
93
+ let mut used = 0;
94
+ while section < sizes.len() {
95
+ let start = block;
96
+ let blocks = &sizes[section];
97
+ while block < blocks.len() && (used < budget || (parts.is_empty() && block == start)) {
98
+ used += blocks[block];
99
+ block += 1;
100
+ }
101
+ parts.push((section, start..block));
102
+ if block < blocks.len() {
103
+ return (parts, (section, block));
104
+ }
105
+ section += 1;
106
+ block = 0;
107
+ if used >= budget {
108
+ break;
109
+ }
110
+ }
111
+ (parts, (section, 0))
112
+ }
113
+
114
+ fn find_cut(paged: &PagedDocument, doc: &Document, start: Cursor) -> Option<(usize, Cursor)> {
115
+ let label = Label::new(PicoStr::intern(emit::CUT_LABEL))?;
116
+ let introspector = paged.introspector();
117
+ let mut best: Option<(usize, Cursor)> = None;
118
+ for content in introspector.query(&Selector::Label(label)) {
119
+ let Some(meta) = content.to_packed::<MetadataElem>() else {
120
+ continue;
121
+ };
122
+ let Value::Int(id) = meta.value else {
123
+ continue;
124
+ };
125
+ let at = emit::cut_position(id);
126
+ if at <= start {
127
+ continue;
128
+ }
129
+ let Some(position) = content.location().and_then(|l| introspector.position(l)) else {
130
+ continue;
131
+ };
132
+ let page = position.page.get();
133
+ let Some(section) = doc.sections.get(at.0) else {
134
+ continue;
135
+ };
136
+ let scaled = section.content_scale > 0.0 && (section.content_scale - 1.0).abs() > 0.001;
137
+ if page < 2 || section.columns > 1 || scaled {
138
+ continue;
139
+ }
140
+ if (position.point.y.to_pt() - section.page.margin.top).abs() > TOP_TOLERANCE {
141
+ continue;
142
+ }
143
+ let better = match best {
144
+ None => true,
145
+ Some((best_page, best_at)) => page > best_page || (page == best_page && at < best_at),
146
+ };
147
+ if better {
148
+ best = Some((page, at));
149
+ }
150
+ }
151
+ best.map(|(page, at)| (page - 1, at))
152
+ }
153
+
154
+ fn footnotes_on(paged: &PagedDocument, pages: usize) -> usize {
155
+ let introspector = paged.introspector();
156
+ introspector
157
+ .query(&Selector::Elem(FootnoteElem::ELEM, None))
158
+ .iter()
159
+ .filter(|content| {
160
+ content
161
+ .location()
162
+ .and_then(|l| introspector.position(l))
163
+ .is_some_and(|p| p.page.get() <= pages)
164
+ })
165
+ .count()
166
+ }
167
+
168
+ #[cfg(test)]
169
+ mod tests {
170
+ use super::*;
171
+ use std::fmt::Write;
172
+ use std::path::Path;
173
+ use typst::layout::{Frame, FrameItem};
174
+
175
+ const BUDGET: usize = 16 * 1024;
176
+
177
+ fn fixture(name: &str) -> (Document, FontSet) {
178
+ let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
179
+ let bytes = std::fs::read(root.join("test/fixtures").join(name)).unwrap();
180
+ let mut doc = crate::docx::read(&bytes).unwrap();
181
+ super::super::sanitize_pages(&mut doc);
182
+ (doc, FontSet::load(&root.join("fonts")).unwrap())
183
+ }
184
+
185
+ fn page_text(page: &typst_layout::Page) -> String {
186
+ fn walk(frame: &Frame, out: &mut String) {
187
+ for (point, item) in frame.items() {
188
+ match item {
189
+ FrameItem::Group(group) => walk(&group.frame, out),
190
+ FrameItem::Text(text) => {
191
+ let _ = write!(out, "{:.2},{:.2} {}|", point.x.to_pt(), point.y.to_pt(), text.text);
192
+ }
193
+ _ => {}
194
+ }
195
+ }
196
+ }
197
+ let mut out = format!("#{} ", page.number);
198
+ walk(&page.frame, &mut out);
199
+ out
200
+ }
201
+
202
+ fn chunked_pages(name: &str) -> (Vec<String>, Vec<Vec<String>>) {
203
+ let (doc, fonts) = fixture(name);
204
+ let full = emit::emit(&doc, &fonts);
205
+ let sizes = full.block_sizes.clone();
206
+ let uses_total_pages = full.uses_total_pages;
207
+ let single = layout(full, fonts.clone()).unwrap().pages().iter().map(page_text).collect();
208
+ let mut chunks = Vec::new();
209
+ paginate(&doc, &fonts, &sizes, uses_total_pages, BUDGET, &mut |paged, keep| {
210
+ chunks.push(paged.pages()[..keep].iter().map(page_text).collect());
211
+ Ok(())
212
+ })
213
+ .unwrap();
214
+ (single, chunks)
215
+ }
216
+
217
+ fn assert_same_pages(single: &[String], chunks: &[Vec<String>]) {
218
+ let chunked: Vec<&String> = chunks.iter().flatten().collect();
219
+ assert_eq!(chunked.len(), single.len());
220
+ for (index, (a, b)) in chunked.iter().zip(single).enumerate() {
221
+ assert_eq!(*a, b, "page {}", index + 1);
222
+ }
223
+ }
224
+
225
+ #[test]
226
+ fn chunked_layout_matches_a_single_pass() {
227
+ let (doc, fonts) = fixture("chunked-sections.docx");
228
+ assert!(emit::emit(&doc, &fonts).uses_total_pages);
229
+ let (single, chunks) = chunked_pages("chunked-sections.docx");
230
+ assert!(chunks.len() >= 8, "only {} chunks", chunks.len());
231
+ assert_same_pages(&single, &chunks);
232
+ }
233
+
234
+ #[test]
235
+ fn chunks_never_end_inside_columns_or_a_table() {
236
+ let (single, chunks) = chunked_pages("chunked-no-cut.docx");
237
+ assert!(chunks.len() >= 3, "only {} chunks", chunks.len());
238
+ assert_same_pages(&single, &chunks);
239
+ let block_starts = ["Opening paragraph", "End of", "Columns paragraph 0:", "row 0 cell 0", "Closing paragraph"];
240
+ for chunk in &chunks[1..] {
241
+ let first = chunk[0].split_once('|').map_or("", |(text, _)| text);
242
+ assert!(block_starts.iter().any(|start| first.contains(start)), "a chunk starts inside a block: {first}");
243
+ }
244
+ let longest = chunks.iter().map(Vec::len).max().unwrap();
245
+ let table_pages = single.iter().filter(|page| page.contains("row ")).count();
246
+ assert!(longest >= table_pages, "the table was split across chunks");
247
+ }
248
+
249
+ #[test]
250
+ fn merged_pdf_is_well_formed() {
251
+ for name in ["chunked-sections.docx", "chunked-no-cut.docx"] {
252
+ let (doc, fonts) = fixture(name);
253
+ let full = emit::emit(&doc, &fonts);
254
+ let pages = layout(emit::emit(&doc, &fonts), fonts.clone()).unwrap().pages().len();
255
+ let pdf = render_in_chunks(&doc, &fonts, &full.block_sizes, full.uses_total_pages, BUDGET).unwrap();
256
+ assert_eq!(merge::check(&pdf).unwrap(), pages, "{name}");
257
+ }
258
+ }
259
+
260
+ #[test]
261
+ fn typst_pdfs_have_the_structure_merge_expects() {
262
+ let (doc, fonts) = fixture("chunked-sections.docx");
263
+ let paged = layout(emit::emit(&doc, &fonts), fonts).unwrap();
264
+ for tagged in [true, false] {
265
+ let options = PdfOptions {
266
+ tagged,
267
+ ..PdfOptions::default()
268
+ };
269
+ let pdf = export(&paged, &options).unwrap();
270
+ assert_eq!(merge::check(&pdf).unwrap(), paged.pages().len());
271
+ let doubled = merge::merge(&[pdf.clone(), pdf]).unwrap();
272
+ assert_eq!(merge::check(&doubled).unwrap(), 2 * paged.pages().len());
273
+ }
274
+ }
275
+ }
@@ -9,12 +9,43 @@ const DEFAULT_SIZE: f64 = 11.0;
9
9
  const DEFAULT_CELL_MARGIN_X: f64 = 5.4;
10
10
  const TEXT_SHIFT: f64 = 0.1;
11
11
 
12
+ pub const CUT_LABEL: &str = "sc-cut";
13
+
12
14
  pub struct Emitted {
13
15
  pub source: String,
14
16
  pub files: Vec<(String, Vec<u8>)>,
17
+ pub block_sizes: Vec<Vec<usize>>,
18
+ pub uses_total_pages: bool,
19
+ }
20
+
21
+ #[derive(Debug, Clone, Default)]
22
+ pub struct Chunk {
23
+ pub parts: Vec<(usize, std::ops::Range<usize>)>,
24
+ pub page_offset: usize,
25
+ pub page_number: Option<u64>,
26
+ pub footnotes: usize,
27
+ pub total_pages: Option<u64>,
28
+ pub resumed: bool,
29
+ }
30
+
31
+ pub fn cut_id(section: usize, block: usize) -> i64 {
32
+ ((section as i64) << 32) | block as i64
33
+ }
34
+
35
+ pub fn cut_position(id: i64) -> (usize, usize) {
36
+ ((id >> 32) as usize, (id & 0xffff_ffff) as usize)
15
37
  }
16
38
 
17
39
  pub fn emit(doc: &Document, fonts: &FontSet) -> Emitted {
40
+ let parts: Vec<(usize, std::ops::Range<usize>)> = doc.sections.iter().enumerate().map(|(i, s)| (i, 0..s.blocks.len())).collect();
41
+ run(doc, fonts, &parts, None)
42
+ }
43
+
44
+ pub fn emit_chunk(doc: &Document, fonts: &FontSet, chunk: &Chunk) -> Emitted {
45
+ run(doc, fonts, &chunk.parts, Some(chunk))
46
+ }
47
+
48
+ fn run(doc: &Document, fonts: &FontSet, parts: &[(usize, std::ops::Range<usize>)], chunk: Option<&Chunk>) -> Emitted {
18
49
  let mut emitter = Emitter {
19
50
  doc,
20
51
  fonts,
@@ -23,14 +54,23 @@ pub fn emit(doc: &Document, fonts: &FontSet) -> Emitted {
23
54
  page: RefCell::new(PageSetup::default()),
24
55
  para_line: Cell::new(None),
25
56
  table_depth: Cell::new(0),
57
+ chunk,
58
+ block_sizes: RefCell::new(Vec::new()),
59
+ uses_total_pages: Cell::new(false),
60
+ drop_leading_gap: Cell::new(chunk.is_some_and(|c| c.resumed)),
61
+ multi_column: false,
26
62
  };
27
63
  emitter.preamble();
28
- for (index, section) in doc.sections.iter().enumerate() {
29
- emitter.section(index, section);
64
+ for (position, (index, range)) in parts.iter().enumerate() {
65
+ if let Some(section) = doc.sections.get(*index) {
66
+ emitter.section(*index, section, range, position == 0);
67
+ }
30
68
  }
31
69
  Emitted {
32
70
  source: emitter.out,
33
71
  files: emitter.files.into_inner(),
72
+ block_sizes: emitter.block_sizes.into_inner(),
73
+ uses_total_pages: emitter.uses_total_pages.get(),
34
74
  }
35
75
  }
36
76
 
@@ -42,6 +82,11 @@ struct Emitter<'a> {
42
82
  page: RefCell<PageSetup>,
43
83
  para_line: Cell<Option<f64>>,
44
84
  table_depth: Cell<u32>,
85
+ chunk: Option<&'a Chunk>,
86
+ block_sizes: RefCell<Vec<Vec<usize>>>,
87
+ uses_total_pages: Cell<bool>,
88
+ drop_leading_gap: Cell<bool>,
89
+ multi_column: bool,
45
90
  }
46
91
 
47
92
  struct CellOut {
@@ -79,18 +124,20 @@ impl Emitter<'_> {
79
124
  );
80
125
  }
81
126
 
82
- fn section(&mut self, index: usize, section: &Section) {
127
+ fn section(&mut self, index: usize, section: &Section, range: &std::ops::Range<usize>, first_in_chunk: bool) {
83
128
  *self.page.borrow_mut() = section.page.clone();
84
129
  let page = &section.page;
130
+ let continued = range.start > 0;
85
131
 
86
- let header = self.margin_slot(section, true, index);
87
- let footer = self.margin_slot(section, false, index);
132
+ let header = self.margin_slot(section, true, index, continued);
133
+ let footer = self.margin_slot(section, false, index, continued);
88
134
  let section_columns = section.columns.min(crate::limits::MAX_TEXT_COLUMNS);
89
- let columns = if section_columns > 1 {
135
+ let columns = if section_columns > 1 || self.multi_column {
90
136
  format!(", columns: {section_columns}")
91
137
  } else {
92
138
  String::new()
93
139
  };
140
+ self.multi_column = section_columns > 1;
94
141
  let _ = writeln!(
95
142
  self.out,
96
143
  "#set page(width: {}, height: {}, margin: (top: {}, bottom: {}, left: {}, right: {}){columns}, header: {header}, footer: {footer}, header-ascent: 0pt, footer-descent: 0pt)",
@@ -104,17 +151,27 @@ impl Emitter<'_> {
104
151
  if section_columns > 1 {
105
152
  let _ = writeln!(self.out, "#set columns(gutter: {})", pt(section.column_gap));
106
153
  }
107
- if let Some(start) = section.page_start {
154
+ if let Some(chunk) = self.chunk.filter(|_| first_in_chunk) {
155
+ if let Some(number) = chunk.page_number {
156
+ let _ = writeln!(self.out, "#counter(page).update({number})");
157
+ }
158
+ if chunk.footnotes > 0 {
159
+ let _ = writeln!(self.out, "#counter(footnote).update({})", chunk.footnotes);
160
+ }
161
+ }
162
+ if let Some(start) = section.page_start.filter(|_| !continued) {
108
163
  let _ = writeln!(self.out, "#counter(page).update({})", start.clamp(0, i64::from(u32::MAX)));
109
164
  }
110
165
  let _ = writeln!(self.out, "#metadata(none) <section-{index}>");
111
- for anchor in &section.anchors {
112
- let mut placed = String::new();
113
- self.anchor(anchor, &mut placed);
114
- self.out.push_str(&placed);
166
+ if !continued {
167
+ for anchor in &section.anchors {
168
+ let mut placed = String::new();
169
+ self.anchor(anchor, &mut placed);
170
+ self.out.push_str(&placed);
171
+ }
115
172
  }
116
173
 
117
- let blocks = self.blocks(&section.blocks, false);
174
+ let blocks = self.blocks_in(&section.blocks, false, Some((index, range)));
118
175
  if section.content_scale > 0.0 && (section.content_scale - 1.0).abs() > 0.001 {
119
176
  let percent = trim_num(section.content_scale * 100.0);
120
177
  let _ = writeln!(self.out, "#scale(x: {percent}%, y: {percent}%, reflow: true)[{blocks}]");
@@ -123,14 +180,14 @@ impl Emitter<'_> {
123
180
  }
124
181
  }
125
182
 
126
- fn margin_slot(&self, section: &Section, header: bool, index: usize) -> String {
183
+ fn margin_slot(&self, section: &Section, header: bool, index: usize, continued: bool) -> String {
127
184
  let (default, first, even) = if header {
128
185
  (&section.header_default, &section.header_first, &section.header_even)
129
186
  } else {
130
187
  (&section.footer_default, &section.footer_first, &section.footer_even)
131
188
  };
132
189
  let empty: Vec<Block> = Vec::new();
133
- let first = if section.title_page { Some(first.as_ref().unwrap_or(&empty)) } else { None };
190
+ let first = if section.title_page && !continued { Some(first.as_ref().unwrap_or(&empty)) } else { None };
134
191
  let even = if self.doc.even_odd_headers { Some(even.as_ref().unwrap_or(&empty)) } else { None };
135
192
  if default.is_none() && first.is_none() && even.is_none() {
136
193
  return "none".to_string();
@@ -159,7 +216,11 @@ impl Emitter<'_> {
159
216
  let even_expr = even.map(|e| render(Some(e)));
160
217
 
161
218
  let mut body = String::new();
162
- let _ = write!(body, "context {{ let n = here().page(); let first = locate(<section-{index}>).page(); ");
219
+ let offset = match self.chunk.map_or(0, |c| c.page_offset) {
220
+ 0 => String::new(),
221
+ pages => format!(" + {pages}"),
222
+ };
223
+ let _ = write!(body, "context {{ let n = here().page(){offset}; let first = locate(<section-{index}>).page(){offset}; ");
163
224
  if let Some(first_expr) = first_expr {
164
225
  let _ = write!(body, "if n == first {{ {first_expr} }} else ");
165
226
  }
@@ -171,6 +232,13 @@ impl Emitter<'_> {
171
232
  }
172
233
 
173
234
  fn blocks(&self, blocks: &[Block], trailing: bool) -> String {
235
+ self.blocks_in(blocks, trailing, None)
236
+ }
237
+
238
+ fn blocks_in(&self, blocks: &[Block], trailing: bool, top: Option<(usize, &std::ops::Range<usize>)>) -> String {
239
+ if top.is_some() && self.chunk.is_none() {
240
+ self.block_sizes.borrow_mut().push(Vec::with_capacity(blocks.len()));
241
+ }
174
242
  let mut out = String::new();
175
243
  let mut prev_after = 0.0;
176
244
  let mut prev_style: Option<&str> = None;
@@ -200,48 +268,74 @@ impl Emitter<'_> {
200
268
  .collect();
201
269
 
202
270
  for (index, block) in blocks.iter().enumerate() {
271
+ let emit = top.is_none_or(|(_, range)| range.contains(&index));
272
+ let leading = emit && top.is_some() && self.drop_leading_gap.replace(false);
273
+ let start = out.len();
274
+ let first_in_group = match block {
275
+ Block::Paragraph(_) => keys[index].is_none() || index == 0 || keys[index - 1] != keys[index],
276
+ _ => true,
277
+ };
278
+ let breaks_first = match block {
279
+ Block::Paragraph(p) => p.props.page_break_before == Some(true) || matches!(p.inlines.first(), Some(Inline::PageBreak)),
280
+ _ => false,
281
+ };
282
+ if let (Some((section, _)), Some(_), true, true, false) = (top, self.chunk, emit, first_in_group, breaks_first) {
283
+ let _ = writeln!(out, "#metadata({}) <{CUT_LABEL}>", cut_id(section, index));
284
+ }
203
285
  match block {
204
286
  Block::Paragraph(p) => {
205
287
  let key = &keys[index];
206
- let first_in_group = key.is_none() || index == 0 || keys[index - 1] != *key;
207
288
  let last_in_group = key.is_none() || index + 1 == keys.len() || keys[index + 1] != *key;
208
289
  let before = p.props.space_before.unwrap_or(0.0);
209
290
  let same_style = prev_paragraph && p.props.style_id.as_deref() == prev_style;
210
291
  let contextual = p.props.contextual_spacing.unwrap_or(false);
211
292
  let lower = if same_style && prev_contextual { 0.0 } else { prev_after };
212
293
  let upper = if same_style && contextual { 0.0 } else { before };
213
- let gap = if self.doc.additive_spacing {
294
+ let gap = if leading {
295
+ 0.0
296
+ } else if self.doc.additive_spacing {
214
297
  lower + upper
215
298
  } else {
216
299
  lower.max(upper)
217
300
  };
218
- self.paragraph(p, gap, first_in_group, last_in_group, &mut out);
301
+ if emit {
302
+ self.paragraph(p, gap, first_in_group, last_in_group, &mut out);
303
+ }
219
304
  prev_after = p.props.space_after.unwrap_or(0.0);
220
305
  prev_style = p.props.style_id.as_deref();
221
306
  prev_paragraph = true;
222
307
  prev_contextual = contextual;
223
308
  }
224
309
  Block::Table(t) => {
225
- if prev_after > 0.01 {
226
- let _ = writeln!(out, "#v({})", pt(prev_after));
310
+ if emit {
311
+ if prev_after > 0.01 && !leading {
312
+ let _ = writeln!(out, "#v({})", pt(prev_after));
313
+ }
314
+ self.table(t, &mut out);
227
315
  }
228
- self.table(t, &mut out);
229
316
  prev_after = 0.0;
230
317
  prev_style = None;
231
318
  prev_paragraph = false;
232
319
  prev_contextual = false;
233
320
  }
234
321
  Block::Columns(c) => {
235
- if prev_after > 0.01 {
236
- let _ = writeln!(out, "#v({})", pt(prev_after));
322
+ if emit {
323
+ if prev_after > 0.01 && !leading {
324
+ let _ = writeln!(out, "#v({})", pt(prev_after));
325
+ }
326
+ self.columns_block(c, &mut out);
237
327
  }
238
- self.columns_block(c, &mut out);
239
328
  prev_after = 0.0;
240
329
  prev_style = None;
241
330
  prev_paragraph = false;
242
331
  prev_contextual = false;
243
332
  }
244
333
  }
334
+ if top.is_some() && self.chunk.is_none() {
335
+ if let Some(sizes) = self.block_sizes.borrow_mut().last_mut() {
336
+ sizes.push(out.len() - start);
337
+ }
338
+ }
245
339
  }
246
340
  if trailing && prev_after > 0.01 {
247
341
  let _ = writeln!(out, "#v({})", pt(prev_after));
@@ -431,7 +525,13 @@ impl Emitter<'_> {
431
525
  any_text = true;
432
526
  let value = match kind {
433
527
  FieldKind::Page => format!("context counter(page).display({})", typst_str(self.page_pattern())),
434
- FieldKind::NumPages => "context counter(page).final().first()".to_string(),
528
+ FieldKind::NumPages => {
529
+ self.uses_total_pages.set(true);
530
+ match self.chunk.and_then(|c| c.total_pages) {
531
+ Some(total) => typst_str(&total.to_string()),
532
+ None => "context counter(page).final().first()".to_string(),
533
+ }
534
+ }
435
535
  };
436
536
  items.push(self.text_expr_content(&value, props, spacing).to_string());
437
537
  x = None;
@@ -1010,9 +1110,10 @@ impl Emitter<'_> {
1010
1110
  }
1011
1111
 
1012
1112
  fn register(&self, image: &ImageData) -> String {
1113
+ let (format, data) = super::images::sanitize(image.format, &image.data);
1013
1114
  let mut files = self.files.borrow_mut();
1014
- let path = format!("media/img{}.{}", files.len() + 1, image.format.extension());
1015
- files.push((path.clone(), image.data.clone()));
1115
+ let path = format!("media/img{}.{}", files.len() + 1, format.extension());
1116
+ files.push((path.clone(), data));
1016
1117
  format!("/{path}")
1017
1118
  }
1018
1119
 
@@ -10,6 +10,7 @@ const SANS: &str = "Liberation Sans";
10
10
  const SERIF: &str = "Liberation Serif";
11
11
  const MONO: &str = "Liberation Mono";
12
12
 
13
+ #[derive(Clone)]
13
14
  pub struct FontSet {
14
15
  pub fonts: Vec<Font>,
15
16
  pub book: LazyHash<FontBook>,
@@ -1,4 +1,5 @@
1
1
  use crate::limits::MAX_IMAGE_PIXELS;
2
+ use crate::model::ImageFormat;
2
3
  use typst::foundations::Bytes;
3
4
  use typst::visualize::{ExchangeFormat, RasterImage};
4
5
 
@@ -8,24 +9,28 @@ const PLACEHOLDER: [u8; 68] = [
8
9
  0x02, 0x00, 0x00, 0x05, 0x00, 0x01, 0x7a, 0x5e, 0xab, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
9
10
  ];
10
11
 
11
- pub fn sanitize(path: &str, data: Vec<u8>) -> Vec<u8> {
12
- let format = match path.rsplit('.').next() {
13
- Some("png") => ExchangeFormat::Png,
14
- Some("jpg") => ExchangeFormat::Jpg,
15
- Some("gif") => ExchangeFormat::Gif,
16
- _ => return data,
12
+ pub fn sanitize(format: ImageFormat, data: &[u8]) -> (ImageFormat, Vec<u8>) {
13
+ let exchange = match format {
14
+ ImageFormat::Png => ExchangeFormat::Png,
15
+ ImageFormat::Jpeg => ExchangeFormat::Jpg,
16
+ ImageFormat::Gif => ExchangeFormat::Gif,
17
+ ImageFormat::Svg => return (format, data.to_vec()),
17
18
  };
18
- let within_limit = dimensions(format, &data).is_some_and(|(w, h)| w > 0 && h > 0 && u64::from(w) * u64::from(h) <= MAX_IMAGE_PIXELS);
19
+ let within_limit = dimensions(exchange, data).is_some_and(|(w, h)| w > 0 && h > 0 && u64::from(w) * u64::from(h) <= MAX_IMAGE_PIXELS);
19
20
  if !within_limit {
20
- return PLACEHOLDER.to_vec();
21
+ return placeholder();
21
22
  }
22
- let bytes = Bytes::new(data);
23
- match RasterImage::plain(bytes.clone(), format) {
24
- Ok(_) => bytes.to_vec(),
25
- Err(_) => PLACEHOLDER.to_vec(),
23
+ let bytes = Bytes::new(data.to_vec());
24
+ match RasterImage::plain(bytes.clone(), exchange) {
25
+ Ok(_) => (format, bytes.to_vec()),
26
+ Err(_) => placeholder(),
26
27
  }
27
28
  }
28
29
 
30
+ fn placeholder() -> (ImageFormat, Vec<u8>) {
31
+ (ImageFormat::Png, PLACEHOLDER.to_vec())
32
+ }
33
+
29
34
  fn dimensions(format: ExchangeFormat, data: &[u8]) -> Option<(u32, u32)> {
30
35
  match format {
31
36
  ExchangeFormat::Png => {
@@ -0,0 +1,286 @@
1
+ use crate::error::Error;
2
+ use std::io::Write;
3
+
4
+ struct Parsed<'a> {
5
+ data: &'a [u8],
6
+ spans: Vec<Option<(usize, usize)>>,
7
+ header_end: usize,
8
+ root: usize,
9
+ info: Option<usize>,
10
+ id: Option<&'a [u8]>,
11
+ pages: usize,
12
+ kids: Vec<usize>,
13
+ }
14
+
15
+ pub fn merge(pdfs: &[Vec<u8>]) -> Result<Vec<u8>, Error> {
16
+ match pdfs {
17
+ [] => return Err(Error::new("cannot merge: no pages")),
18
+ [single] => return Ok(single.clone()),
19
+ _ => {}
20
+ }
21
+ let parsed = pdfs.iter().map(|pdf| parse(pdf)).collect::<Result<Vec<_>, _>>()?;
22
+ let objects: usize = parsed.iter().map(|p| p.spans.len() - 1).sum();
23
+ let pages_id = objects + 1;
24
+ let size = pages_id + 1;
25
+
26
+ let mut out = Vec::with_capacity(pdfs.iter().map(Vec::len).sum::<usize>() + size * 24);
27
+ out.extend_from_slice(&parsed[0].data[..parsed[0].header_end]);
28
+ let mut offsets: Vec<Option<usize>> = vec![None; size];
29
+ let mut kids = Vec::new();
30
+ let mut base = 0;
31
+ for (index, part) in parsed.iter().enumerate() {
32
+ let renumber = |id: usize| if id == part.pages { pages_id } else { base + id };
33
+ for (id, span) in part.spans.iter().enumerate() {
34
+ let Some((start, end)) = *span else {
35
+ continue;
36
+ };
37
+ let foreign_root = index > 0 && (id == part.root || Some(id) == part.info);
38
+ if id == part.pages || foreign_root {
39
+ continue;
40
+ }
41
+ let object = &part.data[start..end];
42
+ let body_at = find(object, b"obj").ok_or_else(broken)? + 3;
43
+ offsets[base + id] = Some(out.len());
44
+ let _ = write!(out, "{} 0 obj", base + id);
45
+ rewrite(&object[body_at..], &renumber, &mut out);
46
+ }
47
+ kids.extend(part.kids.iter().map(|&kid| base + kid));
48
+ base += part.spans.len() - 1;
49
+ }
50
+
51
+ offsets[pages_id] = Some(out.len());
52
+ let _ = write!(out, "{pages_id} 0 obj\n<</Type/Pages/Count {}/Kids[", kids.len());
53
+ for (i, kid) in kids.iter().enumerate() {
54
+ let _ = write!(out, "{}{kid} 0 R", if i > 0 { " " } else { "" });
55
+ }
56
+ out.extend_from_slice(b"]>>\nendobj\n");
57
+
58
+ let xref = out.len();
59
+ let _ = write!(out, "xref\n0 {size}\n");
60
+ for offset in &offsets {
61
+ match offset {
62
+ Some(at) => {
63
+ let _ = write!(out, "{at:010} 00000 n \n");
64
+ }
65
+ None => out.extend_from_slice(b"0000000000 65535 f \n"),
66
+ }
67
+ }
68
+ let first = &parsed[0];
69
+ let _ = write!(out, "trailer\n<</Size {size}/Root {} 0 R", first.root);
70
+ if let Some(info) = first.info {
71
+ let _ = write!(out, "/Info {info} 0 R");
72
+ }
73
+ if let Some(id) = first.id {
74
+ out.extend_from_slice(b"/ID");
75
+ out.extend_from_slice(id);
76
+ }
77
+ let _ = write!(out, ">>\nstartxref\n{xref}\n%%EOF\n");
78
+ Ok(out)
79
+ }
80
+
81
+ #[cfg(test)]
82
+ pub fn check(pdf: &[u8]) -> Result<usize, Error> {
83
+ let parsed = parse(pdf)?;
84
+ let referenced = std::cell::RefCell::new(Vec::new());
85
+ for (start, end) in parsed.spans.iter().flatten().copied() {
86
+ let object = &pdf[start..end];
87
+ let body_at = find(object, b"obj").ok_or_else(broken)? + 3;
88
+ rewrite(
89
+ &object[body_at..],
90
+ &|id| {
91
+ referenced.borrow_mut().push(id);
92
+ id
93
+ },
94
+ &mut Vec::new(),
95
+ );
96
+ }
97
+ for id in referenced.into_inner() {
98
+ if parsed.spans.get(id).copied().flatten().is_none() {
99
+ return Err(Error::new(format!("dangling reference to object {id}")));
100
+ }
101
+ }
102
+ let parent = format!("/Parent {} 0 R", parsed.pages);
103
+ for &kid in &parsed.kids {
104
+ let (start, end) = parsed.spans.get(kid).copied().flatten().ok_or_else(broken)?;
105
+ let page = &pdf[start..end];
106
+ if find(page, b"/Type/Page").is_none() || find(page, parent.as_bytes()).is_none() {
107
+ return Err(Error::new(format!("page object {kid} is not a child of the page tree")));
108
+ }
109
+ }
110
+ Ok(parsed.kids.len())
111
+ }
112
+
113
+ fn broken() -> Error {
114
+ Error::new("cannot merge: unexpected PDF structure")
115
+ }
116
+
117
+ fn parse(data: &[u8]) -> Result<Parsed<'_>, Error> {
118
+ let startxref = rfind(data, b"startxref").ok_or_else(broken)?;
119
+ let xref = number_after(data, startxref + 9).ok_or_else(broken)?;
120
+ if !data.get(xref..).is_some_and(|rest| rest.starts_with(b"xref")) {
121
+ return Err(broken());
122
+ }
123
+ let trailer_at = xref + find(&data[xref..], b"trailer").ok_or_else(broken)?;
124
+ let table = std::str::from_utf8(&data[xref + 4..trailer_at]).map_err(|_| broken())?;
125
+ let mut tokens = table.split_ascii_whitespace();
126
+ let mut entries: Vec<(usize, usize)> = Vec::new();
127
+ let mut size = 0;
128
+ while let (Some(first), Some(count)) = (tokens.next(), tokens.next()) {
129
+ let first: usize = first.parse().map_err(|_| broken())?;
130
+ let count: usize = count.parse().map_err(|_| broken())?;
131
+ for id in first..first + count {
132
+ let (Some(offset), Some(_), Some(kind)) = (tokens.next(), tokens.next(), tokens.next()) else {
133
+ return Err(broken());
134
+ };
135
+ if kind == "n" {
136
+ entries.push((offset.parse().map_err(|_| broken())?, id));
137
+ }
138
+ size = size.max(id + 1);
139
+ }
140
+ }
141
+ entries.sort_unstable();
142
+ let mut spans = vec![None; size];
143
+ for (i, &(offset, id)) in entries.iter().enumerate() {
144
+ let end = entries.get(i + 1).map_or(xref, |&(next, _)| next);
145
+ if offset >= end || !data[offset..].starts_with(format!("{id} 0 obj").as_bytes()) {
146
+ return Err(broken());
147
+ }
148
+ spans[id] = Some((offset, end));
149
+ }
150
+ let header_end = entries.first().map(|&(offset, _)| offset).ok_or_else(broken)?;
151
+
152
+ let trailer = &data[trailer_at..startxref];
153
+ let root = reference_after(trailer, b"/Root").ok_or_else(broken)?;
154
+ let info = reference_after(trailer, b"/Info");
155
+ let id = find(trailer, b"/ID").and_then(|at| {
156
+ let rest = &trailer[at + 3..];
157
+ let open = rest.iter().position(|&c| c == b'[')?;
158
+ let close = rest.iter().position(|&c| c == b']')?;
159
+ (open < close).then(|| &rest[open..=close])
160
+ });
161
+
162
+ let object = |id: usize| spans.get(id).copied().flatten().map(|(start, end)| &data[start..end]);
163
+ let catalog = object(root).ok_or_else(broken)?;
164
+ let pages = reference_after(catalog, b"/Pages").ok_or_else(broken)?;
165
+ let tree = object(pages).ok_or_else(broken)?;
166
+ let kids_at = find(tree, b"/Kids").ok_or_else(broken)?;
167
+ let kids_end = kids_at + find(&tree[kids_at..], b"]").ok_or_else(broken)?;
168
+ let kids: Vec<usize> = std::str::from_utf8(&tree[kids_at + 5..kids_end])
169
+ .map_err(|_| broken())?
170
+ .trim_start_matches(|c: char| c == '[' || c.is_ascii_whitespace())
171
+ .split('R')
172
+ .filter_map(|r| r.split_ascii_whitespace().next().and_then(|n| n.parse().ok()))
173
+ .collect();
174
+ for &kid in &kids {
175
+ let page = object(kid).ok_or_else(broken)?;
176
+ if find(page, b"/Type/Pages").is_some() {
177
+ return Err(broken());
178
+ }
179
+ }
180
+
181
+ Ok(Parsed {
182
+ data,
183
+ spans,
184
+ header_end,
185
+ root,
186
+ info,
187
+ id,
188
+ pages,
189
+ kids,
190
+ })
191
+ }
192
+
193
+ fn rewrite(src: &[u8], renumber: &impl Fn(usize) -> usize, out: &mut Vec<u8>) {
194
+ let mut i = 0;
195
+ let mut nesting = 0usize;
196
+ while i < src.len() {
197
+ let c = src[i];
198
+ if nesting > 0 {
199
+ out.push(c);
200
+ match c {
201
+ b'\\' if i + 1 < src.len() => {
202
+ out.push(src[i + 1]);
203
+ i += 1;
204
+ }
205
+ b'(' => nesting += 1,
206
+ b')' => nesting -= 1,
207
+ _ => {}
208
+ }
209
+ i += 1;
210
+ continue;
211
+ }
212
+ let boundary = i == 0 || !is_regular(src[i - 1]);
213
+ if c == b'(' {
214
+ nesting = 1;
215
+ } else if boundary && src[i..].starts_with(b"stream") && src.get(i + 6).is_some_and(|&n| n == b'\r' || n == b'\n') {
216
+ out.extend_from_slice(&src[i..]);
217
+ return;
218
+ } else if boundary && c.is_ascii_digit() {
219
+ if let Some((id, len)) = reference(&src[i..]) {
220
+ let _ = write!(out, "{} 0 R", renumber(id));
221
+ i += len;
222
+ continue;
223
+ }
224
+ while i < src.len() && is_regular(src[i]) {
225
+ out.push(src[i]);
226
+ i += 1;
227
+ }
228
+ continue;
229
+ }
230
+ out.push(c);
231
+ i += 1;
232
+ }
233
+ }
234
+
235
+ fn reference(src: &[u8]) -> Option<(usize, usize)> {
236
+ let digits = |from: usize| src[from..].iter().take_while(|c| c.is_ascii_digit()).count();
237
+ let spaces = |from: usize| src[from..].iter().take_while(|c| c.is_ascii_whitespace()).count();
238
+ let id_len = digits(0);
239
+ let mut at = id_len;
240
+ let gap = spaces(at);
241
+ if gap == 0 {
242
+ return None;
243
+ }
244
+ at += gap;
245
+ let generation = digits(at);
246
+ if generation == 0 {
247
+ return None;
248
+ }
249
+ at += generation;
250
+ let gap = spaces(at);
251
+ if gap == 0 {
252
+ return None;
253
+ }
254
+ at += gap;
255
+ if src.get(at) != Some(&b'R') || src.get(at + 1).is_some_and(|&c| is_regular(c)) {
256
+ return None;
257
+ }
258
+ let id = std::str::from_utf8(&src[..id_len]).ok()?.parse().ok()?;
259
+ Some((id, at + 1))
260
+ }
261
+
262
+ fn is_regular(c: u8) -> bool {
263
+ !c.is_ascii_whitespace() && !b"()<>[]{}/%".contains(&c)
264
+ }
265
+
266
+ fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
267
+ haystack.windows(needle.len()).position(|w| w == needle)
268
+ }
269
+
270
+ fn rfind(haystack: &[u8], needle: &[u8]) -> Option<usize> {
271
+ haystack.windows(needle.len()).rposition(|w| w == needle)
272
+ }
273
+
274
+ fn number_after(data: &[u8], from: usize) -> Option<usize> {
275
+ let rest = data.get(from..)?;
276
+ let start = rest.iter().position(|c| c.is_ascii_digit())?;
277
+ let len = rest[start..].iter().take_while(|c| c.is_ascii_digit()).count();
278
+ std::str::from_utf8(&rest[start..start + len]).ok()?.parse().ok()
279
+ }
280
+
281
+ fn reference_after(data: &[u8], key: &[u8]) -> Option<usize> {
282
+ let at = find(data, key)? + key.len();
283
+ let rest = &data[at..];
284
+ let start = rest.iter().position(|c| !c.is_ascii_whitespace())?;
285
+ reference(&rest[start..]).map(|(id, _)| id)
286
+ }
@@ -1,6 +1,8 @@
1
+ mod chunks;
1
2
  pub mod emit;
2
3
  pub mod fonts;
3
4
  mod images;
5
+ mod merge;
4
6
  mod notes;
5
7
  pub mod world;
6
8
 
@@ -22,24 +24,23 @@ pub fn render_pdf(mut document: Document, fonts_dir: &Path) -> Result<Vec<u8>, E
22
24
  if let Ok(path) = std::env::var("SIMPLE_CONVERTER_DUMP_TYPST") {
23
25
  let _ = std::fs::write(path, &emitted.source);
24
26
  }
25
- if let Ok(path) = std::env::var("SIMPLE_CONVERTER_LOAD_TYPST") {
27
+ let loaded = std::env::var("SIMPLE_CONVERTER_LOAD_TYPST").ok();
28
+ if let Some(path) = &loaded {
26
29
  emitted.source = std::fs::read_to_string(path).map_err(|e| Error::new(format!("cannot read typst source: {e}")))?;
27
30
  }
31
+ crate::limits::check_layout_source(emitted.source.len())?;
28
32
 
29
- let mut world = ConverterWorld::new(emitted.source, fonts);
30
- for (path, data) in emitted.files {
31
- let data = images::sanitize(&path, data);
32
- world.add_file(&path, data)?;
33
+ let has_notes = doc.sections.iter().any(|s| !s.notes.is_empty());
34
+ if loaded.is_none() && !has_notes && emitted.source.len() > chunks::CHUNK_SOURCE_BYTES {
35
+ let block_sizes = std::mem::take(&mut emitted.block_sizes);
36
+ let uses_total_pages = emitted.uses_total_pages;
37
+ drop(emitted);
38
+ return chunks::render(doc, &fonts, &block_sizes, uses_total_pages);
33
39
  }
34
- let compiled = typst::compile::<PagedDocument>(&world);
35
- let paged = compiled
36
- .output
37
- .map_err(|errors| Error::new(format!("layout failed: {}", diagnostics(&errors))))?;
38
40
 
39
- let mut pdf = typst_pdf::pdf(&paged, &PdfOptions::default())
40
- .map_err(|errors| Error::new(format!("pdf export failed: {}", diagnostics(&errors))))?;
41
+ let paged = layout(emitted, fonts)?;
42
+ let mut pdf = export(&paged, &PdfOptions::default())?;
41
43
 
42
- let has_notes = doc.sections.iter().any(|s| !s.notes.is_empty());
43
44
  if has_notes && paged.pages().len() == doc.sections.len() {
44
45
  let pages: Vec<(f64, &[crate::model::PageNote])> =
45
46
  doc.sections.iter().map(|s| (s.page.height, s.notes.as_slice())).collect();
@@ -50,6 +51,20 @@ pub fn render_pdf(mut document: Document, fonts_dir: &Path) -> Result<Vec<u8>, E
50
51
  Ok(pdf)
51
52
  }
52
53
 
54
+ fn layout(emitted: emit::Emitted, fonts: FontSet) -> Result<PagedDocument, Error> {
55
+ let mut world = ConverterWorld::new(emitted.source, fonts);
56
+ for (path, data) in emitted.files {
57
+ world.add_file(&path, data)?;
58
+ }
59
+ typst::compile::<PagedDocument>(&world)
60
+ .output
61
+ .map_err(|errors| Error::new(format!("layout failed: {}", diagnostics(&errors))))
62
+ }
63
+
64
+ fn export(paged: &PagedDocument, options: &PdfOptions) -> Result<Vec<u8>, Error> {
65
+ typst_pdf::pdf(paged, options).map_err(|errors| Error::new(format!("pdf export failed: {}", diagnostics(&errors))))
66
+ }
67
+
53
68
  fn diagnostics(errors: &[SourceDiagnostic]) -> String {
54
69
  let mut messages: Vec<String> = Vec::new();
55
70
  for error in errors {
@@ -1186,7 +1186,7 @@ fn paginate(sheet: &Sheet, name: &str, styles: &Styles, cells: &mut u64) -> Resu
1186
1186
  for (rg, cg) in &order {
1187
1187
  *cells += (rg.len() * cg.len()) as u64;
1188
1188
  if *cells > crate::limits::MAX_SHEET_CELLS {
1189
- return Err(Error::new(format!("the spreadsheet is too large to convert: more than {} cells to print", crate::limits::MAX_SHEET_CELLS)));
1189
+ return Err(Error::limit(format!("the spreadsheet is too large to convert: more than {} cells to print", crate::limits::MAX_SHEET_CELLS)));
1190
1190
  }
1191
1191
  }
1192
1192
 
@@ -389,7 +389,7 @@ fn read_table(table: &Element, odf: &OdfStyles, formats: &mut Formats, pkg: &Pac
389
389
  let repeats = if has_content || !template.cells.is_empty() { count.min(4096) } else { count };
390
390
  materialised += template.cells.len() as u64 * u64::from(repeats);
391
391
  if materialised > crate::limits::MAX_SHEET_CELLS {
392
- return Err(Error::new(format!("the spreadsheet is too large to convert: more than {} cells", crate::limits::MAX_SHEET_CELLS)));
392
+ return Err(Error::limit(format!("the spreadsheet is too large to convert: more than {} cells", crate::limits::MAX_SHEET_CELLS)));
393
393
  }
394
394
  if !template.cells.is_empty() || height.is_some() || hidden {
395
395
  for k in 0..repeats.min(if template.cells.is_empty() { 1 } else { repeats }) {
package/core/src/xml.rs CHANGED
@@ -64,12 +64,23 @@ pub fn parse(bytes: &[u8]) -> Result<Element, Error> {
64
64
  name: String::from("#document"),
65
65
  ..Element::default()
66
66
  }];
67
+ let mut elements: usize = 0;
67
68
 
68
69
  loop {
69
- match reader.read_event()? {
70
+ let event = reader.read_event()?;
71
+ if matches!(event, Event::Start(_) | Event::Empty(_)) {
72
+ elements += 1;
73
+ if elements > crate::limits::MAX_XML_ELEMENTS {
74
+ return Err(Error::limit(format!(
75
+ "document is too large to convert: a part has more than {} XML elements",
76
+ crate::limits::MAX_XML_ELEMENTS
77
+ )));
78
+ }
79
+ }
80
+ match event {
70
81
  Event::Start(start) => {
71
82
  if stack.len() > crate::limits::MAX_XML_DEPTH {
72
- return Err(Error::new(format!("xml: elements are nested more than {} levels deep", crate::limits::MAX_XML_DEPTH)));
83
+ return Err(Error::limit(format!("xml: elements are nested more than {} levels deep", crate::limits::MAX_XML_DEPTH)));
73
84
  }
74
85
  stack.push(element(&start)?)
75
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-x-to-pdf",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Convert Word, PowerPoint, Excel and OpenDocument files to PDF for Bare: a Rust engine on Typst",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -118,16 +118,16 @@
118
118
  }
119
119
  },
120
120
  "optionalDependencies": {
121
- "bare-x-to-pdf-android-arm": "0.5.3",
122
- "bare-x-to-pdf-android-arm64": "0.5.3",
123
- "bare-x-to-pdf-android-x64": "0.5.3",
124
- "bare-x-to-pdf-darwin-arm64": "0.5.3",
125
- "bare-x-to-pdf-darwin-x64": "0.5.3",
126
- "bare-x-to-pdf-ios-arm64": "0.5.3",
127
- "bare-x-to-pdf-ios-arm64-simulator": "0.5.3",
128
- "bare-x-to-pdf-ios-x64-simulator": "0.5.3",
129
- "bare-x-to-pdf-linux-arm64": "0.5.3",
130
- "bare-x-to-pdf-linux-x64": "0.5.3",
131
- "bare-x-to-pdf-win32-x64": "0.5.3"
121
+ "bare-x-to-pdf-android-arm": "0.6.0",
122
+ "bare-x-to-pdf-android-arm64": "0.6.0",
123
+ "bare-x-to-pdf-android-x64": "0.6.0",
124
+ "bare-x-to-pdf-darwin-arm64": "0.6.0",
125
+ "bare-x-to-pdf-darwin-x64": "0.6.0",
126
+ "bare-x-to-pdf-ios-arm64": "0.6.0",
127
+ "bare-x-to-pdf-ios-arm64-simulator": "0.6.0",
128
+ "bare-x-to-pdf-ios-x64-simulator": "0.6.0",
129
+ "bare-x-to-pdf-linux-arm64": "0.6.0",
130
+ "bare-x-to-pdf-linux-x64": "0.6.0",
131
+ "bare-x-to-pdf-win32-x64": "0.6.0"
132
132
  }
133
133
  }