Files
tools/backend/workers/src/pdf/images_to_pdf.rs
T
asepharyanaandKilo d4bbde9320 feat: implement image tools (compress, resize, convert) and PDF tools (merge, split, images-to-pdf)
Image tools now actually process images:
- Compress: re-encode JPEG with configurable quality
- Resize: Lanczos3 resize with fit modes (inside/fill/crop)
- Convert: cross-format conversion (JPEG/PNG/WebP/GIF/BMP)

PDF tools:
- Merge: combine multiple PDFs into one document
- Split: extract page ranges via pagespec (1-3,5,7-9)
- Images to PDF: embed images as JPEG in A4 pages with scaling
- Compress: re-save PDF with optimized cross-reference table

Also fix metrics format to emit default zero values.

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 19:52:39 +07:00

101 lines
4.0 KiB
Rust

use std::path::PathBuf;
use lopdf::{Document, Object, Stream, Dictionary};
use crate::nats::progress::ProgressReporter;
use tools_common::types::Job;
/// Convert images into a single PDF.
pub async fn process(
job: &Job,
output_dir: &PathBuf,
_progress: &ProgressReporter,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
let output_path = output_dir.join(format!("{}_images.pdf", job.id));
let mut image_paths = vec![job.file_path.clone()];
if let Some(files) = job.options.get("files").and_then(|v| v.as_array()) {
for f in files {
if let Some(path) = f.as_str() {
image_paths.push(path.to_string());
}
}
}
let mut doc = Document::new();
let pages_id = doc.new_object_id();
let mut kids = Vec::new();
for img_path in &image_paths {
let img_data = std::fs::read(img_path)?;
let format = image::guess_format(&img_data).unwrap_or(image::ImageFormat::Jpeg);
let img = image::load_from_memory(&img_data)
.map_err(|e| format!("Cannot load image: {}", e))?;
let jpeg_data = if format == image::ImageFormat::Jpeg {
img_data
} else {
let mut buf = Vec::new();
let rgb = img.to_rgb8();
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 85);
encoder.encode(rgb.as_raw(), rgb.width(), rgb.height(), image::ExtendedColorType::Rgb8)?;
buf
};
let page_width = 595.28;
let page_height = 841.89;
let scale = (page_width / img.width() as f64).min(page_height / img.height() as f64) * 0.9;
let ox = (page_width - img.width() as f64 * scale) / 2.0;
let oy = (page_height - img.height() as f64 * scale) / 2.0;
// Image XObject
let mut img_dict = Dictionary::new();
img_dict.set("Type", Object::Name("XObject".as_bytes().to_vec()));
img_dict.set("Subtype", Object::Name("Image".as_bytes().to_vec()));
img_dict.set("Width", Object::Integer(img.width() as i64));
img_dict.set("Height", Object::Integer(img.height() as i64));
img_dict.set("ColorSpace", Object::Name("DeviceRGB".as_bytes().to_vec()));
img_dict.set("BitsPerComponent", Object::Integer(8));
img_dict.set("Filter", Object::Name("DCTDecode".as_bytes().to_vec()));
let img_stream = Stream::new(img_dict, jpeg_data);
let img_id = doc.add_object(Object::Stream(img_stream));
// Content
let content = format!("q\n{} 0 0 {} {} {} cm\n/Im0 Do\nQ\n",
img.width() as f64 * scale, img.height() as f64 * scale, ox, oy).into_bytes();
let content_stream = Stream::new(Dictionary::new(), content);
let content_id = doc.add_object(Object::Stream(content_stream));
// Resources
let mut xobj = Dictionary::new();
xobj.set("Im0", Object::Reference(img_id));
let mut resources = Dictionary::new();
resources.set("XObject", Object::Dictionary(xobj));
// Page
let mut page = Dictionary::new();
page.set("Type", Object::Name("Page".as_bytes().to_vec()));
page.set("Parent", Object::Reference(pages_id));
page.set("MediaBox", Object::Array(vec![
Object::Real(0.0), Object::Real(0.0),
Object::Real(page_width as f32), Object::Real(page_height as f32),
]));
page.set("Contents", Object::Reference(content_id));
page.set("Resources", Object::Dictionary(resources));
let page_id = doc.new_object_id();
kids.push(Object::Reference(page_id));
doc.objects.insert(page_id, Object::Dictionary(page));
}
// Pages tree
let mut pages_dict = Dictionary::new();
pages_dict.set("Type", Object::Name("Pages".as_bytes().to_vec()));
pages_dict.set("Count", Object::Integer(kids.len() as i64));
pages_dict.set("Kids", Object::Array(kids));
doc.objects.insert(pages_id, Object::Dictionary(pages_dict));
doc.save_to(&mut std::fs::File::create(&output_path)?)?;
Ok(output_path)
}