Download src/image.rs from martinsmessias/rasgapdf: direct link, hf CLI and curl.
- Browser
- Download file 6.94 kB
-
https://huggingface.co/martinsmessias/rasgapdf/resolve/main/src/image.rs
- Command line
-
hf download hf://martinsmessias/rasgapdf/src/image.rs
-
curl -L -o image.rs https://huggingface.co/martinsmessias/rasgapdf/resolve/main/src/image.rs
6.94 kB
| //! Extração de imagens embutidas (XObjects `/Subtype /Image`). | |
| //! | |
| //! O RasgaPDF expõe **metadados** das imagens no JSON espacial e, opcionalmente, | |
| //! grava o **stream codificado original** em disco (sidecar) via [`extract_images`]. | |
| //! Nenhum pixel é decodificado: o stream é copiado como está (ex.: `DCTDecode` → JPEG). | |
| use std::error::Error; | |
| use std::path::{Path, PathBuf}; | |
| use mupdf::pdf::{PageImageInfo, PdfDocument, PdfPage}; | |
| use mupdf::Document; | |
| use sha2::{Digest, Sha256}; | |
| use crate::types::{BoundingBox, SpatialImage}; | |
| /// Uma imagem embutida extraída, já com os bytes originais do stream. | |
| pub struct ExtractedImageData { | |
| /// Número da página (1-indexed). | |
| pub page: usize, | |
| /// Metadados espaciais. | |
| pub image: SpatialImage, | |
| /// Stream codificado original (pode ser vazio). | |
| pub bytes: Vec<u8>, | |
| } | |
| /// Retorna o último nome de filtro declarado em `/Filter`, preservando o caso original. | |
| /// | |
| /// Em `/Filter [/ASCII85Decode /DCTDecode]` o último filtro é o codec real da imagem. | |
| pub fn last_filter_name(filter: Option<&str>) -> Option<String> { | |
| let raw = filter?; | |
| raw.rsplit(|c: char| !c.is_ascii_alphanumeric()) | |
| .find(|token| !token.is_empty()) | |
| .map(str::to_string) | |
| } | |
| /// Versão em caixa alta do filtro, para comparação. | |
| pub fn normalized_filter(filter: Option<&str>) -> Option<String> { | |
| last_filter_name(filter).map(|f| f.to_ascii_uppercase()) | |
| } | |
| /// MIME type do stream quando ele já é um arquivo de imagem autocontido. | |
| pub fn mime_from_filter(filter: Option<&str>) -> Option<&'static str> { | |
| Some(match normalized_filter(filter)?.as_str() { | |
| "DCTDECODE" | "DCT" => "image/jpeg", | |
| "JPXDECODE" | "JPX" => "image/jp2", | |
| "JBIG2DECODE" => "image/jbig2", | |
| "CCITTFAXDECODE" | "CCF" => "image/tiff", | |
| _ => return None, | |
| }) | |
| } | |
| /// Extensão de arquivo sugerida para o stream codificado. | |
| pub fn extension_for_filter(filter: Option<&str>) -> &'static str { | |
| match normalized_filter(filter).as_deref() { | |
| Some("DCTDECODE") | Some("DCT") => "jpg", | |
| Some("JPXDECODE") | Some("JPX") => "jp2", | |
| Some("JBIG2DECODE") => "jb2", | |
| Some("CCITTFAXDECODE") | Some("CCF") => "tif", | |
| Some("FLATEDECODE") | Some("FL") => "flate.raw", | |
| Some("LZWDECODE") | Some("LZW") => "lzw.raw", | |
| _ => "bin", | |
| } | |
| } | |
| /// SHA-256 em hexadecimal minúsculo. | |
| pub fn sha256_hex(bytes: &[u8]) -> String { | |
| use std::fmt::Write as _; | |
| let digest = Sha256::digest(bytes); | |
| let mut out = String::with_capacity(digest.len() * 2); | |
| for byte in digest { | |
| let _ = write!(out, "{byte:02x}"); | |
| } | |
| out | |
| } | |
| /// Monta os metadados espaciais de uma imagem. | |
| pub fn build_spatial_image( | |
| image_id: usize, | |
| info: &PageImageInfo, | |
| bbox: Option<BoundingBox>, | |
| bytes: Option<&[u8]>, | |
| ) -> SpatialImage { | |
| SpatialImage { | |
| image_id, | |
| name: info.name.clone(), | |
| xref: info.xref, | |
| bbox, | |
| pixel_width: info.width, | |
| pixel_height: info.height, | |
| bits_per_component: info.bits_per_component, | |
| color_space: info.color_space.clone(), | |
| filter: last_filter_name(info.filter.as_deref()), | |
| mime_type: mime_from_filter(info.filter.as_deref()).map(str::to_string), | |
| byte_length: bytes.map(|b| b.len()), | |
| sha256: bytes.map(sha256_hex), | |
| file: None, | |
| } | |
| } | |
| /// Lê o stream codificado de uma imagem pelo seu xref. | |
| pub fn extract_image_bytes(doc: &PdfDocument, xref: i32) -> Option<Vec<u8>> { | |
| if xref <= 0 { | |
| return None; | |
| } | |
| doc.extract_image(xref).ok().map(|img| img.encoded) | |
| } | |
| /// Escreve os streams extraídos em `dir` e preenche `image.file` com o nome gerado. | |
| /// | |
| /// Retorna a quantidade de arquivos gravados. Imagens sem bytes são ignoradas. | |
| pub fn write_images_to_dir( | |
| images: &mut [ExtractedImageData], | |
| dir: &Path, | |
| ) -> Result<usize, Box<dyn Error + Send + Sync>> { | |
| std::fs::create_dir_all(dir)?; | |
| let mut written = 0; | |
| for record in images.iter_mut() { | |
| if record.bytes.is_empty() { | |
| continue; | |
| } | |
| let extension = extension_for_filter(record.image.filter.as_deref()); | |
| let file_name = format!( | |
| "page_{:03}_img_{:03}_xref{}.{}", | |
| record.page, record.image.image_id, record.image.xref, extension | |
| ); | |
| std::fs::write(dir.join(&file_name), &record.bytes)?; | |
| record.image.file = Some(file_name); | |
| written += 1; | |
| } | |
| Ok(written) | |
| } | |
| /// Monta o manifesto JSON das imagens extraídas. | |
| pub fn build_manifest( | |
| images: &[ExtractedImageData], | |
| written: usize, | |
| file_name: &str, | |
| ) -> serde_json::Value { | |
| serde_json::json!({ | |
| "file_name": file_name, | |
| "total_images": images.len(), | |
| "written": written, | |
| "images": images | |
| .iter() | |
| .map(|record| serde_json::json!({ | |
| "page": record.page, | |
| "image": record.image, | |
| })) | |
| .collect::<Vec<_>>(), | |
| }) | |
| } | |
| /// Grava `<dir>/manifest.json` e devolve o caminho escrito. | |
| pub fn write_manifest( | |
| images: &[ExtractedImageData], | |
| written: usize, | |
| file_name: &str, | |
| dir: &Path, | |
| ) -> Result<PathBuf, Box<dyn Error + Send + Sync>> { | |
| std::fs::create_dir_all(dir)?; | |
| let path = dir.join("manifest.json"); | |
| let file = std::fs::File::create(&path)?; | |
| serde_json::to_writer_pretty(file, &build_manifest(images, written, file_name))?; | |
| Ok(path) | |
| } | |
| /// Extrai todas as imagens embutidas do documento, com os bytes originais. | |
| /// | |
| /// Percorre as páginas sequencialmente (o documento MuPDF não é `Send`, e a extração | |
| /// de bytes é dominada por I/O de stream, não por CPU). | |
| pub fn extract_images( | |
| pdf_path: &str, | |
| ) -> Result<Vec<ExtractedImageData>, Box<dyn Error + Send + Sync>> { | |
| let doc = Document::open(pdf_path)?; | |
| let page_count = doc.page_count()? as usize; | |
| let pdf_doc = PdfDocument::try_from(doc) | |
| .map_err(|e| format!("document is not a PDF: {e}"))?; | |
| let mut out: Vec<ExtractedImageData> = Vec::new(); | |
| for page_idx in 0..page_count { | |
| let page = pdf_doc.load_page(page_idx as i32)?; | |
| let pdf_page = PdfPage::try_from(page) | |
| .map_err(|e| format!("failed to convert page {}: {e}", page_idx + 1))?; | |
| let infos = pdf_page.images().unwrap_or_default(); | |
| for (idx, info) in infos.iter().enumerate() { | |
| let bytes = extract_image_bytes(&pdf_doc, info.xref).unwrap_or_default(); | |
| let has_bytes = !bytes.is_empty(); | |
| let image = build_spatial_image( | |
| idx + 1, | |
| info, | |
| None, | |
| if has_bytes { Some(bytes.as_slice()) } else { None }, | |
| ); | |
| out.push(ExtractedImageData { | |
| page: page_idx + 1, | |
| image, | |
| bytes, | |
| }); | |
| } | |
| } | |
| Ok(out) | |
| } | |