C14.14: export HTML, HTML and folder, PDF (MuPDF story in the PDF worker) and plain text; print; rich copy

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-12 10:16:11 -04:00
1 parent f1cb7eff99
commit d7ded183ae
27 files changed
+1427 -24

No files matched your search

+2
View File
@@ -26,6 +26,7 @@ fn main() {
"shim/ely_secure.c",
"shim/ely_sign.c",
"shim/ely_output.c",
"shim/ely_story.c",
"shim/ely_shim.h",
"src/proto.rs",
"src/bin/ely-pdf-worker/main.rs",
@@ -86,6 +87,7 @@ fn main() {
.file("shim/ely_secure.c")
.file("shim/ely_sign.c")
.file("shim/ely_output.c")
.file("shim/ely_story.c")
.include(root.join("reference/mupdf/include"))
.include(openssl(&root, "includedir"))
.warnings(true)
+53
View File
@@ -0,0 +1,53 @@
/* A folder's HTML page as a PDF (MARK-11). */
#include "ely_shim.h"
/* A4, two centimetres in from each edge. */
static const fz_rect sheet = {0, 0, 595.28f, 841.89f};
static const fz_rect area = {56.69f, 56.69f, 538.59f, 785.2f};
int ely_html_pdf(fz_context *ctx, const char *dir, const char *page, const char *out)
{
fz_archive *folder = NULL;
fz_buffer *html = NULL;
fz_story *story = NULL;
fz_document_writer *writer = NULL;
fz_var(folder);
fz_var(html);
fz_var(story);
fz_var(writer);
fz_try(ctx)
{
ely_watch_begin(ctx);
folder = fz_open_directory(ctx, dir);
html = fz_read_archive_entry(ctx, folder, page);
story = fz_new_story(ctx, html, NULL, 11, folder);
writer = fz_new_pdf_writer(ctx, out, "");
int more;
do
{
fz_device *dev = fz_begin_page(ctx, writer, sheet);
more = fz_place_story(ctx, story, area, NULL);
fz_draw_story(ctx, story, dev, fz_identity);
fz_end_page(ctx, writer);
} while (more);
fz_close_document_writer(ctx, writer);
/* The story keeps its own warnings. */
const char *said = fz_story_warnings(ctx, story);
if (said && said[0])
fz_throw(ctx, FZ_ERROR_FORMAT, "the page did not set whole: %s", said);
if (ely_watch_end(ctx))
fz_throw(ctx, FZ_ERROR_FORMAT, "the page did not set whole: %s", ely_open_warnings());
}
fz_always(ctx)
{
ely_watch_end(ctx);
fz_drop_document_writer(ctx, writer);
fz_drop_story(ctx, story);
fz_drop_buffer(ctx, html);
fz_drop_archive(ctx, folder);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
@@ -331,7 +331,7 @@ impl Worker {
// A document request names the state it was made for.
let stamped;
let payload = match kind {
tag::OPEN | tag::HELLO => payload,
tag::OPEN | tag::HELLO | tag::HTML_PDF => payload,
_ => {
stamped = self.current(payload)?;
&stamped
@@ -415,6 +415,10 @@ impl Worker {
}
tag::OUTLINE => self.mupdf.outline(self.doc(ask.u64()?)?),
tag::HELLO => Ok(Pack::new().str(ely_pdf_engine::proto::BUILD).bytes()),
tag::HTML_PDF => {
let (dir, page, out) = (ask.str()?, ask.str()?, ask.str()?);
self.mupdf.html_pdf(&dir, &page, &out).map(|()| Vec::new())
}
tag::CLOSE => {
self.forget(ask.u64()?)?;
Ok(Vec::new())
@@ -34,6 +34,7 @@ unsafe extern "C" {
fn ely_watch_end(ctx: *mut Context) -> c_int;
fn ely_check_pictures(ctx: *mut Context, doc: *mut Document) -> c_int;
fn ely_print_copy(ctx: *mut Context, doc: *mut Document, raw: *const c_char, out: *const c_char, password: *const c_char) -> c_int;
fn ely_html_pdf(ctx: *mut Context, dir: *const c_char, page: *const c_char, out: *const c_char) -> c_int;
}
/// What MuPDF warned while opening the last file.
@@ -56,6 +57,16 @@ impl Mupdf {
done.map(|_| ())
}
/// `page` in `dir` set as a PDF at `out`.
pub fn html_pdf(&self, dir: &str, page: &str, out: &str) -> io::Result<()> {
let (dir_c, page_c, out_c) = (text(dir)?, text(page)?, text(out)?);
let done = checked(unsafe { ely_html_pdf(self.ctx, dir_c.as_ptr(), page_c.as_ptr(), out_c.as_ptr()) });
if done.is_err() {
clean(out.as_ref());
}
done.map(|_| log::info!("pdf-worker: set an HTML page as a PDF"))
}
pub fn repaired(&self, doc: &Open) -> io::Result<bool> {
checked(unsafe { ely_repaired(self.ctx, doc.0) }).map(|repaired| repaired != 0)
}
+7 -1
View File
@@ -2,7 +2,7 @@
use std::{io, path::Path};
use ely_ipc::Unpack;
use ely_ipc::{Pack, Unpack};
use crate::client::{Doc, Worker};
use crate::proto::tag;
@@ -42,6 +42,12 @@ impl Worker {
self.request(tag::PRINT_COPY, &self.whole(&doc)?.str(utf8(out)?).bytes()).map(|_| ())
}
/// HTML `page` in `dir`, with its pictures, as a PDF.
pub fn html_pdf(&mut self, dir: &Path, page: &str, out: &Path) -> io::Result<()> {
let ask = Pack::new().str(utf8(dir)?).str(page).str(utf8(out)?);
self.request(tag::HTML_PDF, &ask.bytes()).map(|_| ())
}
/// Decodes every picture; true if any failed.
pub fn check_pictures(&mut self, doc: Doc) -> io::Result<bool> {
let reply = self.request(tag::CHECK_PICTURES, &self.whole(&doc)?.bytes())?;
+2
View File
@@ -81,6 +81,8 @@ pub mod tag {
pub const REPAIR_REPORT: u8 = 58;
/// Decodes every picture; answers whether any failed.
pub const CHECK_PICTURES: u8 = 59;
/// A folder's HTML page as a new PDF; no document.
pub const HTML_PDF: u8 = 60;
pub const OK: u8 = 100;
pub const ERR: u8 = 101;
}
+78
View File
@@ -0,0 +1,78 @@
//! An HTML folder set as a PDF (MARK-11).
use std::path::Path;
use ely_pdf_engine::Worker;
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
/// A 1×1 red PNG.
const DOT: &[u8] = &[
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08,
0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xdd, 0x8d, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45,
0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
];
fn page(dir: &Path, body: &str) {
let html = format!("<!doctype html><html><head><meta charset=\"utf-8\"></head><body>{body}</body></html>");
std::fs::write(dir.join("page.html"), html).expect("writes");
}
/// The PDF's pages and its first page's text.
fn read(worker: &mut Worker, pdf: &Path) -> (usize, String) {
let doc = worker.open(pdf).expect("opens").doc;
let pages = worker.pages(doc).expect("pages");
let text: String = worker.text(doc, 0).expect("text").iter().map(|ch| ch.c).collect();
worker.close(doc).expect("closes");
(pages, text)
}
#[test]
fn a_page_with_pictures_and_cjk_sets_as_a_pdf() {
let dir = tempfile::tempdir().expect("a temp dir");
std::fs::create_dir(dir.path().join("files")).expect("makes");
std::fs::write(dir.path().join("files/dot.png"), DOT).expect("writes");
let svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"10\"><path d=\"M0 0H20V10Z\"/></svg>";
page(dir.path(), &format!("<h1>Title</h1><p>中文段落 {svg}</p><p><img src=\"files/dot.png\" alt=\"dot\"></p>"));
let pdf = dir.path().join("out.pdf");
let mut worker = worker();
worker.html_pdf(dir.path(), "page.html", &pdf).expect("sets");
let (pages, text) = read(&mut worker, &pdf);
assert_eq!(pages, 1);
assert!(text.contains("Title") && text.contains("中文段落"), "{text:?}");
}
#[test]
fn a_long_page_runs_on() {
let dir = tempfile::tempdir().expect("a temp dir");
let body: String = (0..300).map(|n| format!("<p>Paragraph {n}.</p>")).collect();
page(dir.path(), &body);
let pdf = dir.path().join("out.pdf");
let mut worker = worker();
worker.html_pdf(dir.path(), "page.html", &pdf).expect("sets");
assert!(read(&mut worker, &pdf).0 > 3);
}
#[test]
fn a_missing_picture_fails_and_writes_nothing() {
let dir = tempfile::tempdir().expect("a temp dir");
page(dir.path(), "<p><img src=\"files/gone.png\" alt=\"gone\"></p>");
let pdf = dir.path().join("out.pdf");
let error = worker().html_pdf(dir.path(), "page.html", &pdf).expect_err("refused");
assert!(error.to_string().contains("did not set whole"), "{error}");
assert!(!pdf.exists());
}
#[test]
fn a_picture_taller_than_a_page_still_sets() {
let dir = tempfile::tempdir().expect("a temp dir");
let svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100\" height=\"3000\"><path d=\"M0 0H100V3000Z\"/></svg>";
page(dir.path(), &format!("<p>Before</p>{svg}<p>After</p>"));
let pdf = dir.path().join("out.pdf");
let mut worker = worker();
worker.html_pdf(dir.path(), "page.html", &pdf).expect("sets");
assert!(read(&mut worker, &pdf).0 >= 1);
}
+10 -2
View File
@@ -56,7 +56,11 @@ pub(super) const EN: &[(&str, &str)] = &[
("mark.export.done", "Exported {name}"),
("mark.export.missing", "Exported without these images: {list}."),
("mark.export.diagrams", "Diagrams that did not draw stay as code, at lines {list}."),
("mark.export.formula", "The formula at line {line} did not render: {error}."),
("mark.export.formulas", "Formulas that did not render stay as code: {list}."),
("mark.export.formula", "line {line}, {error}"),
("mark.export.folder_taken", "{name} is already there; the pictures need that folder."),
("mark.copy.rich", "Copy as Rich Text"),
("mark.copy.failed", "Copying failed: {error}"),
("mark.export.failed", "The export failed: {error}"),
("mark.look.focus", "Focus Mode"),
("mark.look.typewriter", "Typewriter Scrolling"),
@@ -149,7 +153,11 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[
("mark.export.done", "已导出 {name}"),
("mark.export.missing", "已导出,缺这些图片:{list}。"),
("mark.export.diagrams", "没能画出的图表保留为代码,在第 {list} 行。"),
("mark.export.formula", "第 {line} 行的公式没能排出:{error}。"),
("mark.export.formulas", "没能排出的公式保留为代码:{list}。"),
("mark.export.formula", "第 {line} 行,{error}"),
("mark.export.folder_taken", "{name} 已存在;图片要放进这个文件夹。"),
("mark.copy.rich", "复制为富文本"),
("mark.copy.failed", "复制失败:{error}"),
("mark.export.failed", "导出失败:{error}"),
("mark.look.focus", "专注模式"),
("mark.look.typewriter", "打字机滚动"),