mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 18:59:48 +00:00
d30bb111db
A tab named in Arabic showed replacement boxes: no interface font had the glyphs, and egui joins the letters of a right-to-left word but places the runs in logical order. - Fonts: the interface takes `Arab` faces from the craft-fonts build input (also on wasm), and on desktop adds one font already installed on the machine as the last fallback of every family. It is read at runtime, never embedded; PDFCRAFT_SYSTEM_FONTS=0 turns it off and `cargo xtask screenshots` sets that. AGENTS.md §1.4 records the rule. - Order: bidi::visual puts single-line text in display order with a left-to-right base, used for the painted tab label and hover text, the recent-files name and path, and the Properties File and Location rows. Accessibility names keep the logical text. misc.rtl-ui stays planned: multi-line text, text input and a mirrored layout are not done. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
6.6 KiB
Rust
126 lines
6.6 KiB
Rust
//! `cargo xtask screenshots [name…]`: regenerate the README screenshots in `docs/images/`.
|
|
//!
|
|
//! Every image is a headless render of the real app (`examples/shot.rs`) showing the showcase
|
|
//! PDF. That PDF is built only from the OFL fonts and generated artwork in ATTRIBUTION.toml, so
|
|
//! the screenshots are contributor-original and policy-compliant (AGENTS.md §1). Afterwards the
|
|
//! `docs/images/*.png` entries in ATTRIBUTION.toml are rewritten with fresh hashes, and
|
|
//! ATTRIBUTION.md is regenerated.
|
|
//!
|
|
//! The document is opened by a *relative* path so no local directory names (user names) can
|
|
//! appear in a screenshot.
|
|
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
|
|
/// (file stem, title, what it shows, shot arguments after the PDF path).
|
|
pub const SCENES: &[(&str, &str, &str, &[&str])] = &[
|
|
("pdfcraft-viewer", "Viewer with comments", "README hero", &["--panel", "comments", "--zoom", "72"]),
|
|
("pdfcraft-dark", "Dark theme", "README: themes", &["--theme", "dark", "--panel", "comments", "--page", "11", "--zoom", "80", "--notice", "off"]),
|
|
("pdfcraft-organize", "Organize pages", "README: organize", &["--organize", "on", "--panel", "none", "--select", "5,6,7"]),
|
|
("pdfcraft-find", "Find text", "README: search", &["--find", "type", "--page", "4", "--zoom", "90", "--panel", "none", "--notice", "off"]),
|
|
("pdfcraft-bookmarks", "Bookmarks", "README: navigation", &["--panel", "bookmarks", "--page", "7", "--zoom", "75", "--notice", "off"]),
|
|
("pdfcraft-forms", "Interactive form", "README: forms", &["--page", "13", "--fields", "on", "--zoom", "80", "--panel", "fields"]),
|
|
("pdfcraft-layers", "Layers", "README: layers", &["--panel", "layers", "--page", "12", "--zoom", "75", "--notice", "off"]),
|
|
(
|
|
"pdfcraft-scripts",
|
|
"Scripts of the world",
|
|
"README: rendering",
|
|
&["--page", "7", "--zoom", "125", "--left", "closed", "--panel", "none", "--notice", "off"],
|
|
),
|
|
("pdfcraft-properties", "Document properties", "README: metadata", &["--dialog", "properties", "--notice", "off"]),
|
|
("pdfcraft-palette", "Command palette", "README: tools", &["--palette", "page", "--notice", "off"]),
|
|
("pdfcraft-split", "Split dialog", "README: split", &["--organize", "on", "--panel", "none", "--dialog", "split", "--notice", "off"]),
|
|
("pdfcraft-tools", "All tools", "README: tool catalogue", &["--tools", "expanded", "--home", "on"]),
|
|
(
|
|
"pdfcraft-twoup",
|
|
"Two-up reading",
|
|
"README: layouts",
|
|
&["--layout", "two-up", "--mode", "read", "--page", "4", "--theme", "dark", "--notice", "off"],
|
|
),
|
|
];
|
|
|
|
const PDF: &str = "dist/demo/pdfcraft-showcase.pdf";
|
|
|
|
pub fn run(args: &[String]) -> Result<()> {
|
|
let root = crate::assets::root();
|
|
if !root.join(PDF).is_file() {
|
|
println!("screenshots: building the showcase PDF first");
|
|
crate::demo_pdf::run(&[])?;
|
|
}
|
|
let status = Command::new(env!("CARGO"))
|
|
.args(["build", "--release", "-p", "pdfcraft-ui-egui", "--example", "shot"])
|
|
.current_dir(&root)
|
|
.status()
|
|
.context("building the shot example")?;
|
|
if !status.success() {
|
|
bail!("building the shot example failed");
|
|
}
|
|
let target = std::env::var("CARGO_TARGET_DIR").map(std::path::PathBuf::from).unwrap_or_else(|_| root.join("target"));
|
|
let shot = target.join("release/examples").join(if cfg!(windows) { "shot.exe" } else { "shot" });
|
|
std::fs::create_dir_all(root.join("docs/images"))?;
|
|
for (name, _, _, scene) in SCENES {
|
|
if !args.is_empty() && !args.iter().any(|a| a == name) {
|
|
continue;
|
|
}
|
|
let out = format!("docs/images/{name}.png");
|
|
let status = Command::new(&shot)
|
|
.arg(&out)
|
|
.arg(PDF)
|
|
.args(["--size", "1440x900", "--scale", "2", "--width", "1600"])
|
|
.args(*scene)
|
|
// Published images show only the embedded, openly licensed fonts (AGENTS.md §1.2).
|
|
.env("PDFCRAFT_SYSTEM_FONTS", "0")
|
|
.current_dir(&root)
|
|
.status()
|
|
.with_context(|| format!("running shot for {name}"))?;
|
|
if !status.success() {
|
|
bail!("screenshot {name} failed");
|
|
}
|
|
}
|
|
update_attribution(&root)?;
|
|
crate::assets::run(&["--write".to_string()])
|
|
}
|
|
|
|
/// Replace the `docs/images/` entries of ATTRIBUTION.toml with one per screenshot on disk.
|
|
fn update_attribution(root: &Path) -> Result<()> {
|
|
let path = root.join("ATTRIBUTION.toml");
|
|
let text = std::fs::read_to_string(&path)?;
|
|
// Split into the header and `[[…]]` blocks; drop existing screenshot blocks.
|
|
let mut parts: Vec<String> = Vec::new();
|
|
for (i, block) in text.split("\n[[").enumerate() {
|
|
let block = if i == 0 { block.to_string() } else { format!("[[{block}") };
|
|
if block.starts_with("[[asset]]") && block.contains("path = \"docs/images/") {
|
|
continue;
|
|
}
|
|
parts.push(block);
|
|
}
|
|
let mut out = parts.join("\n");
|
|
if !out.ends_with('\n') {
|
|
out.push('\n');
|
|
}
|
|
let mut images: Vec<_> = std::fs::read_dir(root.join("docs/images"))?.flatten().map(|e| e.file_name().to_string_lossy().into_owned()).collect();
|
|
images.sort();
|
|
for file in images.iter().filter(|f| f.ends_with(".png")) {
|
|
let stem = file.trim_end_matches(".png");
|
|
let scene = SCENES.iter().find(|(n, ..)| *n == stem);
|
|
let (title, usage) = scene.map(|(_, t, u, _)| (*t, *u)).unwrap_or(("PdfCraft screenshot", "README"));
|
|
// The home screen shows the ArtCraft mark: say so (it is a Storyteller trademark, not open
|
|
// source; AGENTS.md §1.2), so the screenshot's licence is not read as covering it.
|
|
let shows_home = scene.is_some_and(|(.., args)| args.windows(2).any(|w| w[0] == "--home" && w[1] == "on"));
|
|
let contents = if shows_home {
|
|
"Shows PdfCraft UI, Lucide icons, the showcase PDF built from the [[fetched]] OFL fonts, and the ArtCraft mark (Storyteller trademark from docs/brand/, not covered by this licence)"
|
|
} else {
|
|
"Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts"
|
|
};
|
|
let bytes = std::fs::read(root.join("docs/images").join(file))?;
|
|
out.push_str(&format!(
|
|
"\n[[asset]]\npath = \"docs/images/{file}\"\ntitle = \"PdfCraft screenshot: {title}\"\nauthor = \"PdfCraft contributors (generated by `cargo xtask screenshots`)\"\nsource = \"this repository\"\nlicence = \"MIT OR Apache-2.0\"\nlicence_file = \"LICENSE-MIT\"\nkind = \"image\"\nusage = \"{usage}. {contents}\"\nsha256 = \"{}\"\n",
|
|
crate::assets::sha256_hex(&bytes)
|
|
));
|
|
}
|
|
std::fs::write(&path, out)?;
|
|
Ok(())
|
|
}
|