mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 16:36:13 -06:00
This commit is contained in:
@@ -34,6 +34,7 @@ mod scan_colours;
|
||||
mod scan_fields;
|
||||
pub mod signature_image;
|
||||
pub mod signature_scan;
|
||||
mod stamp_preview;
|
||||
pub mod text_fonts;
|
||||
mod text_preview;
|
||||
pub mod word_count;
|
||||
@@ -41,6 +42,7 @@ pub mod xfa;
|
||||
|
||||
pub use signature_image::{ImageSignaturePreview, SignatureImage};
|
||||
pub use signature_scan::{PhotoRect, ScanError, SignaturePhoto, Turn};
|
||||
pub use stamp_preview::StampPreview;
|
||||
pub use text_preview::TextEditPreview;
|
||||
|
||||
pub use pdfcraft_organize::{BoxSpec, FieldConflicts, PageBox, SplitBy, Transition, TransitionSpec, split_ranges};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//! A stamp while it is moved or resized: the page without it, and the stamp alone.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use pdfcraft_cos::{SaveOptions, write_incremental};
|
||||
use pdfcraft_render::{PageRenderer, RenderConfig, RenderPool, RenderRequest, RenderedPage};
|
||||
|
||||
use crate::{Document, guard};
|
||||
|
||||
/// Read-only layers for interactive movement and resizing: the page without the stamp, and the
|
||||
/// stamp alone on a transparent page. Moving or resizing a stamp only changes its `/Rect`, which
|
||||
/// its appearance is stretched to fit, so these two show the result before it is applied.
|
||||
/// Preparing them never changes the working bytes, dirty flag or undo history.
|
||||
pub struct StampPreview {
|
||||
page: usize,
|
||||
background: Arc<Vec<u8>>,
|
||||
stamp: Arc<Vec<u8>>,
|
||||
config: RenderConfig,
|
||||
}
|
||||
|
||||
impl StampPreview {
|
||||
/// One worker for the page beneath the stamp.
|
||||
pub fn background_renderer(&self) -> RenderPool {
|
||||
RenderPool::new(self.background.clone(), 1, self.config.clone())
|
||||
}
|
||||
|
||||
/// One worker for the stamp alone: its page has no content, no other annotations and no
|
||||
/// background, so only the stamp's own pixels are drawn.
|
||||
pub fn stamp_renderer(&self) -> RenderPool {
|
||||
RenderPool::new(self.stamp.clone(), 1, self.stamp_config())
|
||||
}
|
||||
|
||||
/// The page beneath the stamp, rendered synchronously with the renderer's normal pixel limits.
|
||||
pub fn render_background(&self, scale: f32) -> Result<RenderedPage, String> {
|
||||
self.render(&self.background, self.config.clone(), scale)
|
||||
}
|
||||
|
||||
/// The stamp alone on a transparent page, rendered like [`Self::render_background`].
|
||||
pub fn render_stamp(&self, scale: f32) -> Result<RenderedPage, String> {
|
||||
self.render(&self.stamp, self.stamp_config(), scale)
|
||||
}
|
||||
|
||||
fn stamp_config(&self) -> RenderConfig {
|
||||
RenderConfig { transparent: true, ..self.config.clone() }
|
||||
}
|
||||
|
||||
fn render(&self, bytes: &Arc<Vec<u8>>, config: RenderConfig, scale: f32) -> Result<RenderedPage, String> {
|
||||
if !scale.is_finite() || scale <= 0.0 {
|
||||
return Err("preview scale must be finite and positive".into());
|
||||
}
|
||||
guard(|| {
|
||||
let out = PageRenderer::new(bytes.clone(), config).render(RenderRequest { page: self.page, scale, ..Default::default() });
|
||||
if let Some(e) = &out.error {
|
||||
return Err(e.clone());
|
||||
}
|
||||
Ok(out)
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// The layers that show the stamp at `(page, index)` moved or resized as it is dragged.
|
||||
/// `None` for an annotation that isn't a stamp, or one without an appearance to draw.
|
||||
pub fn stamp_preview(&self, page: usize, index: usize) -> Result<Option<StampPreview>, String> {
|
||||
guard(|| {
|
||||
let editor = self.editor.as_ref().ok_or("the document can't be read")?;
|
||||
let is_stamp = self.info.annotations.iter().any(|a| a.page == page && a.index == index && a.subtype == "Stamp");
|
||||
if !is_stamp {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut stamp = editor.cos.clone();
|
||||
if !pdfcraft_annot::isolate_annotation(&mut stamp, page, index).map_err(|e| e.to_string())? {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut background = editor.cos.clone();
|
||||
pdfcraft_annot::delete_annotation(&mut background, page, index).map_err(|e| e.to_string())?;
|
||||
// The page beneath as it is displayed: comments without an appearance get theirs.
|
||||
let background = pdfcraft_annot::with_missing_appearances(&background).unwrap_or(background);
|
||||
let write = |cos: &pdfcraft_cos::Document| write_incremental(cos, &SaveOptions::default()).map(Arc::new).map_err(|e| e.to_string());
|
||||
Ok(Some(StampPreview { page, background: write(&background)?, stamp: write(&stamp)?, config: self.config.clone() }))
|
||||
})?
|
||||
}
|
||||
}
|
||||
@@ -2554,6 +2554,60 @@ fn comment_text(s: &Session, id: DocId) -> Vec<String> {
|
||||
t
|
||||
}
|
||||
|
||||
/// A dragged stamp's layers: the page without it (its text and other comments kept) and the
|
||||
/// stamp alone on a transparent page. Preparing them changes nothing in the document.
|
||||
#[test]
|
||||
fn stamp_preview_layers_split_the_stamp_from_its_page() {
|
||||
let (mut s, id) = session_with(1);
|
||||
s.apply(id, fill_item(Shape::Mark { rect: [30.0, 60.0, 130.0, 66.0], mark: FillMark::Line }, "")).unwrap();
|
||||
s.apply(id, rect_comment(0, [140.0, 40.0, 190.0, 90.0])).unwrap();
|
||||
let doc = s.get(id).unwrap();
|
||||
let (generation, dirty) = (doc.edit_generation(), doc.dirty);
|
||||
let preview = doc.stamp_preview(0, 0).unwrap().expect("the line is a stamp");
|
||||
let (stamp, background) = (preview.render_stamp(1.0).unwrap(), preview.render_background(1.0).unwrap());
|
||||
let px = |p: &pdfcraft_render::RenderedPage, x: u32, y: u32| -> [u8; 4] {
|
||||
p.rgba[(((300 - y) * p.width + x) * 4) as usize..][..4].try_into().unwrap()
|
||||
};
|
||||
let line = |x: u32, y: u32| (30..=130).contains(&x) && (59..=67).contains(&y);
|
||||
let (mut drawn, mut stray) = (0, Vec::new());
|
||||
for y in 1..300 {
|
||||
for x in 0..200 {
|
||||
match (line(x, y), px(&stamp, x, y)) {
|
||||
(true, [.., 255]) => drawn += 1,
|
||||
(false, v) if v != [0, 0, 0, 0] => stray.push((x, y)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(drawn > 100, "the line is drawn opaque: {drawn} pixels");
|
||||
assert!(stray.is_empty(), "only the line is drawn on its layer: {:?}", &stray[..stray.len().min(8)]);
|
||||
assert!((30..=130).all(|x| px(&background, x, 63) == [255, 255, 255, 255]), "the page beneath has no line");
|
||||
assert!((138..=142).any(|x| px(&background, x, 65) != [255, 255, 255, 255]), "other comments stay");
|
||||
assert!((20..120).any(|x| (150..170).any(|y| px(&background, x, y)[0] < 128)), "the page text stays");
|
||||
assert!(doc.stamp_preview(0, 1).unwrap().is_none(), "a rectangle isn't a stamp");
|
||||
assert!(doc.stamp_preview(0, 9).unwrap().is_none());
|
||||
let doc = s.get(id).unwrap();
|
||||
assert_eq!((doc.edit_generation(), doc.dirty, doc.can_undo()), (generation, dirty, Some("Add rectangle")), "the document is unchanged");
|
||||
|
||||
// Another app's comments without appearances: beneath a dragged stamp they are drawn as on
|
||||
// screen, and such a stamp has nothing to lay over the page.
|
||||
let bare = b"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 300] >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R] >> endobj
|
||||
4 0 obj << /Type /Annot /Subtype /Square /Rect [140 40 190 90] /C [1 0 0] >> endobj
|
||||
5 0 obj << /Type /Annot /Subtype /Stamp /Rect [20 20 80 40] /Name /Draft >> endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF";
|
||||
let mut s = Session::new();
|
||||
let id = s.open("bare.pdf", None, Arc::new(bare.to_vec()), None).unwrap();
|
||||
s.apply(id, fill_item(Shape::Mark { rect: [30.0, 60.0, 130.0, 66.0], mark: FillMark::Line }, "")).unwrap();
|
||||
let doc = s.get(id).unwrap();
|
||||
let background = doc.stamp_preview(0, 2).unwrap().expect("the line is a stamp").render_background(1.0).unwrap();
|
||||
assert!((138..=142).any(|x| px(&background, x, 65)[..3] == [255, 0, 0]), "the square without an appearance is drawn beneath");
|
||||
assert!(doc.stamp_preview(0, 1).unwrap().is_none(), "a stamp without an appearance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattening_fill_and_sign_keeps_other_comments_fields_and_undoes() {
|
||||
let (mut s, id) = session_with(1);
|
||||
|
||||
Reference in New Issue
Block a user