#![allow(dead_code)]
use std::path::{Path, PathBuf};
use lopdf::{Dictionary, Document, Object, ObjectId, Stream, dictionary};
use serde_json::{Value, json};
pub fn call(tool: &str, args: Value) -> Value {
pdfops::tools::call(tool, args).unwrap_or_else(|e| panic!("{tool} failed: {e:#}"))
}
pub fn call_err(tool: &str, args: Value) -> String {
format!(
"{:#}",
pdfops::tools::call(tool, args).expect_err("expected an error")
)
}
pub fn page_count(path: &Path) -> u64 {
call("pdf_info", json!({"input": path}))["pages"]
.as_u64()
.unwrap()
}
pub fn texts(path: &Path) -> Vec<String> {
let v = call("pdf_text", json!({"input": path}));
v["pages"]
.as_array()
.unwrap()
.iter()
.map(|p| p["text"].as_str().unwrap().to_string())
.collect()
}
fn skeleton(doc: &mut Document, pages_id: ObjectId, kids: Vec<Object>, extra: Dictionary) {
let font = doc.add_object(dictionary! {
"Type" => "Font", "Subtype" => "Type1", "BaseFont" => "Helvetica", "Encoding" => "WinAnsiEncoding",
});
doc.objects.insert(
pages_id,
Object::Dictionary(dictionary! {
"Type" => "Pages",
"Count" => kids.len() as i64,
"Kids" => kids,
"MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
"Resources" => dictionary! { "Font" => dictionary! { "F1" => font } },
}),
);
let mut catalog = dictionary! { "Type" => "Catalog", "Pages" => pages_id };
catalog.extend(&extra);
let catalog = doc.add_object(catalog);
doc.trailer.set("Root", catalog);
let info = doc.add_object(dictionary! { "Title" => Object::string_literal("Sample"), "Author" => Object::string_literal("Ada") });
doc.trailer.set("Info", info);
}
fn text_page(doc: &mut Document, pages_id: ObjectId, text: &str) -> ObjectId {
content_page(
doc,
pages_id,
&format!("BT /F1 18 Tf 72 720 Td ({text}) Tj ET"),
)
}
fn content_page(doc: &mut Document, pages_id: ObjectId, content: &str) -> ObjectId {
let content = doc.add_object(Stream::new(Dictionary::new(), content.as_bytes().to_vec()));
doc.add_object(dictionary! { "Type" => "Page", "Parent" => pages_id, "Contents" => content })
}
pub fn sample(dir: &Path, name: &str, pages: u32) -> PathBuf {
let mut doc = Document::with_version("1.5");
let pages_id = doc.new_object_id();
let ids: Vec<ObjectId> = (1..=pages)
.map(|n| {
text_page(
&mut doc,
pages_id,
&format!("Page {n} of the sample. Keyword alpha-{n}."),
)
})
.collect();
let mut extra = Dictionary::new();
if pages >= 2 {
let outlines = doc.new_object_id();
let item = doc.add_object(dictionary! {
"Title" => Object::string_literal("Second chapter"),
"Parent" => outlines,
"Dest" => vec![ids[1].into(), "Fit".into()],
});
doc.objects.insert(
outlines,
Object::Dictionary(
dictionary! { "Type" => "Outlines", "First" => item, "Last" => item, "Count" => 1 },
),
);
extra.set("Outlines", outlines);
}
skeleton(
&mut doc,
pages_id,
ids.into_iter().map(Object::from).collect(),
extra,
);
let path = dir.join(name);
doc.save(&path).unwrap();
path
}
pub fn form(dir: &Path) -> PathBuf {
let mut doc = Document::with_version("1.5");
let pages_id = doc.new_object_id();
let page = content_page(
&mut doc,
pages_id,
"BT /F1 18 Tf 72 720 Td (Registration form) Tj ET q 20 0 0 20 400 700 cm /Im1 Do Q",
);
let image = doc.add_object(Stream::new(
dictionary! {
"Type" => "XObject", "Subtype" => "Image", "Width" => 2, "Height" => 2,
"ColorSpace" => "DeviceRGB", "BitsPerComponent" => 8,
},
vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255],
));
let font = doc.add_object(
dictionary! { "Type" => "Font", "Subtype" => "Type1", "BaseFont" => "Helvetica" },
);
let on = doc.add_object(Stream::new(dictionary! { "Type" => "XObject", "Subtype" => "Form", "BBox" => vec![0.into(), 0.into(), 12.into(), 12.into()] }, b"0 0 12 12 re f".to_vec()));
let off = doc.add_object(Stream::new(dictionary! { "Type" => "XObject", "Subtype" => "Form", "BBox" => vec![0.into(), 0.into(), 12.into(), 12.into()] }, Vec::new()));
let widget = |ft: &str, name: &str, rect: [i64; 4]| {
dictionary! {
"Type" => "Annot", "Subtype" => "Widget", "FT" => ft, "T" => Object::string_literal(name), "P" => page,
"Rect" => rect.iter().map(|&v| v.into()).collect::<Vec<Object>>(),
}
};
let name = doc.add_object(widget("Tx", "name", [72, 600, 300, 620]));
let mut agree = widget("Btn", "agree", [72, 560, 84, 572]);
agree.set("V", "Off");
agree.set("AS", "Off");
agree.set(
"AP",
dictionary! { "N" => dictionary! { "Yes" => on, "Off" => off } },
);
let agree = doc.add_object(agree);
let mut color = widget("Ch", "color", [72, 520, 300, 540]);
color.set(
"Opt",
vec![
Object::string_literal("red"),
Object::string_literal("green"),
],
);
let color = doc.add_object(color);
let mut notes = widget("Tx", "notes", [72, 400, 300, 500]);
notes.set("Ff", 4096);
let notes = doc.add_object(notes);
let fields: Vec<Object> = vec![name.into(), agree.into(), color.into(), notes.into()];
doc.get_dictionary_mut(page)
.unwrap()
.set("Annots", fields.clone());
doc.get_dictionary_mut(page).unwrap().set(
"Resources",
dictionary! { "Font" => dictionary! { "F1" => font }, "XObject" => dictionary! { "Im1" => image } },
);
let mut extra = Dictionary::new();
extra.set(
"AcroForm",
dictionary! {
"Fields" => fields,
"DA" => Object::string_literal("/Helv 10 Tf 0 g"),
"DR" => dictionary! { "Font" => dictionary! { "Helv" => font } },
},
);
skeleton(&mut doc, pages_id, vec![page.into()], extra);
let path = dir.join("form.pdf");
doc.save(&path).unwrap();
path
}
pub fn with_images(dir: &Path, name: &str, images: Vec<Stream>) -> PathBuf {
let mut doc = Document::with_version("1.5");
let pages_id = doc.new_object_id();
let mut xobjects = Dictionary::new();
let mut content = String::new();
for (i, image) in images.into_iter().enumerate() {
let id = doc.add_object(image);
xobjects.set(format!("Im{i}"), id);
content += &format!("q 100 0 0 100 {} 600 cm /Im{i} Do Q\n", 50 + i * 120);
}
let page = content_page(&mut doc, pages_id, &content);
doc.get_dictionary_mut(page)
.unwrap()
.set("Resources", dictionary! { "XObject" => xobjects });
skeleton(&mut doc, pages_id, vec![page.into()], Dictionary::new());
let path = dir.join(name);
doc.save(&path).unwrap();
path
}
pub fn image_stream(
width: i64,
height: i64,
color_space: Object,
bits: i64,
data: Vec<u8>,
) -> Stream {
Stream::new(
dictionary! {
"Type" => "XObject", "Subtype" => "Image", "Width" => width, "Height" => height,
"ColorSpace" => color_space, "BitsPerComponent" => bits,
},
data,
)
}
pub fn scan(dir: &Path, source: &Path) -> PathBuf {
let v = call(
"pdf_render",
json!({"input": source, "out_dir": dir.join("scan"), "pages": "1", "dpi": 150}),
);
let bytes = std::fs::read(v["files"][0]["file"].as_str().unwrap()).unwrap();
let mut reader = png::Decoder::new(std::io::Cursor::new(bytes))
.read_info()
.unwrap();
let mut buf = vec![0; reader.output_buffer_size().unwrap()];
let info = reader.next_frame(&mut buf).unwrap();
let channels = buf.len() / (info.width * info.height) as usize;
let gray: Vec<u8> = buf.chunks(channels).map(|p| p[0]).collect();
let mut doc = Document::with_version("1.5");
let pages_id = doc.new_object_id();
let image = doc.add_object(image_stream(
info.width as i64,
info.height as i64,
"DeviceGray".into(),
8,
gray,
));
let page = content_page(&mut doc, pages_id, "q 612 0 0 792 0 0 cm /Im1 Do Q");
doc.get_dictionary_mut(page).unwrap().set(
"Resources",
dictionary! { "XObject" => dictionary! { "Im1" => image } },
);
skeleton(&mut doc, pages_id, vec![page.into()], Dictionary::new());
let path = dir.join("scan.pdf");
doc.save(&path).unwrap();
path
}
pub fn ocr_lang() -> Option<String> {
let out = std::process::Command::new("tesseract")
.arg("--list-langs")
.output()
.ok()?;
let text = String::from_utf8([out.stdout, out.stderr].concat()).ok()?;
let langs: Vec<&str> = text
.lines()
.skip(1)
.map(str::trim)
.filter(|l| !l.is_empty() && *l != "osd")
.collect();
langs
.iter()
.find(|l| **l == "eng")
.or(langs.first())
.map(|l| l.to_string())
}
pub fn serve(routes: Vec<(&'static str, Vec<u8>)>) -> String {
use std::io::{Read, Write};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let base = format!("http://{}", listener.local_addr().unwrap());
std::thread::spawn(move || {
for mut stream in listener.incoming().flatten() {
let mut request = Vec::new();
let mut byte = [0u8; 1];
while !request.ends_with(b"\r\n\r\n") && stream.read(&mut byte).is_ok_and(|n| n == 1) {
request.push(byte[0]);
}
let path = String::from_utf8_lossy(&request)
.split_whitespace()
.nth(1)
.unwrap_or("")
.to_string();
let reply = match routes.iter().find(|r| r.0 == path) {
Some((_, body)) => {
let mut out = format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
)
.into_bytes();
out.extend_from_slice(body);
out
}
None => b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
.to_vec(),
};
let _ = stream.write_all(&reply);
}
});
base
}
pub fn ink<const N: usize>(
pdf: &std::path::Path,
dir: &std::path::Path,
rects: [[usize; 4]; N],
) -> [usize; N] {
let v = call(
"pdf_render",
json!({"input": pdf, "out_dir": dir, "dpi": 72}),
);
let bytes = std::fs::read(v["files"][0]["file"].as_str().unwrap()).unwrap();
let mut reader = png::Decoder::new(std::io::Cursor::new(bytes))
.read_info()
.unwrap();
let mut buf = vec![0; reader.output_buffer_size().unwrap()];
let info = reader.next_frame(&mut buf).unwrap();
let (width, height) = (info.width as usize, info.height as usize);
let channels = buf.len() / (width * height);
rects.map(|[x0, y0, x1, y1]| {
(height - y1..height - y0)
.flat_map(|y| (x0..x1).map(move |x| (x, y)))
.filter(|&(x, y)| buf[(y * width + x) * channels] < 128)
.count()
})
}
pub fn custom(dir: &Path, name: &str, contents: &[&str]) -> PathBuf {
let mut doc = Document::with_version("1.5");
let pages_id = doc.new_object_id();
let ids: Vec<Object> = contents
.iter()
.map(|c| content_page(&mut doc, pages_id, c).into())
.collect();
skeleton(&mut doc, pages_id, ids, Dictionary::new());
let path = dir.join(name);
doc.save(&path).unwrap();
path
}
pub fn content_of(path: &Path, page: u32) -> String {
let doc = Document::load(path).unwrap();
let id = doc.get_pages()[&page];
String::from_utf8_lossy(&doc.get_page_content(id).unwrap()).into_owned()
}
pub fn words(path: &Path, page: u32) -> Vec<(String, [f64; 4])> {
let v = call(
"pdf_layout",
json!({"input": path, "pages": page.to_string(), "level": "words"}),
);
v["pages"][0]["words"]
.as_array()
.unwrap()
.iter()
.map(|w| {
let b: Vec<f64> = w["bbox"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_f64().unwrap())
.collect();
(
w["text"].as_str().unwrap().to_string(),
[b[0], b[1], b[2], b[3]],
)
})
.collect()
}