R7.4: an unembedded font is no damage (#5)
The warning watch skips MuPDF's non-embedded font notice and its repeat counts. Text and search reads are watched too: damage they meet is kept on the document, told by the next draw or picture check, and fails image and text exports until then. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
969cb67c96
commit
6b676e06e6
9 files changed
+191
-9
No files matched your search
@@ -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]` 下校验和错的图片使两种导出都被拒、图块为损坏。先读文字或搜索后,这样的两张图片、颜色键掩码无效的一张图片、以及它在两个未嵌入字体之前或之后画出,画图块、导出与检查图片仍报损坏。
|
||||
@@ -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;
|
||||
|
||||
@@ -31,6 +31,8 @@ struct Doc {
|
||||
/// What MuPDF warned at open; kept as the repair report.
|
||||
warnings: String,
|
||||
repair: Option<String>,
|
||||
/// Damage a read met that no draw has told yet.
|
||||
found: bool,
|
||||
/// The repaired copy standing in; removed on close.
|
||||
copy: Option<std::path::PathBuf>,
|
||||
/// 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()),
|
||||
|
||||
@@ -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<Vec<u8>>) -> io::Result<Vec<u8>> {
|
||||
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<bool> {
|
||||
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<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
||||
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)?;
|
||||
|
||||
@@ -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<bool>) -> 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");
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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:版本号,三端建包、签名公证、上传,合集包一并上传,本机装上
|
||||
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
Reference in new issue
Block a user