Files

Images to PDF

Pictures put into a PDF, one per page, fitted and centred, with captions if you like: photo reports, scans and receipts.

Version 1.0.0 · 2 functions · 3 crates

#pdf#images#reports#export

Functions

What use images_pdf.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use images_pdf, images_pdf_captions from images_pdf (Quillo 0.6).

images_pdf(images: list, out: str, page: str, margin: float) → intor an error

A PDF at out (folders made) with each picture of images on a page of its own, as large as fits inside margin (in points, 72 to the inch), centred: the page count. page is "A4", "A5", "A3", "Letter" or "Legal" (turned for landscape pictures), or "fit" for pages the size of each picture. Photos are turned upright by their EXIF orientation.

images_pdf_captions(items: list, out: str, page: str, margin: float) → intor an error

The same with a caption under each picture: items are { path, caption }. Captions are one line in Helvetica, in the Latin alphabet (other characters become "?").

Get it

$ quillo install images_pdf               # images_pdf.qwer into this folder$ quillo install images_pdf --example     # and its example, images_pdf.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/images_pdf.qwer$ curl -O https://quillolang.org/plugins/files/images_pdf.qwe$ curl -O https://quillolang.org/plugins/files/images_pdf_sample.jpg$ curl -O https://quillolang.org/plugins/files/images_pdf_sample.png

Then, in your program, all of it or only what you call:

main.qwe
use images_pdf.qweruse images_pdf, images_pdf_captions from images_pdf      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
lopdf = { version = "0.36", default-features = false }image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }encoding_rs = "0.8"

Example

images_pdf.qwe
// A site visit's photos as one PDF report, and pictures as a PDF of their own size.////   quillo run images_pdf.qwe      then open http://localhost:8080/report.pdfuse "images_pdf.qwer"route GET /report.pdf {    images_pdf_captions([        { path: "images_pdf_sample.jpg", caption: "The hills behind the café, 14 June" },        { path: "images_pdf_sample.png", caption: "The order page, as customers see it" },    ], "data/visit.pdf", "A4", 36.0)?    file("data/visit.pdf")}route GET /pages = {    report: images_pdf(["images_pdf_sample.jpg", "images_pdf_sample.png", "images_pdf_sample.jpg"], "data/photos.pdf", "Letter", 54.0)?,    fitted: images_pdf(["images_pdf_sample.png"], "data/screen.pdf", "fit", 0.0)?,}
$ quillo run images_pdf.qwe$ curl 'localhost:8080/pages'$ curl 'localhost:8080/report.pdf'

Source

images_pdf.qwer
//! Pictures (JPEG, PNG, WebP, GIF) put into a PDF, one per page, fitted and centred, with captions if you like: photo reports, scans, receipts.//! version: 1.0.0//!//! [dependencies]//! lopdf = { version = "0.36", default-features = false }//! image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }//! encoding_rs = "0.8"use image::{DynamicImage, ImageDecoder};use lopdf::{dictionary, Document, Object, ObjectId, Stream};const MAX_IMAGES: usize = 500;fn limits() -> image::Limits {    let mut l = image::Limits::default();    l.max_image_width = Some(12_000);    l.max_image_height = Some(12_000);    l.max_alloc = Some(512 * 1024 * 1024);    l}fn reader(path: &str) -> Result<image::ImageReader<std::io::BufReader<std::fs::File>>, String> {    let mut r = image::ImageReader::open(path).map_err(|e| format!("{}: {}", path, e))?.with_guessed_format().map_err(|e| format!("{}: {}", path, e))?;    r.limits(limits());    Ok(r)}// The picture as a PDF image: a JPEG that needs no turning goes in as it is; anything else is decoded, turned upright and compressed.fn embed(doc: &mut Document, path: &str) -> Result<(ObjectId, u32, u32), String> {    let bytes = std::fs::read(path).map_err(|e| format!("{}: {}", path, e))?;    if bytes.starts_with(&[0xFF, 0xD8]) {        let mut dec = image::codecs::jpeg::JpegDecoder::new(std::io::Cursor::new(&bytes)).map_err(|e| format!("{}: {}", path, e))?;        let (w, h) = dec.dimensions();        let orientation = dec.orientation().unwrap_or(image::metadata::Orientation::NoTransforms);        let space = match dec.color_type() {            image::ColorType::L8 => Some("DeviceGray"),            image::ColorType::Rgb8 => Some("DeviceRGB"),            _ => None,        };        if let (Some(space), image::metadata::Orientation::NoTransforms) = (space, orientation) {            if w <= 12_000 && h <= 12_000 {                let mut s = Stream::new(dictionary! { "Type" => "XObject", "Subtype" => "Image", "Width" => w, "Height" => h, "ColorSpace" => space, "BitsPerComponent" => 8, "Filter" => "DCTDecode" }, bytes);                s.allows_compression = false;                return Ok((doc.add_object(s), w, h));            }        }    }    let mut dec = reader(path)?.into_decoder().map_err(|e| format!("{}: {}", path, e))?;    let orientation = dec.orientation().unwrap_or(image::metadata::Orientation::NoTransforms);    let mut img = DynamicImage::from_decoder(dec).map_err(|e| format!("{}: {}", path, e))?;    img.apply_orientation(orientation);    let (w, h) = (img.width(), img.height());    let mut dict = dictionary! { "Type" => "XObject", "Subtype" => "Image", "Width" => w, "Height" => h, "ColorSpace" => "DeviceRGB", "BitsPerComponent" => 8 };    if img.color().has_alpha() {        let alpha: Vec<u8> = img.to_rgba8().pixels().map(|p| p.0[3]).collect();        let mut sm = Stream::new(dictionary! { "Type" => "XObject", "Subtype" => "Image", "Width" => w, "Height" => h, "ColorSpace" => "DeviceGray", "BitsPerComponent" => 8 }, alpha);        let _ = sm.compress();        dict.set("SMask", doc.add_object(sm));    }    let mut s = Stream::new(dict, img.to_rgb8().into_raw());    let _ = s.compress();    Ok((doc.add_object(s), w, h))}fn page_size(page: &str, w: u32, h: u32) -> Result<(f64, f64), String> {    let (pw, ph) = match page.to_lowercase().as_str() {        "a4" | "" => (595.28, 841.89),        "a5" => (419.53, 595.28),        "a3" => (841.89, 1190.55),        "letter" => (612.0, 792.0),        "legal" => (612.0, 1008.0),        // The picture's own size, at 96 pixels to the inch.        "fit" => (w as f64 * 0.75, h as f64 * 0.75),        p => return Err(format!("the page is A4, A5, A3, Letter, Legal or fit, not `{}`", p)),    };    // Landscape pictures get landscape pages.    Ok(if page != "fit" && w > h { (ph, pw) } else { (pw, ph) })}// Text in the PDF's standard fonts' encoding; what it lacks becomes "?".fn winansi(s: &str) -> Vec<u8> {    s.chars().map(|c| {        let mut buf = [0u8; 4];        let (bytes, _, bad) = encoding_rs::WINDOWS_1252.encode(c.encode_utf8(&mut buf));        if bad || bytes.len() != 1 { b'?' } else { bytes[0] }    }).collect()}fn build(items: &[(String, String)], out: &str, page: &str, margin: f64) -> Result<i64, String> {    if items.is_empty() || items.len() > MAX_IMAGES {        return Err(format!("give 1 to {} pictures", MAX_IMAGES));    }    let margin = margin.clamp(0.0, 200.0);    let mut doc = Document::with_version("1.5");    let pages_id = doc.new_object_id();    let font = doc.add_object(dictionary! { "Type" => "Font", "Subtype" => "Type1", "BaseFont" => "Helvetica", "Encoding" => "WinAnsiEncoding" });    let mut kids = vec![];    for (path, caption) in items {        let (img, w, h) = embed(&mut doc, path)?;        let (pw, ph) = page_size(page, w, h)?;        let m = if page == "fit" { 0.0 } else { margin };        let cap_h = if caption.is_empty() { 0.0 } else { 24.0 };        let (bw, bh) = ((pw - 2.0 * m).max(1.0), (ph - 2.0 * m - cap_h).max(1.0));        let scale = (bw / w as f64).min(bh / h as f64);        let (iw, ih) = (w as f64 * scale, h as f64 * scale);        let (x, y) = (m + (bw - iw) / 2.0, m + cap_h + (bh - ih) / 2.0);        let mut content = format!("q {:.2} 0 0 {:.2} {:.2} {:.2} cm /Im1 Do Q\n", iw, ih, x, y).into_bytes();        if !caption.is_empty() {            let text: Vec<u8> = winansi(caption).iter().flat_map(|&b| match b { b'(' | b')' | b'\\' => vec![b'\\', b], b => vec![b] }).collect();            // Centred under the picture, Helvetica 11 pt: about half a point of width per point of size per character.            let width = caption.chars().count() as f64 * 11.0 * 0.5;            content.extend(format!("BT /F1 11 Tf {:.2} {:.2} Td (", (pw - width).max(0.0) / 2.0, y - 18.0).into_bytes());            content.extend(text);            content.extend(b") Tj ET\n");        }        let cid = doc.add_object(Stream::new(dictionary! {}, content));        let pid = doc.add_object(dictionary! {            "Type" => "Page", "Parent" => pages_id, "Contents" => cid,            "MediaBox" => vec![0.into(), 0.into(), Object::Real(pw as f32), Object::Real(ph as f32)],            "Resources" => dictionary! { "XObject" => dictionary! { "Im1" => img }, "Font" => dictionary! { "F1" => font } },        });        kids.push(Object::Reference(pid));    }    let n = kids.len() as i64;    doc.objects.insert(pages_id, Object::Dictionary(dictionary! { "Type" => "Pages", "Kids" => kids, "Count" => n }));    let catalog = doc.add_object(dictionary! { "Type" => "Catalog", "Pages" => pages_id });    doc.trailer.set("Root", catalog);    doc.compress();    if let Some(dir) = std::path::Path::new(out).parent().filter(|d| !d.as_os_str().is_empty()) {        std::fs::create_dir_all(dir).map_err(|e| format!("{}: {}", dir.display(), e))?;    }    doc.save(out).map_err(|e| format!("{}: {}", out, e))?;    Ok(n)}/// A PDF at `out` (folders made) with each picture of `images` on a page of its own, as large as fits inside `margin` (in points, 72 to the inch), centred: the page count./// `page` is "A4", "A5", "A3", "Letter" or "Legal" (turned for landscape pictures), or "fit" for pages the size of each picture. Photos are turned upright by their EXIF orientation.pub fn images_pdf(images: Vec<String>, out: &str, page: &str, margin: f64) -> Result<i64, String> {    build(&images.into_iter().map(|p| (p, String::new())).collect::<Vec<_>>(), out, page, margin)}/// The same with a caption under each picture: `items` are `{ path, caption }`. Captions are one line in Helvetica, in the Latin alphabet (other characters become "?").pub fn images_pdf_captions(items: Vec<serde_json::Value>, out: &str, page: &str, margin: f64) -> Result<i64, String> {    let list: Vec<(String, String)> = items.iter().map(|v| (v["path"].as_str().unwrap_or("").to_string(), v["caption"].as_str().unwrap_or("").to_string())).collect();    if let Some((_, _)) = list.iter().find(|(p, _)| p.is_empty()) {        return Err("each item is { path, caption }".into());    }    build(&list, out, page, margin)}
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

On Images to PDF: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.