mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-08 03:54:35 +00:00
Add text: an ink is read only from numbers and kept as painted (review)
A stored /CMYK with a non-numeric entry was read from its numbers alone, so [0 /Bad 0 0 1] became 100 K; a /Gray that isn't a number with a /CMYK beside it fell through to the CMYK. Both now give no ink, and the text paints in its /Color. g and k are written to the thousandth, as rg is, so an ink set or read with finer digits painted other than what the item, its stored /Gray or /CMYK and content_list held. The ink is now kept as painted, with its look, when set and when read back. RGB text is unchanged.
This commit is contained in:
3 files changed
+78
-9
No files matched your search
@@ -3,13 +3,16 @@ use super::layout::{first_undrawable, is_thai, nums, shaped_arabic, thai_error};
|
||||
use super::types::{Added, AddedImage, AddedText, CHAR_SPACING_LIMIT, Family, Ink, MarkerScan, TAG};
|
||||
use super::{Content, Dict, Document, EditError, GlyphError, MarkKind, ObjRef, Object, Stream, align_from, check, contents, page_list, place_tagged};
|
||||
|
||||
/// A text item's gray or CMYK ink. Items written before inks, and an ink out of range, have none:
|
||||
/// the text paints in its RGB `/Color`.
|
||||
/// A text item's gray or CMYK ink. Items written before inks, and an ink out of range or not
|
||||
/// made only of numbers (a `/Gray` name, a `/CMYK` entry that isn't a number), have none: the
|
||||
/// text paints in its RGB `/Color`.
|
||||
fn ink(doc: &Document, d: &Dict) -> Option<Ink> {
|
||||
let ink = match (d.get(b"Gray").map(|g| doc.resolve(g)).and_then(|g| g.as_f64()), d.get(b"CMYK")) {
|
||||
(Some(g), _) => Ink::Gray(g),
|
||||
(None, Some(c)) => Ink::Cmyk(nums(doc, Some(c)).try_into().ok()?),
|
||||
(None, None) => return None,
|
||||
let ink = if let Some(g) = d.get(b"Gray") {
|
||||
Ink::Gray(doc.resolve(g).as_f64()?)
|
||||
} else {
|
||||
let cmyk = doc.resolve(d.get(b"CMYK")?);
|
||||
let parts: Vec<f64> = cmyk.as_array()?.iter().map(Object::as_f64).collect::<Option<_>>()?;
|
||||
Ink::Cmyk(parts.try_into().ok()?)
|
||||
};
|
||||
Some(ink).filter(|i| i.valid())
|
||||
}
|
||||
@@ -48,6 +51,10 @@ fn parse(doc: &Document, d: &Dict) -> Option<Content> {
|
||||
};
|
||||
// An ink whose look isn't the stored colour (a file edited by hand) is none.
|
||||
t.drop_stale_ink();
|
||||
// One that stands is the ink as painted, finer digits dropped, and its look.
|
||||
if let Some(i) = t.ink {
|
||||
t.set_ink(Some(i));
|
||||
}
|
||||
Some(Content::Text(t))
|
||||
}
|
||||
b"Image" => {
|
||||
|
||||
@@ -106,6 +106,16 @@ impl Ink {
|
||||
self.components().iter().all(|v| v.is_finite() && (0.0..=1.0).contains(v))
|
||||
}
|
||||
|
||||
/// The ink as its operator paints it: each component to the thousandth that `g` and `k` are
|
||||
/// written with (as RGB's `rg`), so the item, what it reads back as and the page agree.
|
||||
pub fn painted(self) -> Ink {
|
||||
let round = |v: f64| (v * 1000.0).round() / 1000.0;
|
||||
match self {
|
||||
Ink::Gray(g) => Ink::Gray(round(g)),
|
||||
Ink::Cmyk(c) => Ink::Cmyk(c.map(round)),
|
||||
}
|
||||
}
|
||||
|
||||
fn components(&self) -> &[f64] {
|
||||
match self {
|
||||
Ink::Gray(g) => std::slice::from_ref(g),
|
||||
@@ -166,8 +176,11 @@ impl AddedText {
|
||||
crate::paragraph_align::align_of(&self.aligns, i, self.align)
|
||||
}
|
||||
|
||||
/// Paint the text in `ink` (its look becomes [`AddedText::color`]), or in RGB with `None`.
|
||||
/// Paint the text in `ink` (its look becomes [`AddedText::color`]), or in RGB with `None`. An
|
||||
/// ink in range is kept as painted ([`Ink::painted`]); one out of range stays as given, for
|
||||
/// saving to refuse.
|
||||
pub fn set_ink(&mut self, ink: Option<Ink>) {
|
||||
let ink = ink.map(|i| if i.valid() { i.painted() } else { i });
|
||||
if let Some(i) = ink {
|
||||
self.color = i.rgb();
|
||||
}
|
||||
|
||||
@@ -1120,7 +1120,7 @@ fn a_hand_edited_ink_reads_back_only_when_it_is_the_stored_colours_look() {
|
||||
add_content(&mut doc, 0, &Content::Text(AddedText { rect: [72.0, 600.0, 300.0, 700.0], text: "Proof".into(), ..AddedText::default() })).unwrap();
|
||||
let obj = list_added(&doc)[0].obj;
|
||||
let real = |v: &[f64]| Object::Array(v.iter().map(|x| Object::Real(*x)).collect());
|
||||
let read = |entries: &[(&[u8], Object)]| {
|
||||
let read_text = |entries: &[(&[u8], Object)]| {
|
||||
let mut edited = doc.clone();
|
||||
let Object::Stream(mut s) = (*edited.get(obj)).clone() else { panic!("an item is a stream") };
|
||||
let mut params = s.dict.get(b"PCAdded").and_then(|p| p.as_dict().cloned()).unwrap();
|
||||
@@ -1130,11 +1130,15 @@ fn a_hand_edited_ink_reads_back_only_when_it_is_the_stored_colours_look() {
|
||||
s.dict.set(b"PCAdded".to_vec(), Object::Dict(params));
|
||||
edited.set(obj, Object::Stream(s));
|
||||
let Content::Text(t) = list_added(&edited)[0].content.clone() else { panic!() };
|
||||
t.ink
|
||||
t
|
||||
};
|
||||
let read = |entries: &[(&[u8], Object)]| read_text(entries).ink;
|
||||
// As PDFCraft writes it: the ink stands.
|
||||
assert_eq!(read(&[(b"Color", real(&[0.5; 3])), (b"Gray", Object::Real(0.5))]), Some(Ink::Gray(0.5)));
|
||||
assert_eq!(read(&[(b"Color", real(&[0.0; 3])), (b"CMYK", real(&[0.0, 0.0, 0.0, 1.0]))]), Some(Ink::Cmyk([0.0, 0.0, 0.0, 1.0])));
|
||||
// Finer than it paints, it reads back as painted (`0.5 g`), its look with it.
|
||||
let fine = read_text(&[(b"Color", real(&[0.50049; 3])), (b"Gray", Object::Real(0.50049))]);
|
||||
assert_eq!((fine.ink, fine.color), (Some(Ink::Gray(0.5)), [0.5; 3]));
|
||||
for (what, entries) in [
|
||||
("a colour the ink isn't the look of", vec![(&b"Color"[..], real(&[1.0, 0.0, 0.0])), (b"CMYK", real(&[0.0, 0.0, 0.0, 1.0]))]),
|
||||
("three CMYK numbers", vec![(b"Color", real(&[0.0; 3])), (b"CMYK", real(&[0.0, 0.0, 1.0]))]),
|
||||
@@ -1142,6 +1146,17 @@ fn a_hand_edited_ink_reads_back_only_when_it_is_the_stored_colours_look() {
|
||||
("CMYK not an array", vec![(b"Color", real(&[0.0; 3])), (b"CMYK", Object::Real(1.0))]),
|
||||
("gray out of range", vec![(b"Color", real(&[1.0; 3])), (b"Gray", Object::Real(2.0))]),
|
||||
("gray not a number", vec![(b"Color", real(&[0.0; 3])), (b"Gray", Object::name("Black"))]),
|
||||
(
|
||||
"a CMYK entry not a number",
|
||||
vec![
|
||||
(b"Color", real(&[0.0; 3])),
|
||||
(b"CMYK", Object::Array(vec![Object::Real(0.0), Object::name("Bad"), Object::Real(0.0), Object::Real(0.0), Object::Real(1.0)])),
|
||||
],
|
||||
),
|
||||
(
|
||||
"gray not a number beside a CMYK",
|
||||
vec![(b"Color", real(&[0.0; 3])), (b"Gray", Object::name("Black")), (b"CMYK", real(&[0.0, 0.0, 0.0, 1.0]))],
|
||||
),
|
||||
("gray NaN", vec![(b"Color", real(&[0.0; 3])), (b"Gray", Object::Real(f64::NAN))]),
|
||||
("CMYK infinite", vec![(b"Color", real(&[0.0; 3])), (b"CMYK", real(&[0.0, 0.0, 0.0, f64::INFINITY]))]),
|
||||
] {
|
||||
@@ -1149,6 +1164,40 @@ fn a_hand_edited_ink_reads_back_only_when_it_is_the_stored_colours_look() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #6440: an ink finer than the thousandth `g` and `k` are written to is kept as painted, so the
|
||||
/// item, its stored `/CMYK` and what it reads back as are what the page shows.
|
||||
#[test]
|
||||
fn an_ink_is_kept_as_it_is_painted() {
|
||||
use crate::added::Ink;
|
||||
let mut doc = fixture();
|
||||
let mut t = AddedText { rect: [72.0, 600.0, 300.0, 700.0], text: "Proof".into(), size: 14.0, ..AddedText::default() };
|
||||
t.set_ink(Some(Ink::Cmyk([0.12345, 0.00049, 0.0, 0.99951])));
|
||||
let painted = Ink::Cmyk([0.123, 0.0, 0.0, 1.0]);
|
||||
assert_eq!((t.ink, t.color), (Some(painted), painted.rgb()));
|
||||
add_content(&mut doc, 0, &Content::Text(t)).unwrap();
|
||||
let doc = reopen(&doc);
|
||||
let item = &list_added(&doc)[0];
|
||||
let Object::Stream(s) = &*doc.get(item.obj) else { panic!("an item is a stream") };
|
||||
let stream = String::from_utf8_lossy(&s.decoded().unwrap()).into_owned();
|
||||
assert!(
|
||||
stream.contains(
|
||||
"/PCFHelvetica 14 Tf 0.123 0 0 1 k
|
||||
"
|
||||
),
|
||||
"{stream}"
|
||||
);
|
||||
let stored = s
|
||||
.dict
|
||||
.get(b"PCAdded")
|
||||
.and_then(|p| p.as_dict())
|
||||
.and_then(|p| p.get(b"CMYK"))
|
||||
.and_then(Object::as_array)
|
||||
.map(|a| a.iter().filter_map(Object::as_f64).collect::<Vec<_>>());
|
||||
assert_eq!(stored, Some(vec![0.123, 0.0, 0.0, 1.0]));
|
||||
let Content::Text(back) = &item.content else { panic!() };
|
||||
assert_eq!(back.painted_ink(), Some(painted));
|
||||
}
|
||||
|
||||
/// #4464: a page left with one added item draws that item and nothing else.
|
||||
#[test]
|
||||
fn an_isolated_item_is_all_that_is_left_on_its_page() {
|
||||
|
||||
Reference in new issue
Block a user