mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 11:17:10 -04:00
This commit is contained in:
1 parent
4b45fc9cb4
commit
549fa7ae04
2 files changed
+81
-10
No files matched your search
@@ -20,6 +20,9 @@ environment variable if you set one.
|
|||||||
Inter and JetBrains Mono fonts).
|
Inter and JetBrains Mono fonts).
|
||||||
2. **Chrome.** Headless Chrome prints the page to PDF with
|
2. **Chrome.** Headless Chrome prints the page to PDF with
|
||||||
`--headless=new --no-pdf-header-footer --generate-pdf-document-outline --print-to-pdf`.
|
`--headless=new --no-pdf-header-footer --generate-pdf-document-outline --print-to-pdf`.
|
||||||
|
It runs with a throwaway `--user-data-dir` and
|
||||||
|
`--disable-features=MacAppCodeSignClone`, so no code-sign clones are left
|
||||||
|
behind on macOS.
|
||||||
Skia produces 12 tagged pages with embedded font subsets. Variable
|
Skia produces 12 tagged pages with embedded font subsets. Variable
|
||||||
system-ui weights come out as Type 3 fonts and emoji as Type 3 bitmap
|
system-ui weights come out as Type 3 fonts and emoji as Type 3 bitmap
|
||||||
glyphs. The pages also get shadings, blend modes, soft masks, tiling
|
glyphs. The pages also get shadings, blend modes, soft masks, tiling
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::env;
|
|||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -80,13 +80,9 @@ pub fn print_to_pdf(chrome: &Path, html: &Path, pdf: &Path, log: &Path) -> Resul
|
|||||||
print_arg.push(pdf);
|
print_arg.push(pdf);
|
||||||
let log_file = fs::File::create(log).with_context(|| format!("creating {}", log.display()))?;
|
let log_file = fs::File::create(log).with_context(|| format!("creating {}", log.display()))?;
|
||||||
|
|
||||||
|
let profile = TempProfile::new();
|
||||||
let mut child = Command::new(chrome)
|
let mut child = Command::new(chrome)
|
||||||
.arg("--headless=new")
|
.args(chrome_args(&profile.0, print_arg, super::file_url(html)))
|
||||||
.arg("--disable-gpu")
|
|
||||||
.arg("--no-pdf-header-footer")
|
|
||||||
.arg("--generate-pdf-document-outline")
|
|
||||||
.arg(print_arg)
|
|
||||||
.arg(super::file_url(html))
|
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(log_file)
|
.stderr(log_file)
|
||||||
.spawn()
|
.spawn()
|
||||||
@@ -109,8 +105,7 @@ pub fn print_to_pdf(chrome: &Path, html: &Path, pdf: &Path, log: &Path) -> Resul
|
|||||||
if len == last_len && len.is_some() && is_complete(pdf) {
|
if len == last_len && len.is_some() && is_complete(pdf) {
|
||||||
quiet_polls += 1;
|
quiet_polls += 1;
|
||||||
if quiet_polls >= 10 {
|
if quiet_polls >= 10 {
|
||||||
let _ = child.kill();
|
stop(&mut child);
|
||||||
let _ = child.wait();
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -118,14 +113,87 @@ pub fn print_to_pdf(chrome: &Path, html: &Path, pdf: &Path, log: &Path) -> Resul
|
|||||||
}
|
}
|
||||||
last_len = len;
|
last_len = len;
|
||||||
if started.elapsed() > Duration::from_secs(120) {
|
if started.elapsed() > Duration::from_secs(120) {
|
||||||
let _ = child.kill();
|
stop(&mut child);
|
||||||
bail!("Chrome timed out printing {}; see {}", html.display(), log.display());
|
bail!("Chrome timed out printing {}; see {}", html.display(), log.display());
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(500));
|
thread::sleep(Duration::from_millis(500));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Command-line arguments for a headless print. `MacAppCodeSignClone` is disabled because on macOS
|
||||||
|
/// every launch otherwise makes a code-sign clone of the app under `/private/var/folders`, and a
|
||||||
|
/// browser that is killed never removes it. The profile is a throwaway directory.
|
||||||
|
fn chrome_args(profile: &Path, print_arg: OsString, url: impl Into<OsString>) -> Vec<OsString> {
|
||||||
|
let mut user_data = OsString::from("--user-data-dir=");
|
||||||
|
user_data.push(profile);
|
||||||
|
vec![
|
||||||
|
"--headless=new".into(),
|
||||||
|
"--disable-gpu".into(),
|
||||||
|
"--disable-features=MacAppCodeSignClone".into(),
|
||||||
|
user_data,
|
||||||
|
"--no-pdf-header-footer".into(),
|
||||||
|
"--generate-pdf-document-outline".into(),
|
||||||
|
print_arg,
|
||||||
|
url.into(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask Chrome to quit (SIGTERM on Unix) so it can clean up after itself, and kill it only if it
|
||||||
|
/// is still running after a few seconds.
|
||||||
|
fn stop(child: &mut Child) {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let _ = Command::new("kill").arg("-TERM").arg(child.id().to_string()).status();
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(5);
|
||||||
|
while Instant::now() < deadline {
|
||||||
|
if matches!(child.try_wait(), Ok(Some(_))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A throwaway Chrome profile directory, removed when dropped (also on error paths).
|
||||||
|
struct TempProfile(PathBuf);
|
||||||
|
|
||||||
|
impl TempProfile {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self(env::temp_dir().join(format!("pdfcraft-chrome-profile-{}", std::process::id())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempProfile {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = fs::remove_dir_all(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A PDF is complete once its trailer's `%%EOF` marker has been written.
|
/// A PDF is complete once its trailer's `%%EOF` marker has been written.
|
||||||
fn is_complete(pdf: &Path) -> bool {
|
fn is_complete(pdf: &Path) -> bool {
|
||||||
fs::read(pdf).map(|bytes| bytes.len() > 1024 && bytes[bytes.len() - 1024..].windows(5).any(|w| w == b"%%EOF")).unwrap_or(false)
|
fs::read(pdf).map(|bytes| bytes.len() > 1024 && bytes[bytes.len() - 1024..].windows(5).any(|w| w == b"%%EOF")).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn args_disable_code_sign_clone_and_use_throwaway_profile() {
|
||||||
|
let args = chrome_args(Path::new("/tmp/prof"), OsString::from("--print-to-pdf=out.pdf"), "file:///a.html");
|
||||||
|
assert!(args.iter().any(|a| a == "--disable-features=MacAppCodeSignClone"));
|
||||||
|
assert!(args.iter().any(|a| a == "--user-data-dir=/tmp/prof"));
|
||||||
|
assert_eq!(args.last().map(|a| a.as_os_str()), Some(std::ffi::OsStr::new("file:///a.html")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn temp_profile_is_removed_on_drop() {
|
||||||
|
let profile = TempProfile::new();
|
||||||
|
let dir = profile.0.clone();
|
||||||
|
fs::create_dir_all(dir.join("Default")).unwrap();
|
||||||
|
drop(profile);
|
||||||
|
assert!(!dir.exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user