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
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 errorA 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 errorThe 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.qweOr 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.pngThen, in your program, all of it or only what you call:
use images_pdf.qweruse images_pdf, images_pdf_captions from images_pdf // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
lopdf = { version = "0.36", default-features = false }image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }encoding_rs = "0.8"Example
// 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
//! 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)}Related
pdf_tools.qwer · 1.0.0Merge PDFs, pick and split their pages, count them, and read or set the title and author.8 views · 6 downloadsPDF documents pdf.qwer · 1.0.0Simple PDFs: receipts, letters, tickets and reports as headings and paragraphs, wrapped and flowed onto A4 or Letter pages.7 views · 6 downloadsWord documents docx.qwer · 1.0.0Word documents written from blocks: headings, paragraphs with bold and italic, lists, tables and page breaks.6 views · 7 downloadsEPUB epub.qwer · 1.0.0E-books made from chapters of HTML or Markdown, with a title, author, cover and table of contents, and their details read back.8 views · 8 downloadsExcel files spreadsheet.qwer · 1.0.0Read the sheets of .xlsx, .xls and .ods files, and write .xlsx reports with a bold header and fitted columns.7 views · 7 downloadsFont subsets font_subset.qwer · 1.0.0TrueType fonts cut down to the characters a page or a PDF uses, as .ttf, as .woff for the web, or as a data URL for CSS, with the characters the font lacks.6 views · 7 downloadsThread 0 posts
On Images to PDF: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.