M12: resolve main integration for PR #2624

This commit is contained in:
PerchAnton
2026-10-19 19:07:47 +03:00
74 changed files with 3543 additions and 490 deletions
+31 -15
View File
@@ -93,14 +93,14 @@ impl Exporter {
encode_png(r.width, r.height, &r.rgba)
}
/// Page `page` as an image file of `format`.
/// Page `page` as an image file of `format`, which says the resolution it was drawn at.
pub fn image(&mut self, page: usize, dpi: f64, format: ImageFormat) -> Result<Vec<u8>, String> {
self.check(page)?;
let r = self.renderer.render(RenderRequest { page, scale: (dpi.clamp(18.0, 1200.0) / 72.0) as f32, ..Default::default() });
if let Some(e) = r.error {
return Err(format!("page {}: {e}", page + 1));
}
encode_image(r.width, r.height, &r.rgba, format)
encode_image(r.width, r.height, &r.rgba, format, self.dpi_used(page, dpi))
}
/// The reading-order text of a page.
@@ -127,12 +127,14 @@ impl Exporter {
}
}
/// Premultiplied RGBA → an image file of `format`.
pub fn encode_image(width: u32, height: u32, premultiplied: &[u8], format: ImageFormat) -> Result<Vec<u8>, String> {
/// Premultiplied RGBA → an image file of `format`. A JPEG or TIFF says it is `dpi` dots per inch,
/// so other programs show and print it at the page's size. A PNG says nothing: its `pHYs` counts
/// pixels per metre, so 72 dpi would read back as 72.009 and a page made from it would shrink.
pub fn encode_image(width: u32, height: u32, premultiplied: &[u8], format: ImageFormat, dpi: f64) -> Result<Vec<u8>, String> {
match format {
ImageFormat::Png => encode_png(width, height, premultiplied),
ImageFormat::Jpeg { quality } => encode_jpeg(width, height, premultiplied, quality),
ImageFormat::Tiff => encode_tiff(width, height, premultiplied),
ImageFormat::Jpeg { quality } => encode_jpeg(width, height, premultiplied, quality, dpi),
ImageFormat::Tiff => encode_tiff(width, height, premultiplied, dpi),
}
}
@@ -199,23 +201,37 @@ fn over_white(premultiplied: &[u8]) -> Vec<u8> {
.collect()
}
/// Premultiplied RGBA → baseline JPEG (pages are opaque: transparency becomes white paper).
pub fn encode_jpeg(width: u32, height: u32, premultiplied: &[u8], quality: u8) -> Result<Vec<u8>, String> {
/// Premultiplied RGBA → baseline JPEG at `dpi` (pages are opaque: transparency becomes white paper).
pub fn encode_jpeg(width: u32, height: u32, premultiplied: &[u8], quality: u8, dpi: f64) -> Result<Vec<u8>, String> {
use image::ImageEncoder;
use image::codecs::jpeg::{JpegEncoder, PixelDensity};
let rgb = over_white(premultiplied);
let mut out = Vec::new();
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, quality.clamp(1, 100))
.write_image(&rgb, width, height, image::ExtendedColorType::Rgb8)
.map_err(|e| e.to_string())?;
let mut enc = JpegEncoder::new_with_quality(&mut out, quality.clamp(1, 100));
// The JFIF density is whole dots per inch.
enc.set_pixel_density(PixelDensity::dpi(dpi.round() as u16));
enc.write_image(&rgb, width, height, image::ExtendedColorType::Rgb8).map_err(|e| e.to_string())?;
Ok(out)
}
/// Premultiplied RGBA → TIFF (RGB, LZW-compressed by the encoder's default).
pub fn encode_tiff(width: u32, height: u32, premultiplied: &[u8]) -> Result<Vec<u8>, String> {
use image::ImageEncoder;
/// Premultiplied RGBA → TIFF (RGB, LZW-compressed) at `dpi`. Written with `tiff` directly:
/// `image`'s `TiffEncoder` always says 1 × 1 with no unit and leaves the strips uncompressed.
/// No predictor: LZW alone is the compression TIFF readers support most widely after none.
pub fn encode_tiff(width: u32, height: u32, premultiplied: &[u8], dpi: f64) -> Result<Vec<u8>, String> {
use tiff::encoder::{Compression, Rational, TiffEncoder, colortype::RGB8};
use tiff::tags::{ResolutionUnit, Tag};
let err = |e: tiff::TiffError| e.to_string();
let rgb = over_white(premultiplied);
let mut out = std::io::Cursor::new(Vec::new());
image::codecs::tiff::TiffEncoder::new(&mut out).write_image(&rgb, width, height, image::ExtendedColorType::Rgb8).map_err(|e| e.to_string())?;
let mut tiff = TiffEncoder::new(&mut out).map_err(err)?.with_compression(Compression::Lzw);
let mut image = tiff.new_image::<RGB8>(width, height).map_err(err)?;
// In hundredths: a page drawn at a lowered resolution (`Exporter::dpi_used`) is rarely a whole number.
let resolution = Rational { n: (dpi * 100.0).round() as u32, d: 100 };
let tags = image.encoder();
tags.write_tag(Tag::ResolutionUnit, ResolutionUnit::Inch).map_err(err)?;
tags.write_tag(Tag::XResolution, resolution.clone()).map_err(err)?;
tags.write_tag(Tag::YResolution, resolution).map_err(err)?;
image.write_data(&rgb).map_err(err)?;
Ok(out.into_inner())
}
+5 -3
View File
@@ -1281,9 +1281,11 @@ pub enum Edit {
width: f64,
height: f64,
},
/// Copies of `pages` inserted after the last of them.
/// Copies of `pages` inserted at `at` (an index in the page order as it is now), or after
/// the last of them when `at` is `None`.
DuplicatePages {
pages: Vec<usize>,
at: Option<usize>,
},
/// Replace Pages: the content of `pages` is replaced by `src_pages` of another PDF; the
/// pages' links, comments and bookmarks stay.
@@ -1948,7 +1950,7 @@ impl Edit {
Edit::MovePages { pages, .. } => plural("Move page", "Move pages", pages.len()),
Edit::ReversePages { .. } => Label::of("Reverse pages"),
Edit::InsertBlankPage { .. } => Label::of("Insert blank page"),
Edit::DuplicatePages { pages } => plural("Duplicate page", "Duplicate pages", pages.len()),
Edit::DuplicatePages { pages, .. } => plural("Duplicate page", "Duplicate pages", pages.len()),
Edit::ReplacePages { pages, .. } => plural("Replace page", "Replace pages", pages.len()),
Edit::SetPageBox { pages, which: PageBox::Crop, .. } => plural("Crop page", "Crop pages", pages.len()),
Edit::SetPageBox { .. } => Label::of("Set page boxes"),
@@ -2335,7 +2337,7 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) ->
pdfcraft_organize::insert_blank_page(doc, *at, *width, *height)?;
}
Edit::SetInfo { key, value } => pdfcraft_organize::set_info(doc, key, value)?,
Edit::DuplicatePages { pages } => pdfcraft_organize::duplicate_pages(doc, pages)?,
Edit::DuplicatePages { pages, at } => pdfcraft_organize::duplicate_pages(doc, pages, *at)?,
Edit::ReplacePages { pages, name, bytes, src_pages } => {
let src = open_source(name, bytes)?;
pdfcraft_organize::replace_pages(doc, pages, &src, src_pages)?;
+44 -2
View File
@@ -870,7 +870,7 @@ fn form_with_field(name: &str) -> Vec<u8> {
fn duplicating_a_page_with_a_field_keeps_the_field_name() {
let mut s = Session::new().with_clock(|| 1_700_000_000);
let id = s.open("a.pdf", None, Arc::new(form_with_field("Text1")), None).unwrap();
s.apply(id, Edit::DuplicatePages { pages: vec![0] }).unwrap();
s.apply(id, Edit::DuplicatePages { pages: vec![0], at: None }).unwrap();
let doc = s.get(id).unwrap();
assert_eq!(doc.info.pages.len(), 2);
assert!(doc.form.iter().all(|f| f.name == "Text1"), "{:?}", doc.form.iter().map(|f| &f.name).collect::<Vec<_>>());
@@ -2183,7 +2183,7 @@ fn crop_and_duplicate_pages_show_in_the_viewer() {
assert_eq!(d.info.pages[0].crop, [10.0, 20.0, 170.0, 260.0]);
assert_eq!((d.info.pages[0].width, d.info.pages[0].height), (160.0, 240.0), "the viewer shows the cropped size");
assert_eq!(d.page_boxes()[1][1], [0.0, 0.0, 200.0, 300.0]);
s.apply(id, Edit::DuplicatePages { pages: vec![1] }).unwrap();
s.apply(id, Edit::DuplicatePages { pages: vec![1], at: None }).unwrap();
assert_eq!(page_texts(&s, id), ["Page 1", "Page 2", "Page 2"]);
assert_eq!(s.get(id).unwrap().can_undo(), Some("Duplicate page"));
}
@@ -2574,6 +2574,48 @@ fn pages_export_as_png_jpeg_and_tiff() {
let tif = ex.image(0, 144.0, ImageFormat::Tiff).unwrap();
let decoded = image::load_from_memory_with_format(&tif, image::ImageFormat::Tiff).unwrap();
assert_eq!((decoded.width(), decoded.height()), (400, 600));
let mut tags = tiff::decoder::Decoder::new(std::io::Cursor::new(&tif)).unwrap();
let compression = tags.get_tag_u32(tiff::tags::Tag::Compression).unwrap();
let predictor = tags.get_tag_u32(tiff::tags::Tag::Predictor).unwrap();
assert_eq!((compression, predictor), (5, 1), "LZW, without a predictor");
}
/// A TIFF's resolution unit (2 = inch) and its horizontal and vertical resolution.
fn tiff_resolution(bytes: &[u8]) -> (u32, f64, f64) {
use tiff::{decoder::ifd::Value, tags::Tag};
let mut d = tiff::decoder::Decoder::new(std::io::Cursor::new(bytes)).unwrap();
let mut res = |tag| match d.get_tag(tag).unwrap() {
Value::Rational(n, d) => f64::from(n) / f64::from(d),
v => panic!("{tag:?} is {v:?}"),
};
let (x, y) = (res(Tag::XResolution), res(Tag::YResolution));
(d.get_tag_u32(Tag::ResolutionUnit).unwrap(), x, y)
}
/// An exported TIFF or JPEG says the resolution it was drawn at, so other programs show and print it
/// at the page's size. A TIFF said 1 × 1 with no unit, and a JPEG said nothing.
#[test]
fn exported_pages_record_the_resolution_they_were_drawn_at() {
use crate::export::{Exporter, ImageFormat};
let (s, id) = session_with(1);
let mut ex = Exporter::new(s.get(id).unwrap());
assert_eq!(tiff_resolution(&ex.image(0, 144.0, ImageFormat::Tiff).unwrap()), (2, 144.0, 144.0));
let jpg = ex.image(0, 144.0, ImageFormat::Jpeg { quality: 85 }).unwrap();
assert_eq!(&jpg[6..11], b"JFIF\0");
assert_eq!(jpg[13..18], [1, 0, 144, 0, 144], "JFIF density in dots per inch");
// pHYs counts pixels per metre: 72 dpi would read back as 72.009, and a page made from the PNG
// (doc_create from images) would come out 0.01% small.
let png = ex.image(0, 72.0, ImageFormat::Png).unwrap();
assert!(png::Decoder::new(std::io::Cursor::new(png)).read_info().unwrap().info().pixel_dims.is_none(), "a PNG states no resolution");
// A page too large for the renderer at the asked resolution says the one it was drawn at.
let mut s = Session::new().with_clock(|| 1_700_000_000);
let bytes = s.create_blank(14_400.0, 792.0, 1).unwrap();
let id = s.open("wide.pdf", None, bytes, None).unwrap();
let mut ex = Exporter::new(s.get(id).unwrap());
let used = ex.dpi_used(0, 600.0);
let (unit, x, y) = tiff_resolution(&ex.image(0, 600.0, ImageFormat::Tiff).unwrap());
assert!(unit == 2 && (x - used).abs() < 0.01 && (y - used).abs() < 0.01, "{x} × {y}, drawn at {used}");
}
#[test]