mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-11 16:31:54 -06:00
Watermark: text watermark transparency (#3001)
Ported from #3001 by hand onto current main (the PR was 1857 commits behind). The text watermark gets a transparency in percent next to Word's Semitransparent check box: 0 opaque, 50 Semitransparent, any other value kept (`v:fill opacity`). - doc: `Watermark::transparency` (Option<u8>), `transparency()`, `set_transparency()`, `opacity()`; older documents without it follow `semitransparent`. - DOCX: reads any `v:fill opacity`, writes `.75`, `.2`, ... like Word writes `.5`. - Render and PDF draw the same opacity. Changed vs the PR's base: the screen used 35 % for Semitransparent and 80 % for opaque; both now use the real opacity (50 % / 100 %), as the PDF already did. - Command `design.watermark`: `transparency` (0-100, non-finite and out-of-range clamped) and `semitransparent`. - Dialog: Semitransparent check box and a percentage. - Beyond the PR: the ODT writer and reader keep the transparency (`draw:opacity`) instead of only on/off. Co-Authored-By: C00kieMonsta <22135261+C00kieMonsta@users.noreply.github.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FypgCVCCEo7u5wetHaJRU4
This commit is contained in:
co-authored by
C00kieMonsta
Claude Sonnet 5.5
parent
8c57b0775c
commit
0261e92d10
@@ -234,7 +234,12 @@ pub struct Watermark {
|
||||
pub font: String,
|
||||
pub color: Rgb,
|
||||
pub diagonal: bool,
|
||||
/// Word's Semitransparent check box (the text 50 % transparent).
|
||||
pub semitransparent: bool,
|
||||
/// The text's transparency in percent when it is neither 0 nor 50 (see
|
||||
/// [`Watermark::transparency`]); `None` follows `semitransparent`, as older documents do.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transparency: Option<u8>,
|
||||
/// The shape's box (its `style` width and height, points), which the text is stretched to
|
||||
/// fill (`v:textpath fitshape`); `None`: the box Word's Design › Watermark gives
|
||||
/// ([`Watermark::frame`]).
|
||||
@@ -250,12 +255,34 @@ impl Default for Watermark {
|
||||
color: Rgb(0xC0, 0xC0, 0xC0),
|
||||
diagonal: true,
|
||||
semitransparent: true,
|
||||
transparency: None,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Watermark {
|
||||
/// The text's transparency in percent: 0 is opaque, 100 invisible, Semitransparent is 50.
|
||||
pub fn transparency(&self) -> u8 {
|
||||
match self.transparency {
|
||||
Some(t) => t.min(100),
|
||||
None if self.semitransparent => 50,
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the text's transparency in percent (at most 100). Semitransparent follows it.
|
||||
pub fn set_transparency(&mut self, percent: u8) {
|
||||
let t = percent.min(100);
|
||||
self.semitransparent = t > 0;
|
||||
self.transparency = (t != 0 && t != 50).then_some(t);
|
||||
}
|
||||
|
||||
/// The text's opacity, 0 to 1.
|
||||
pub fn opacity(&self) -> f32 {
|
||||
1.0 - f32::from(self.transparency()) / 100.0
|
||||
}
|
||||
|
||||
/// The box the text fills on a page `page_w` × `page_h` (points): its own size, else half the
|
||||
/// page diagonal across (half the page width when not diagonal), as high as the text's length
|
||||
/// makes it. Centred on the margins and, when diagonal, turned 45° counterclockwise (Word's
|
||||
@@ -332,3 +359,30 @@ impl Default for Settings {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod watermark_tests {
|
||||
use super::Watermark;
|
||||
|
||||
#[test]
|
||||
fn transparency_follows_semitransparent() {
|
||||
let mut wm = Watermark::default();
|
||||
assert_eq!((wm.transparency(), wm.opacity()), (50, 0.5));
|
||||
wm.semitransparent = false;
|
||||
assert_eq!(wm.transparency(), 0);
|
||||
wm.set_transparency(80);
|
||||
assert_eq!((wm.semitransparent, wm.transparency, wm.transparency()), (true, Some(80), 80));
|
||||
// Word's two values are stored as the check box alone.
|
||||
wm.set_transparency(50);
|
||||
assert_eq!((wm.semitransparent, wm.transparency), (true, None));
|
||||
wm.set_transparency(0);
|
||||
assert_eq!((wm.semitransparent, wm.transparency), (false, None));
|
||||
wm.set_transparency(255);
|
||||
assert_eq!((wm.transparency(), wm.opacity()), (100, 0.0));
|
||||
// Older documents have no transparency field.
|
||||
let old: Watermark = serde_json::from_str(r#"{"text":"X","semitransparent":false}"#).unwrap();
|
||||
assert_eq!(old.transparency(), 0);
|
||||
let hostile: Watermark = serde_json::from_str(r#"{"transparency":200}"#).unwrap();
|
||||
assert_eq!(hostile.transparency(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,7 +605,7 @@ fn vml_float(shape: &El, style: &str, in_hf: bool) -> Float {
|
||||
|
||||
/// Word's text watermark (Design › Watermark): a VML shape in a header whose `v:textpath` draws
|
||||
/// its `string` as text (ECMA-376 Part 4, VML `textpath`), in the textpath's `font-family`, filled
|
||||
/// with the shape's `fillcolor`, semitransparent when its `v:fill` has an `opacity` below 1, and
|
||||
/// with the shape's `fillcolor`, as transparent as its `v:fill` `opacity` says (any value), and
|
||||
/// diagonal when the shape is turned. Only Word's watermark shape counts (its `id`), or text
|
||||
/// placed absolutely and centred both ways; other header WordArt (a logo or a title) is None.
|
||||
fn vml_watermark(shape: &El) -> Option<wordcraft_doc::Watermark> {
|
||||
@@ -638,7 +638,9 @@ fn vml_watermark(shape: &El) -> Option<wordcraft_doc::Watermark> {
|
||||
// The box the text is stretched to (`fitshape`).
|
||||
let len = |k: &str| vml_style(style, k).and_then(|v| measure(v, 1.0)).filter(|v| v.is_finite() && *v > 0.0).map(|v| v.min(4000.0));
|
||||
let size = len("width").zip(len("height")).map(|(w, h)| [w, h]);
|
||||
Some(wordcraft_doc::Watermark { text, font, color, diagonal: vml_spin(style).deg != 0.0, semitransparent: opacity < 1.0, size })
|
||||
let mut wm = wordcraft_doc::Watermark { text, font, color, diagonal: vml_spin(style).deg != 0.0, size, ..default };
|
||||
wm.set_transparency(((1.0 - opacity) * 100.0).round().clamp(0.0, 100.0) as u8);
|
||||
Some(wm)
|
||||
}
|
||||
|
||||
/// A VML `opacity`, 0..=1.
|
||||
|
||||
@@ -18,6 +18,18 @@ pub(super) fn text(wm: &Watermark) -> Option<String> {
|
||||
(!t.trim().is_empty()).then_some(t)
|
||||
}
|
||||
|
||||
/// The `v:fill` `opacity` for the text's transparency: Word's `.5` for Semitransparent. None when
|
||||
/// the text is opaque.
|
||||
fn opacity(wm: &Watermark) -> Option<String> {
|
||||
let t = wm.transparency();
|
||||
if t == 0 {
|
||||
return None;
|
||||
}
|
||||
let o = format!("{:.2}", f32::from(100 - t) / 100.0);
|
||||
let o = o.trim_end_matches('0').trim_end_matches('.');
|
||||
Some(o.strip_prefix("0.").map_or_else(|| o.to_string(), |f| format!(".{f}")))
|
||||
}
|
||||
|
||||
/// A `w:r` holding the watermark shape (and its shape type), for the first paragraph of header
|
||||
/// part number `n`, in its box ([`Watermark::frame`]: its own, or Word's default for the page).
|
||||
pub(super) fn run(wm: &Watermark, n: usize, page: &SectionProps) -> Option<String> {
|
||||
@@ -45,7 +57,7 @@ pub(super) fn run(wm: &Watermark, n: usize, page: &SectionProps) -> Option<Strin
|
||||
mso-position-horizontal:center;mso-position-horizontal-relative:margin;\
|
||||
mso-position-vertical:center;mso-position-vertical-relative:margin"
|
||||
);
|
||||
let fill = if wm.semitransparent { r#"<v:fill opacity=".5"/>"# } else { "" };
|
||||
let fill = opacity(wm).map(|o| format!(r#"<v:fill opacity="{o}"/>"#)).unwrap_or_default();
|
||||
Some(format!(
|
||||
concat!(
|
||||
r#"<w:r><w:rPr><w:noProof/></w:rPr><w:pict>"#,
|
||||
@@ -64,3 +76,18 @@ pub(super) fn run(wm: &Watermark, n: usize, page: &SectionProps) -> Option<Strin
|
||||
text = esc(&text),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn opacity_is_written_like_word() {
|
||||
let mut wm = Watermark::default();
|
||||
assert_eq!(opacity(&wm).as_deref(), Some(".5"));
|
||||
for (t, want) in [(0, None), (25, Some(".75")), (80, Some(".2")), (99, Some(".01")), (100, Some("0"))] {
|
||||
wm.set_transparency(t);
|
||||
assert_eq!(opacity(&wm).as_deref(), want, "{t}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1381,7 +1381,14 @@ fn vml_textpath_in_header_is_the_watermark() {
|
||||
// Horizontal, centred both ways without Word's id, opacity in 65536ths.
|
||||
let flat = r##"<v:shape type="#_x0000_t136" style="position:absolute;width:300pt;height:60pt;mso-position-horizontal:center;mso-position-vertical:center" fillcolor="#336699"><v:fill opacity="32768f"/><v:textpath string="ASAP"/></v:shape>"##;
|
||||
let wm = read(flat, true).0.expect("watermark");
|
||||
assert_eq!((wm.color, wm.diagonal, wm.semitransparent), (Rgb(0x33, 0x66, 0x99), false, true));
|
||||
assert_eq!((wm.color, wm.diagonal, wm.semitransparent, wm.transparency()), (Rgb(0x33, 0x66, 0x99), false, true, 50));
|
||||
|
||||
// Any opacity is read as a transparency percentage, not only Word's `.5`.
|
||||
for (opacity, want) in [(".25", 75), ("0", 100), ("1", 0), ("2", 0), ("-1", 100)] {
|
||||
let shape = flat.replace(r#"opacity="32768f""#, &format!(r#"opacity="{opacity}""#));
|
||||
let wm = read(&shape, true).0.expect("watermark");
|
||||
assert_eq!(wm.transparency(), want, "{opacity}");
|
||||
}
|
||||
|
||||
// Not watermarks: the same shape in the body (WordArt); header WordArt that isn't centred
|
||||
// and isn't Word's watermark (a logo); a textpath turned off; a watermark-like shape inside a
|
||||
|
||||
@@ -1561,7 +1561,10 @@ fn watermark_roundtrips() {
|
||||
diagonal: false,
|
||||
semitransparent: false,
|
||||
size: None,
|
||||
..Default::default()
|
||||
},
|
||||
// Any transparency, not only Word's two.
|
||||
Watermark { transparency: Some(80), ..Default::default() },
|
||||
// A box of its own (not Word's default for the page) is kept.
|
||||
Watermark { text: "COPY".into(), size: Some([300.0, 80.0]), ..Default::default() },
|
||||
] {
|
||||
@@ -1573,6 +1576,26 @@ fn watermark_roundtrips() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #2954: the text watermark's transparency round-trips as `v:fill` `opacity`; Word's
|
||||
/// Semitransparent is `.5`, and an opaque watermark has no `v:fill`.
|
||||
#[test]
|
||||
fn text_watermark_transparency_roundtrips() {
|
||||
for (t, fill) in [(0, None), (25, Some(".75")), (50, Some(".5")), (80, Some(".2")), (100, Some("0"))] {
|
||||
let mut d = doc_with(vec![Paragraph::with_text("Body", CharProps::default())]);
|
||||
let mut wm = Watermark::default();
|
||||
wm.set_transparency(t);
|
||||
d.settings.watermark = Some(wm.clone());
|
||||
let bytes = wordcraft_docx::write(&d).unwrap();
|
||||
let hdr = part_xml(&bytes, "word/header1.xml");
|
||||
match fill {
|
||||
Some(o) => assert!(hdr.contains(&format!(r#"<v:fill opacity="{o}"/>"#)), "{t}: {hdr}"),
|
||||
None => assert!(!hdr.contains("v:fill"), "{t}"),
|
||||
}
|
||||
let r = wordcraft_docx::read(&bytes).unwrap();
|
||||
assert_eq!(r.settings.watermark, Some(wm), "{t}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #3994: a watermark font name round-trips through the textpath's CSS-like style whatever it
|
||||
/// carries — apostrophes, semicolons, colons, quotes, backslashes. Before, those characters
|
||||
/// were deleted (the font was saved under a different name) and a trailing backslash escaped
|
||||
|
||||
@@ -17,6 +17,18 @@ fn tiny_png() -> Vec<u8> {
|
||||
out.into_inner()
|
||||
}
|
||||
|
||||
/// #2954: a text watermark's transparency survives Word 2003 XML (the same VML shape as in DOCX).
|
||||
#[test]
|
||||
fn text_watermark_transparency_round_trips_through_2003() {
|
||||
let mut d = Document::new();
|
||||
d.body = vec![para_block(Paragraph::with_text("Body", CharProps::default()))];
|
||||
let mut wm = wordcraft_doc::Watermark::default();
|
||||
wm.set_transparency(70);
|
||||
d.settings.watermark = Some(wm.clone());
|
||||
let back = read_xml(&write_wordml2003(&d).expect("write")).expect("read");
|
||||
assert_eq!(back.settings.watermark, Some(wm));
|
||||
}
|
||||
|
||||
fn paras(doc: &Document) -> Vec<&Paragraph> {
|
||||
doc.body.iter().filter_map(|b| b.as_para()).collect()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use serde_json::{Value, json};
|
||||
use std::sync::Arc;
|
||||
use wordcraft_doc::props::{Border, BorderStyle, Borders, CharProps, LineSpacing, Rgb, TextColor};
|
||||
use wordcraft_doc::{Watermark, styles};
|
||||
use wordcraft_doc::styles;
|
||||
|
||||
use super::sel_result;
|
||||
use crate::{CmdError, CmdResult, CommandSpec, Session, p};
|
||||
@@ -71,7 +71,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
CommandSpec::new("design.scaleFontSizes", "Scale Font Sizes", "Design › Document Formatting", scale_font_sizes)
|
||||
.params(r#"{"delta": pt (-1638..1638, not 0) | "percent": number (-99..1000, not 0; 10 = +10%)} (exactly one)"#),
|
||||
CommandSpec::new("design.watermark", "Watermark", "Design › Page Background", watermark)
|
||||
.params(r#"{"text"?: string, "remove"?: bool, "diagonal"?: bool, "color"?: "RRGGBB"}"#),
|
||||
.params(r#"{"text"?: string, "remove"?: bool, "diagonal"?: bool, "color"?: "RRGGBB", "transparency"?: 0-100 (percent; Word's Semitransparent is 50), "semitransparent"?: bool (50 or 0 percent)}"#),
|
||||
CommandSpec::new("design.pageColor", "Page Color", "Design › Page Background", |s, v| {
|
||||
s.doc.settings.page_color = p::str(v, "color").and_then(Rgb::parse);
|
||||
s.doc.settings.page_picture = None;
|
||||
@@ -262,7 +262,14 @@ fn watermark(s: &mut Session, v: &Value) -> CmdResult {
|
||||
if let Some(c) = p::str(v, "color").and_then(Rgb::parse) {
|
||||
wm.color = c;
|
||||
}
|
||||
s.doc.settings.watermark = Some(Watermark { ..wm });
|
||||
if let Some(on) = p::bool(v, "semitransparent") {
|
||||
wm.set_transparency(if on { 50 } else { 0 });
|
||||
}
|
||||
// Applied after `semitransparent`, so a percentage wins when both are given.
|
||||
if let Some(t) = p::f32(v, "transparency") {
|
||||
wm.set_transparency(if t.is_finite() { t.clamp(0.0, 100.0).round() as u8 } else { 0 });
|
||||
}
|
||||
s.doc.settings.watermark = Some(wm);
|
||||
sel_result(s)
|
||||
}
|
||||
|
||||
@@ -288,3 +295,33 @@ fn page_borders(s: &mut Session, v: &Value) -> CmdResult {
|
||||
}
|
||||
sel_result(s)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use wordcraft_doc::{Document, Watermark};
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn wm(s: &Session) -> Watermark {
|
||||
s.doc.settings.watermark.clone().unwrap()
|
||||
}
|
||||
|
||||
/// #2954: the text's transparency in percent, kept in step with Semitransparent.
|
||||
#[test]
|
||||
fn text_watermark_transparency() {
|
||||
let mut s = Session::new(Document::new());
|
||||
s.run("design.watermark", &json!({"text": "DRAFT"})).unwrap();
|
||||
assert_eq!((wm(&s).transparency(), wm(&s).semitransparent), (50, true));
|
||||
s.run("design.watermark", &json!({"transparency": 80})).unwrap();
|
||||
assert_eq!((wm(&s).transparency(), wm(&s).semitransparent), (80, true));
|
||||
s.run("design.watermark", &json!({"semitransparent": false})).unwrap();
|
||||
assert_eq!((wm(&s).transparency(), wm(&s).semitransparent), (0, false));
|
||||
s.run("design.watermark", &json!({"transparency": 250.7})).unwrap();
|
||||
assert_eq!(wm(&s).transparency(), 100);
|
||||
s.run("design.watermark", &json!({"transparency": -3})).unwrap();
|
||||
assert_eq!(wm(&s).transparency(), 0);
|
||||
s.run("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(wm(&s).transparency(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ pub(super) struct Body<'a> {
|
||||
/// A watermark found (in a header): see [`Self::start_shape`].
|
||||
pub(super) watermark: Option<wordcraft_doc::Watermark>,
|
||||
/// The shape being read is a watermark's, if its geometry follows its text: its size, turn,
|
||||
/// colour and see-through, and whether its geometry is fontwork.
|
||||
watermark_shape: Option<((f32, f32), f32, Option<wordcraft_doc::props::Rgb>, bool, bool)>,
|
||||
/// colour and transparency (percent), and whether its geometry is fontwork.
|
||||
watermark_shape: Option<((f32, f32), f32, Option<wordcraft_doc::props::Rgb>, u8, bool)>,
|
||||
pub(super) manifest: Option<&'a HashMap<String, String>>,
|
||||
containers: Vec<Vec<FBlock>>,
|
||||
/// Drawings anchored in the text but outside any paragraph (LibreOffice's page-anchored
|
||||
|
||||
@@ -176,7 +176,7 @@ impl Body<'_> {
|
||||
/// The shape ending is a watermark (see [`Self::start_shape`]) holding `blocks`: the
|
||||
/// document's watermark, from its text, font, colour, size and turn, and not a shape.
|
||||
fn take_watermark(&mut self, blocks: &[FBlock]) -> bool {
|
||||
let Some(((w, h), rot, color, see_through, true)) = self.watermark_shape.take() else { return false };
|
||||
let Some(((w, h), rot, color, transparency, true)) = self.watermark_shape.take() else { return false };
|
||||
let mut text = String::new();
|
||||
let mut font = None;
|
||||
for b in blocks {
|
||||
@@ -195,14 +195,16 @@ impl Body<'_> {
|
||||
}
|
||||
let turned = rot.rem_euclid(360.0);
|
||||
let base = wordcraft_doc::Watermark::default();
|
||||
self.watermark = Some(wordcraft_doc::Watermark {
|
||||
let mut wm = wordcraft_doc::Watermark {
|
||||
text,
|
||||
font: font.unwrap_or(base.font),
|
||||
color: color.unwrap_or(base.color),
|
||||
diagonal: turned > 1.0 && turned < 359.0,
|
||||
semitransparent: see_through,
|
||||
size: Some([w, h]).filter(|[w, h]| w.is_finite() && h.is_finite() && *w > 0.0 && *h > 0.0),
|
||||
});
|
||||
..base
|
||||
};
|
||||
wm.set_transparency(transparency);
|
||||
self.watermark = Some(wm);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -372,8 +374,8 @@ impl Body<'_> {
|
||||
self.frame_look.conn = self.conn_of(a).map(Box::new);
|
||||
// Word's and LibreOffice's watermark: a shape of this name whose geometry follows its text.
|
||||
self.watermark_shape = get(a, "draw:name").filter(|n| n.starts_with(crate::odt::writer::WATERMARK_NAME)).map(|_| {
|
||||
let (color, see_through) = g.watermark_look();
|
||||
((w, h), float.rot, color, see_through, false)
|
||||
let (color, transparency) = g.watermark_look();
|
||||
((w, h), float.rot, color, transparency, false)
|
||||
});
|
||||
self.shape_path = crate::odt::shapes::freeform(local(q), a, w, h).map(std::sync::Arc::new);
|
||||
self.shape_kind = Some(kind);
|
||||
|
||||
@@ -181,9 +181,9 @@ impl Graphic {
|
||||
}
|
||||
}
|
||||
|
||||
/// A watermark's look from its shape's style: its fill colour, and whether it is see-through.
|
||||
pub(super) fn watermark_look(&self) -> (Option<Rgb>, bool) {
|
||||
(self.fill_color, self.opacity.is_some_and(|o| o < 100.0))
|
||||
/// A watermark's look from its shape's style: its fill colour, and its transparency in percent.
|
||||
pub(super) fn watermark_look(&self) -> (Option<Rgb>, u8) {
|
||||
(self.fill_color, self.opacity.filter(|o| o.is_finite()).map_or(0, |o| (100.0 - o).round().clamp(0.0, 100.0) as u8))
|
||||
}
|
||||
|
||||
/// This placement with `parent`'s filling what it leaves unsaid.
|
||||
|
||||
@@ -272,6 +272,15 @@ fn a_watermark_is_a_shape_in_the_header_and_reads_back_as_one() {
|
||||
let back = import(&saved).unwrap();
|
||||
let wm = back.settings.watermark.clone().expect("the watermark");
|
||||
assert_eq!((wm.text.as_str(), wm.diagonal, wm.semitransparent), ("CONFIDENTIAL", true, true));
|
||||
// Any transparency is kept, not only half.
|
||||
for t in [0u8, 30, 50, 80] {
|
||||
let mut other = d.clone();
|
||||
let mut w = wordcraft_doc::Watermark { text: "CONFIDENTIAL".into(), ..Default::default() };
|
||||
w.set_transparency(t);
|
||||
other.settings.watermark = Some(w.clone());
|
||||
let back = import(&export(&other).unwrap()).unwrap();
|
||||
assert_eq!(back.settings.watermark.map(|w| w.transparency()), Some(t), "{t}");
|
||||
}
|
||||
// Not a shape in the header too.
|
||||
let header = back.last_section.headers.default.and_then(|id| back.parts.get(&id)).unwrap();
|
||||
assert!(header.blocks.iter().filter_map(|b| b.as_para()).all(|p| p.objects.is_empty()));
|
||||
|
||||
@@ -603,11 +603,14 @@ impl Writer {
|
||||
/// The document's watermark as LibreOffice and Word write one in a header: a fontwork shape
|
||||
/// (§10.3.2 `draw:custom-shape` whose geometry follows its text, `draw:text-path`) behind the
|
||||
/// text, centred on a page whose text area is `area` (width, height; points), turned up 45°
|
||||
/// when diagonal, filled with its colour (half transparent when semitransparent). It doesn't
|
||||
/// when diagonal, filled with its colour (as transparent as the watermark says). It doesn't
|
||||
/// grow to its text: its text is stretched to it.
|
||||
pub(in crate::odt) fn watermark(&mut self, wm: &wordcraft_doc::Watermark, page: (f32, f32), area: (f32, f32)) -> String {
|
||||
let [w, h] = wm.frame(page.0, page.1);
|
||||
let opacity = if wm.semitransparent { " draw:opacity=\"50%\"" } else { "" };
|
||||
let opacity = match wm.transparency() {
|
||||
0 => String::new(),
|
||||
t => format!(" draw:opacity=\"{}%\"", 100 - t),
|
||||
};
|
||||
let style = self.graphic_style(format!(
|
||||
" draw:stroke=\"none\" draw:fill=\"solid\" draw:fill-color=\"#{}\"{opacity} style:run-through=\"background\" style:wrap=\"run-through\" style:horizontal-pos=\"center\" style:horizontal-rel=\"page-content\" style:vertical-pos=\"middle\" style:vertical-rel=\"page-content\" style:flow-with-text=\"false\" draw:auto-grow-height=\"false\" draw:auto-grow-width=\"false\"",
|
||||
wm.color.hex().to_ascii_lowercase()
|
||||
|
||||
@@ -374,7 +374,7 @@ impl Exporter<'_> {
|
||||
.collect();
|
||||
s.push_transform(&Transform::from_row(c * stretch, sn * stretch, -sn, c, c * dx - sn * dy + cx, sn * dx + c * dy + cy));
|
||||
s.set_stroke(None);
|
||||
s.set_fill(Some(fill(wm.color, if wm.semitransparent { 0.5 } else { 1.0 })));
|
||||
s.set_fill(Some(fill(wm.color, wm.opacity())));
|
||||
match font {
|
||||
Some(font) => {
|
||||
s.draw_glyphs(Point::from_xy(0.0, 0.0), &glyphs, font, &text, size, false);
|
||||
|
||||
@@ -480,7 +480,7 @@ fn draw_watermark(ctx: &mut RenderContext, view: Affine, page: &Page, wm: &wordc
|
||||
* Affine::rotate(angle)
|
||||
* Affine::translate((-bw / 2.0, bh / 2.0 - descent * bh))
|
||||
* Affine::scale_non_uniform(stretch, 1.0);
|
||||
ctx.set_paint(color(opts.ink(wm.color), if wm.semitransparent { 0.35 } else { 0.8 }));
|
||||
ctx.set_paint(color(opts.ink(wm.color), wm.opacity()));
|
||||
let db = FontDb::global();
|
||||
let mut x = 0.0;
|
||||
for g in &glyphs {
|
||||
|
||||
@@ -3,6 +3,7 @@ use wordcraft_layout::display::page_display;
|
||||
use wordcraft_layout::{LayoutCache, LayoutOptions, layout};
|
||||
|
||||
mod color_glyphs;
|
||||
mod watermark;
|
||||
|
||||
/// The first and last pixel rows with ink in a black-on-white [`render_text_line`] raster.
|
||||
fn ink_rows(img: &Rendered) -> Option<(usize, usize)> {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//! Design › Watermark on screen.
|
||||
|
||||
use super::*;
|
||||
|
||||
/// #2954: a text watermark at 80 % transparency is lighter than at 0 %, and invisible at 100 %.
|
||||
#[test]
|
||||
fn text_watermark_transparency() {
|
||||
let mut d = Document::new();
|
||||
let darkest = |d: &Document| {
|
||||
let l = layout(d, &mut LayoutCache::new(), &LayoutOptions::default());
|
||||
let img = render_page(d, &l.pages[0], 1.0, &RenderOptions::default());
|
||||
img.pixels.chunks(4).map(|p| u32::from(p[0]) + u32::from(p[1]) + u32::from(p[2])).min().unwrap()
|
||||
};
|
||||
let mut wm = wordcraft_doc::Watermark { text: "WWW".into(), color: wordcraft_doc::props::Rgb::BLACK, diagonal: false, ..Default::default() };
|
||||
let mut at = |t: u8| {
|
||||
wm.set_transparency(t);
|
||||
d.settings.watermark = Some(wm.clone());
|
||||
darkest(&d)
|
||||
};
|
||||
let (opaque, half, light, gone) = (at(0), at(50), at(80), at(100));
|
||||
assert!(opaque < 30, "{opaque}");
|
||||
assert!(half > opaque + 300 && light > half + 150, "{opaque} {half} {light}");
|
||||
assert_eq!(gone, 765);
|
||||
}
|
||||
@@ -218,6 +218,8 @@ pub enum Dialog {
|
||||
Watermark {
|
||||
text: String,
|
||||
diagonal: bool,
|
||||
/// Percent, 0 to 100; Semitransparent is 50.
|
||||
transparency: u8,
|
||||
},
|
||||
/// Home › Font › Phonetic Guide: ruby text over the selected text (`format.phonetic`).
|
||||
Phonetic {
|
||||
@@ -731,7 +733,17 @@ impl Dialog {
|
||||
"bookmark" => Dialog::Bookmark { name: bookmark_at_caret(&app.session), by_location: false, hidden: false, deleted: false },
|
||||
"wordCount" => Dialog::WordCount { stats: app.session.run("review.wordCount", &json!({})).unwrap_or_default() },
|
||||
"zoom" => Dialog::Zoom { percent: (app.session.view.zoom * 100.0).round() },
|
||||
"watermark" => Dialog::Watermark { text: "CONFIDENTIAL".into(), diagonal: true },
|
||||
"watermark" => {
|
||||
// The document's watermark when it has one, else Word's defaults.
|
||||
let wm = app
|
||||
.session
|
||||
.doc
|
||||
.settings
|
||||
.watermark
|
||||
.clone()
|
||||
.unwrap_or_else(|| wordcraft_doc::Watermark { text: "CONFIDENTIAL".into(), ..Default::default() });
|
||||
Dialog::Watermark { transparency: wm.transparency(), diagonal: wm.diagonal, text: wm.text }
|
||||
}
|
||||
"phonetic" => {
|
||||
let v = wordcraft_engine::cmd::phonetic::state(&app.session);
|
||||
let f = |k: &str| v.get(k).and_then(Value::as_f64).unwrap_or(0.0) as f32;
|
||||
|
||||
@@ -871,20 +871,28 @@ fn zoom_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
}
|
||||
|
||||
fn watermark_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
let Dialog::Watermark { text, diagonal } = d else {
|
||||
let Dialog::Watermark { text, diagonal, transparency } = d else {
|
||||
return false;
|
||||
};
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Text:"));
|
||||
ui.text_edit_singleline(text);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
let mut semi = *transparency > 0;
|
||||
if ui.checkbox(&mut semi, tl!("Semitransparent")).changed() {
|
||||
*transparency = if semi { 50 } else { 0 };
|
||||
}
|
||||
ui.label(tl!("Transparency:"));
|
||||
ui.add(egui::DragValue::new(transparency).range(0..=100).suffix(" %"));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(diagonal, true, tl!("Diagonal"));
|
||||
ui.radio_value(diagonal, false, tl!("Horizontal"));
|
||||
});
|
||||
let (ok, cancel) = buttons(ui, tl!("OK"));
|
||||
if ok {
|
||||
let _ = app.run("design.watermark", json!({"text": text, "diagonal": *diagonal}));
|
||||
let _ = app.run("design.watermark", json!({"text": text, "diagonal": *diagonal, "transparency": *transparency}));
|
||||
}
|
||||
ok || cancel
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user