diff --git a/agents.md b/agents.md index 0ff27dc..f3015c9 100644 --- a/agents.md +++ b/agents.md @@ -311,3 +311,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-16(用户定):合集安装包(R7.1)。macOS 一个公证的 `ElyOffice-<版本>-<架构>.dmg`,装首批六个已签名的 .app 与“应用程序”替身,用户拖入所要的(`release.sh` 的 `dmg` 与单个应用共用);无 Developer ID Installer 证书,不出 .pkg。Windows 一个 `ElyOffice-<版本>-windows-x86_64-setup.exe`(`packaging/windows/bundle.nsi`),组件页勾选应用,所选的各自的安装程序以 `/S` 运行,所以装法、位置、升级、卸载项与单装完全相同;某个退出码非零即停下并写明其名与退出码(2 为文件被占用或位置被占),之前装上的照旧。Linux 不出合集。单个应用的包、更新清单照旧。 - 2026-10-16(Gitea #6):设置窗口多出一行应用菜单(R7.2)。设置窗口沿用主窗口的外壳 `Shell`,Windows 与 Linux 上随之画出菜单栏(macOS 菜单在系统菜单栏,不受影响)与更新横幅。菜单栏与更新横幅只属主窗口:`Shell::secondary` 去掉两者,设置窗口用它;`settings/tests.rs` 量内容起点距标题栏底:只装菜单时主窗口大于零(macOS 为零),再有待下载的新版时更大,设置窗口为零。 - 2026-10-16(Gitea #4):装了宋体仍报缺少(R7.3)。内核按名找字体时比较 `GetEnglishSearchFontName`(小写 ASCII、去空格、中日韩本地名译为英文名,`宋体` 即 `simsun`),所以文档的 `宋体` 找到并用上 SimSun;缺字检查却比较原名与所用字体名,报为缺失。Linux 上内核以界面语言(英文)为 fontconfig 字体取名,文档里的本地名一律报缺。补丁 `0048-lok-fonts-missing-by-search-name.patch`:缺字检查同样比较检索名。实测(2026-10-16):Arch 容器装 ElyDoc 0.1.5 的 Arch 包与 simsun.ttc,40 行宋体五号、文档网格的 docx 报缺宋体,正文实为 SimSun(无此字体时为方框),与 Windows 同为一页;换上打过 0048 的内核后同一容器不再报缺、仍为一页。用户所说一页变两页未能复现,需其文件。缺字检查测试:`navigate/tests.rs` 以随包的 DejaVu Sans 写作 `DEJAVU SANS`,只报真缺的字体。另见:Linux 上属性栏的字体框为空(字体列表只有 SimSun,文档写宋体),未改。 +- 2026-10-16(Gitea #5):每个 PDF 打开即报已损坏(R7.4)。C11.8 把画图块与导出时 MuPDF 的任何警告都算作损坏;读页面时载入未嵌入的字体也发警告(中文文件里写名而不嵌入的宋体等:`non-embedded font using identity encoding`),合法的文件因此转为修复副本、保存一律另存。现在警告监视(`keep_warning`,打开时的扫描、画图块、导出与 Markdown 的 PDF 共用)只略去这一句与紧随其后的 MuPDF 重复计数(`... repeated N times...`);其余照算损坏,含内容的语法错误、无字体写字、坏的嵌入字体与图片流。读文字与搜索也受监视(坏图片读过即被缓存,之后画图块、导出与检查都不再报警告):其间遇到的损坏记入报告与该文档,下一次画图块或检查图片报出,报出之前导出图片与文字一律拒绝。监视开始时,MuPDF 尚未报出的重复计数(来自其余未受监视的读取)按它所重复的那句归类(`ctx->warn.message`):字体提示的重复略去,其余照算。`tests/output.rs` 核对:两个同名未嵌入字体的文件先读文字再画图块、导出图片与文字都不算损坏;无字体写字与 `BT ] ET` 的图块为损坏;`[/FlateDecode /ASCIIHexDecode]` 下校验和错的图片使两种导出都被拒、图块为损坏。先读文字或搜索后,这样的两张图片、颜色键掩码无效的一张图片、以及它在两个未嵌入字体之前或之后画出,画图块、导出与检查图片仍报损坏。 diff --git a/crates/ely-pdf-engine/shim/ely_mupdf.c b/crates/ely-pdf-engine/shim/ely_mupdf.c index b84cc1c..41b6ace 100644 --- a/crates/ely-pdf-engine/shim/ely_mupdf.c +++ b/crates/ely-pdf-engine/shim/ely_mupdf.c @@ -56,10 +56,21 @@ typedef struct void *user; } passing; +/* MuPDF draws an unembedded font with another. */ +static const char notice[] = "non-embedded font using identity encoding:"; + +/* 1 while the last warning was that notice. */ +static int substituted; + static void keep_warning(void *user, const char *message) { passing *p = user; - if (strlen(warned) + strlen(message) + 2 < sizeof warned) + /* An unembedded font drawn by another is no damage. */ + if (!strncmp(message, notice, sizeof notice - 1)) + substituted = 1; + else if (strncmp(message, "... repeated ", 13)) + substituted = 0; + if (!substituted && strlen(warned) + strlen(message) + 2 < sizeof warned) { fz_strlcat(warned, message, sizeof warned); fz_strlcat(warned, "\n", sizeof warned); @@ -74,6 +85,8 @@ static int watched; /* Collects MuPDF's warnings until `ely_watch_end`. */ void ely_watch_begin(fz_context *ctx) { + /* A pending repeat is of MuPDF's last warning. */ + substituted = !strncmp(ctx->warn.message, notice, sizeof notice - 1); watching.next = fz_warning_callback(ctx, &watching.user); warned[0] = 0; watched = 1; diff --git a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/main.rs b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/main.rs index 25f9c94..ec31b84 100644 --- a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/main.rs +++ b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/main.rs @@ -31,6 +31,8 @@ struct Doc { /// What MuPDF warned at open; kept as the repair report. warnings: String, repair: Option, + /// Damage a read met that no draw has told yet. + found: bool, /// The repaired copy standing in; removed on close. copy: Option, /// MuPDF's steps: an edit's id, or none for a save. @@ -359,7 +361,7 @@ impl Worker { let id = self.next; self.next += 1; let warnings = output::open_warnings(); - let doc = Doc { open, password: String::new(), owner, warnings, repair: None, copy: None, steps: Vec::new(), at: 0 }; + let doc = Doc { open, password: String::new(), owner, warnings, repair: None, found: false, copy: None, steps: Vec::new(), at: 0 }; self.docs.insert(id, doc); if !locked && let Err(error) = self.repair(id) { self.forget(id)?; @@ -405,13 +407,12 @@ impl Worker { self.drawn(id, page, (scale, rotate), tile, dark) } tag::TEXT => { - let doc = self.doc(ask.u64()?)?; - self.mupdf.text(doc, ask.u64()? as usize) + let (id, page) = (ask.u64()?, ask.u64()? as usize); + self.read(id, |mupdf, doc| mupdf.text(doc, page)) } tag::SEARCH => { - let doc = self.doc(ask.u64()?)?; - let page = ask.u64()? as usize; - self.mupdf.search(doc, page, &ask.str()?) + let (id, page, needle) = (ask.u64()?, ask.u64()? as usize, ask.str()?); + self.read(id, |mupdf, doc| mupdf.search(doc, page, &needle)) } tag::OUTLINE => self.mupdf.outline(self.doc(ask.u64()?)?), tag::HELLO => Ok(Pack::new().str(ely_pdf_engine::proto::BUILD).bytes()), diff --git a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/output.rs b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/output.rs index f3838c8..0c90dc3 100644 --- a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/output.rs +++ b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/output.rs @@ -82,6 +82,24 @@ impl crate::Worker { Ok(()) } + /// A text read; damage it meets waits for a draw. + pub(crate) fn read(&mut self, id: u64, read: impl FnOnce(&Mupdf, &Open) -> io::Result>) -> io::Result> { + let doc = self.doc(id)?; + unsafe { ely_watch_begin(self.mupdf.ctx) }; + let answer = read(&self.mupdf, doc); + if unsafe { ely_watch_end(self.mupdf.ctx) } != 0 { + self.note_damage(id)?; + self.docs.get_mut(&id).expect("just read").found = true; + } + answer + } + + /// Takes damage a read found; the caller tells it. + fn found(&mut self, id: u64) -> io::Result { + let doc = self.docs.get_mut(&id).ok_or_else(|| io::Error::other(format!("no document {id}")))?; + Ok(std::mem::take(&mut doc.found)) + } + /// A tile, and whether drawing it met damage. pub(crate) fn drawn(&mut self, id: u64, page: usize, view: (f32, i32), tile: [i32; 4], dark: bool) -> io::Result> { unsafe { ely_watch_begin(self.mupdf.ctx) }; @@ -90,7 +108,9 @@ impl crate::Worker { if warned { self.note_damage(id)?; } - Ok(Pack::new().u64(u64::from(warned)).blob(&pixels?).bytes()) + let pixels = pixels?; + let damaged = warned | self.found(id)?; + Ok(Pack::new().u64(u64::from(damaged)).blob(&pixels).bytes()) } /// Every picture decoded before an overwrite; true if any failed. @@ -99,6 +119,7 @@ impl crate::Worker { if failed { self.note_damage(id)?; } + let failed = failed | self.found(id)?; Ok(Pack::new().u64(u64::from(failed)).bytes()) } @@ -153,6 +174,12 @@ impl crate::Worker { pub(crate) fn output(&mut self, kind: u8, id: u64, ask: &mut Unpack) -> io::Result> { let has = self.allowed(id)?; let doc = self.record(id)?; + // What a read found is no whole export. + if matches!(kind, tag::EXPORT_IMAGE | tag::EXPORT_TEXT) && doc.found { + refuse(may::COPY, has)?; + let report = doc.repair.as_deref().expect("noted with the damage"); + return Err(io::Error::other(format!("the document did not read whole: {report}"))); + } match kind { tag::EXPORT_IMAGE => { refuse(may::COPY, has)?; diff --git a/crates/ely-pdf-engine/tests/output.rs b/crates/ely-pdf-engine/tests/output.rs index a4f1882..49b3b9b 100644 --- a/crates/ely-pdf-engine/tests/output.rs +++ b/crates/ely-pdf-engine/tests/output.rs @@ -148,6 +148,7 @@ fn a_broken_stream_under_a_sound_table_is_damage_and_fails_exports() { let report = worker.repair_report(doc).expect("asks").expect("damage is reported"); assert!(!report.trim().is_empty()); assert!(worker.export_text(doc, &dir.path().join("doc.txt")).is_err(), "partial text is no export"); + assert!(worker.export_image(doc, 0, 72.0, &dir.path().join("page.png")).is_err(), "nor a partial page"); worker.rotate_pages(doc, &[0], 90).expect("edits the copy"); doc = worker.now(doc).expect("the state"); worker.save(doc, &dir.path().join("fixed.pdf")).expect("saves the copy"); @@ -186,3 +187,131 @@ fn a_broken_picture_shows_when_drawn_or_checked() { assert!(other.check_pictures(again).expect("checks"), "the check finds it undrawn"); assert_eq!(std::fs::read(&path).expect("reads"), bytes); } + +/// As many Chinese files name their fonts. +const SIMSUN: &str = "<< /Type /Font /Subtype /Type0 /BaseFont /SimSun /Encoding /Identity-H /DescendantFonts [<< /Type /Font /Subtype /CIDFontType2 /BaseFont /SimSun /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 2 >> /FontDescriptor << /Type /FontDescriptor /FontName /SimSun /Flags 6 /FontBBox [0 -141 996 855] /ItalicAngle 0 /Ascent 859 /Descent -141 /CapHeight 859 /StemV 91 >> >>] >>"; + +/// Two runs, each in its own unembedded font. +const TWO_RUNS: &str = "BT /F1 24 Tf 72 700 Td <00410042> Tj /F2 24 Tf <0043> Tj ET"; + +#[test] +fn an_unembedded_font_is_no_damage() { + let dir = tempfile::tempdir().expect("a temp dir"); + let path = dir.path().join("doc.pdf"); + // Two fonts, one notice: a repeat is no damage either. + let fonts = " /Resources << /Font << /F1 5 0 R /F2 6 0 R >> >>"; + let bytes = one_page("", fonts, TWO_RUNS, &[SIMSUN.to_string(), SIMSUN.to_string()]); + std::fs::write(&path, &bytes).expect("writes"); + // Each on a fresh worker: no font loaded before. + let (mut worker, doc) = opened(&path); + worker.export_text(doc, &dir.path().join("doc.txt")).expect("the text exports"); + let (mut worker, doc) = opened(&path); + worker.export_image(doc, 0, 72.0, &dir.path().join("page.png")).expect("the page exports"); + let (mut worker, doc) = opened(&path); + // Read unwatched first: its repeat is pending. + worker.text(doc, 0).expect("reads"); + let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws"); + assert!(!tile.damaged, "a substituted font draws whole"); + assert_eq!(worker.repair_report(doc).expect("asks"), None); +} + +#[test] +fn unreadable_content_still_draws_as_damage() { + let dir = tempfile::tempdir().expect("a temp dir"); + // Text with no font; a stray bracket. + for content in ["BT 72 700 Td (Missing text) Tj ET", "BT ] ET"] { + let path = dir.path().join("doc.pdf"); + std::fs::write(&path, one_page("", "", content, &[])).expect("writes"); + let (mut worker, doc) = opened(&path); + let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws"); + assert!(tile.damaged, "{content} draws as damage"); + } +} + +/// Two pictures whose zlib checksums are wrong. +fn broken_pictures(dir: &Path) -> std::path::PathBuf { + let hex = format!("{}>", "ff0000".repeat(16)); + // One stored deflate block, its checksum wrong. + let mut zlib = vec![0x78, 0x01, 0x01]; + zlib.extend_from_slice(&(hex.len() as u16).to_le_bytes()); + zlib.extend_from_slice(&(!(hex.len() as u16)).to_le_bytes()); + zlib.extend_from_slice(hex.as_bytes()); + zlib.extend_from_slice(&[0, 0, 0, 0]); + let slot = "#".repeat(zlib.len()); + let image = format!("<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter [/FlateDecode /ASCIIHexDecode] /Length {} >> +stream +{slot} +endstream", zlib.len()); + let names = " /Resources << /XObject << /Im1 5 0 R /Im2 6 0 R >> >>"; + let mut bytes = one_page("", names, "q 100 0 0 100 72 600 cm /Im1 Do Q q 100 0 0 100 72 400 cm /Im2 Do Q", &[image.clone(), image]); + for _ in 0..2 { + let at = bytes.windows(slot.len()).position(|w| w == slot.as_bytes()).expect("the slot"); + bytes[at..at + zlib.len()].copy_from_slice(&zlib); + } + let path = dir.join("doc.pdf"); + std::fs::write(&path, &bytes).expect("writes"); + path +} + +/// A picture whose colour key mask is invalid. +fn masked() -> String { + let hex = format!("{}>", "ff0000".repeat(16)); + format!("<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Mask [null null null null null null] /Filter /ASCIIHexDecode /Length {} >> +stream +{hex} +endstream", hex.len()) +} + +/// The bad mask, alone or drawn beside two font notices. +fn bad_mask(dir: &Path, fonts: Option) -> std::path::PathBuf { + let picture = "q 100 0 0 100 72 600 cm /Im1 Do Q"; + let (resources, content, objects) = match fonts { + None => (String::new(), picture.to_string(), vec![masked()]), + Some(first) => ( + " /Font << /F1 6 0 R /F2 7 0 R >>".to_string(), + if first { format!("{picture} {TWO_RUNS}") } else { format!("{TWO_RUNS} {picture}") }, + vec![masked(), SIMSUN.to_string(), SIMSUN.to_string()], + ), + }; + let names = format!(" /Resources << /XObject << /Im1 5 0 R >>{resources} >>"); + let path = dir.join(format!("mask-{fonts:?}.pdf")); + std::fs::write(&path, one_page("", &names, &content, &objects)).expect("writes"); + path +} + +#[test] +fn a_broken_picture_under_two_filters_fails_exports() { + let dir = tempfile::tempdir().expect("a temp dir"); + let path = broken_pictures(dir.path()); + let (mut worker, doc) = opened(&path); + assert!(worker.export_text(doc, &dir.path().join("doc.txt")).is_err(), "its text read whole"); + let (mut worker, doc) = opened(&path); + assert!(worker.export_image(doc, 0, 72.0, &dir.path().join("page.png")).is_err(), "its page read whole"); + let (mut worker, doc) = opened(&path); + let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws"); + assert!(tile.damaged, "the picture draws as damage"); +} + +#[test] +fn a_broken_picture_read_before_is_still_damage() { + let dir = tempfile::tempdir().expect("a temp dir"); + let files = [broken_pictures(dir.path()), bad_mask(dir.path(), None), bad_mask(dir.path(), Some(true)), bad_mask(dir.path(), Some(false))]; + // Reading caches the pictures; their repeat waits. + let read: [fn(&mut Worker, Doc); 2] = [|w, d| drop(w.text(d, 0).expect("reads")), |w, d| drop(w.search(d, 0, "x").expect("searches"))]; + for (path, first) in files.iter().flat_map(|p| read.map(|r| (p, r))) { + let (mut worker, doc) = opened(&path); + first(&mut worker, doc); + let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws"); + assert!(tile.damaged, "the picture draws as damage"); + assert!(worker.repair_report(doc).expect("asks").is_some()); + let (mut worker, doc) = opened(&path); + first(&mut worker, doc); + assert!(worker.export_image(doc, 0, 72.0, &dir.path().join("page.png")).is_err(), "its page read whole"); + let (mut worker, doc) = opened(&path); + first(&mut worker, doc); + assert!(worker.export_text(doc, &dir.path().join("doc.txt")).is_err(), "its text read whole"); + let (mut worker, doc) = opened(&path); + first(&mut worker, doc); + assert!(worker.check_pictures(doc).expect("checks"), "the check finds it"); + } +} diff --git a/tasks/TASKS.md b/tasks/TASKS.md index d0e2ac6..2c71d96 100644 --- a/tasks/TASKS.md +++ b/tasks/TASKS.md @@ -279,7 +279,7 @@ - [x] R7.1 合集安装包(§4,用户定):macOS 一个公证的 dmg 装六个 .app 与“应用程序”替身,用户拖入所要的;Windows 一个安装程序,组件页勾选六个应用,每个与单独安装程序装得一样;Linux 不做合集;单个应用的包照旧(`release.sh` 的 `dmg` 单个与合集共用,Mac mini 上以两个假应用核对卷内内容;`bundle.nsi` 逐个以 `/S` 运行所选的安装程序,某个退出码非零即停下并写明;`windows-bundle.sh` 三个假应用:都装上,第二个被拒时退出码 2、第三个不运行;真包随 R7.7 建出 — 第 1 轮:被拒的是最后一个,去掉 `Abort` 测试仍过;加第三个应用后去掉即失败;第 2 轮 PASS) - [x] R7.2 设置窗口多出一行应用菜单(#6,§5):设置窗口沿用主窗口的外壳 `Shell`,Windows 与 Linux 上随之画出菜单栏与更新横幅;设置窗口不画这两者(`Shell::secondary`;测试量内容起点距标题栏底,五种变体各自失败 — 第 1 轮:只查标记容器;第 2 轮:主窗口两者合计;第 3 轮 PASS) - [x] R7.3 装了宋体仍报缺少(#4,§7、§8):内核按 `宋体` 找到 SimSun,缺字检查却比较原名与所用字体的英文名,报为缺失;查明 Linux 上的版面是否真用 SimSun,一页变两页的根因(补丁 0048:缺字检查比较 `GetEnglishSearchFontName`;Windows 与 WSL 内核已重建;`navigate/tests.rs` 的 `DEJAVU SANS` 用例修前失败、修后通过,ElyDoc 单线程 55 项通过;Arch 容器装 0.1.5 包与 simsun.ttc:修前报缺宋体,正文实为 SimSun,一页;换上新内核不再报缺,仍一页。一页变两页未能复现,需报告者的文件 — 第 1 轮 PASS) -- [ ] R7.4 打开 PDF 总说已损坏(#5,§11):画图块时 MuPDF 的任何警告都算损坏,未嵌入的中日韩字体(“non-embedded font using identity encoding”)即触发;损坏只认结构修复、读不出的对象与流、画不出的图片与内容 +- [x] R7.4 打开 PDF 总说已损坏(#5,§11):画图块时 MuPDF 的任何警告都算损坏,未嵌入的中日韩字体(“non-embedded font using identity encoding”)即触发;损坏只认结构修复、读不出的对象与流、画不出的图片与内容 - [ ] R7.5 ElyMark 打开的正文块变成一块深色框(§14,用户定):按下的段落保持原样,不加底色与内边距,不挪位置;代码块仍有自己的底板 - [ ] R7.6 Windows 与 Linux 不能打印(#3,§19.3):`print_pdf` 只有 macOS;Windows 用系统打印对话框,Linux 经 xdg 桌面门户的打印接口 - [ ] R7.7 发布 0.1.6:版本号,三端建包、签名公证、上传,合集包一并上传,本机装上 diff --git a/tasks/reviews/R7.4-r1.md b/tasks/reviews/R7.4-r1.md new file mode 100644 index 0000000..4f50d06 --- /dev/null +++ b/tasks/reviews/R7.4-r1.md @@ -0,0 +1,5 @@ +FAIL + +1. crates/ely-pdf-engine/shim/ely_mupdf.c:253 — Tile rendering discards content errors while building the display list. The opening scan decodes streams but does not interpret their drawing commands. With a sound cross-reference table and content `BT 72 700 Td (Missing text) Tj ET`, MuPDF warns that it cannot draw the text, yet this change returns an entirely white tile with `damaged = false`, no repair report, and `check_pictures = false`. HEAD reports damage for the same file. Thus unreadable content loses its warning and protection against overwriting the original, contrary to R7.4 and PDF-14. `BT ] ET` also reproduces this regression; the new assertion in agents.md:314 that syntax errors only reach the error callback is false. Preserve content failures during display-list construction while excluding the harmless font notices specifically, add regressions for these cases, and correct that assertion. Windows comparison evidence: `.work/R7.4-r1-compare.log`. + +2. crates/ely-pdf-engine/shim/ely_mupdf.c:250; crates/ely-pdf-engine/shim/ely_output.c:60 — The unmonitored export pass caches images as well as fonts, so a second pass cannot reliably detect damage. Reproduction: a 4×4 RGB image with `/Filter [/FlateDecode /ASCIIHexDecode]`, containing zlib-compressed `ff0000` repeated 16 times followed by `>`, with its four checksum bytes replaced by zeros. MuPDF reports `ignoring zlib error: incorrect data check` during the first pass and caches the image; both PNG and text exports then return success without a repair report. Both exports reject this same fixture on HEAD. This silently accepts a failed stream read, contrary to R7.4 and the retained C11.8 export requirement; the new content-stream checksum test does not cover cached image streams. Retain and classify failures from the first pass instead of assuming every warning there is font substitution, and add a regression using this image filter chain. Windows comparison evidence: `.work/R7.4-r1-compare.log`. diff --git a/tasks/reviews/R7.4-r2.md b/tasks/reviews/R7.4-r2.md new file mode 100644 index 0000000..77bd5e5 --- /dev/null +++ b/tasks/reviews/R7.4-r2.md @@ -0,0 +1,3 @@ +FAIL + +1. crates/ely-pdf-engine/shim/ely_mupdf.c:86 — Flushing before installing the warning collector discards genuine damage found by an earlier unwatched text/search read. Reproduction: a PDF with a sound cross-reference table and two distinct 4×4 RGB image objects with `/Filter [/FlateDecode /ASCIIHexDecode]`, each containing zlib-compressed `ff0000` repeated 16 times followed by `>`, with its checksum replaced by four zero bytes. Call `text` or `search` before rendering: MuPDF caches the images and leaves a repeated checksum warning pending; the new flush discards it, so rendering returns `damaged = false` and no repair report. Text extraction followed by either PNG or text export also succeeds. HEAD reports damage and rejects those exports. A single RGB image with `/Mask [null null null null null null]` reproduces the same loss; after the text read, even `check_pictures` returns false, removing the overwrite protection required by PDF-14. Capture and classify warnings during the earlier reads and retain damage for the affected document, excluding only the harmless font notice and its repeats; add regressions that read/search damaged images before rendering, exporting, or checking them. Windows comparison evidence: `.work/R7.4-r2-compare.log`. diff --git a/tasks/reviews/R7.4-r3.md b/tasks/reviews/R7.4-r3.md new file mode 100644 index 0000000..fdbc19f --- /dev/null +++ b/tasks/reviews/R7.4-r3.md @@ -0,0 +1,3 @@ +FAIL + +1. crates/ely-pdf-engine/shim/ely_mupdf.c:89 — Seeding from only the last warning still loses genuine damage when an unwatched read reaches fonts after a damaged image. Reproduction: combine the new bad-mask fixture (4×4 RGB, `/Mask [null null null null null null]`, `/ASCIIHexDecode`, `ff0000` repeated 16 times followed by `>`) with the two separate unembedded SimSun fonts from `an_unembedded_font_is_no_damage`; draw the image before the two text runs. On fresh workers, call `text()` or `search()` before rendering, exporting, or checking pictures. The mask warning occurs six times, but the first font notice flushes those warnings outside the collector; only the font repeat remains pending. The new filter ignores that repeat, and the cached image raises no further warning: rendering returns `damaged = false`, the repair report is `None`, PNG and text exports succeed, and `check_pictures` returns false. HEAD detects damage and rejects both exports on this same fixture. This removes the damaged original's overwrite protection required by R7.4 and PDF-14. Capture and classify warnings during text/search reads and retain genuine damage for the document; add a regression combining the bad image and font notices in both drawing orders. Windows comparison and exact reproduction: `.work/R7.4-r3-compare.log`.