write_gradient_fill.rs

Die Rust-Datei des Beispiels „Mit einem Verlauf gefüllter Text und Formen“. Ein Verlauf als die Farbe, mit der etwas gefüllt wird: eine Schlagzeile, deren Buchstaben Fenster auf eine Mischung aus drei Farben sind, ein mit einem kreisförmigen Verlauf gefülltes Feld, das die Seite durchscheinen lässt, und ein Band, dessen Mischung aufrecht gestellt und dessen Rahmen mit dem kreisförmigen Verlauf gemalt ist.

Rust 343 Zeilen

Wofür dieses Beispiel da ist

Eine Schlagzeile in einer Farbmischung gehört auf ein Plakat, einen Umschlag, eine Titelseite — und der übliche Weg dahin ist, sie in einem Zeichenprogramm zu bauen und das Bild einzufügen. Dann ist der Text kein Text mehr: Er lässt sich nicht durchsuchen, nicht kopieren und nicht übersetzen, ohne zu dem zurückzugehen, der ihn gemacht hat.

Was dieses Beispiel zeigt

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
//! Fills text and a shape with a gradient, through a shading pattern.
//!
//! Where `write_gradient` paints a gradient into a clipped box, this makes a
//! gradient the colour something is *filled* with: a headline whose letters are
//! windows onto a three-stop blend, and a panel filled with a radial one. The
//! gradient lives in the page's own space, so the letters show the part of it
//! behind them.
//!
//! A band at the foot of the page is filled with a gradient laid along the
//! pattern's own horizontal axis and turned upright by the pattern's matrix,
//! and edged with the panel's radial gradient. The panel's pattern carries a
//! graphics state of its own, which paints it at 70 % opacity.
//!
//! The three captions are held in `Words`, once per language, and
//! `HQF_PDF_LANG` picks which set is drawn. The headline is the library's own
//! name and reads the same in every language.
//!
//! Usage: `cargo run --example write_gradient_fill -- tmp/gradient_fill.pdf
//! [font.ttf]`
//!        `HQF_PDF_LANG=fr cargo run --example write_gradient_fill --
//! tmp/degrade_remplissage.pdf`

use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use hqf_pdf::content::Content;
use hqf_pdf::{
    Axial, DeviceSpace, Document, Exponential, ExtGState, ExtendEnds, Font, FontHandle, Page,
    Paint, Radial, Rgb, Shading, ShadingPattern, Stitching,
};

#[path = "shared/out.rs"]
mod out;

#[path = "shared/licence.rs"]
mod licence;

#[path = "shared/language.rs"]
mod language;

#[path = "shared/failure.rs"]
mod failure;

use language::Language;

/// The font the example draws with when none is given on the command line.
fn default_font() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fonts")
        .join("DejaVuSans.ttf")
}

/// The headline the gradient fills, which is the library's own name.
const HEADLINE: &str = "hqf-pdf";

/// The words the page is written in, one set per language.
#[derive(Debug)]
struct Words {
    /// The caption under the headline.
    letters: &'static str,
    /// The caption under the panel.
    shape: &'static str,
    /// The caption under the band.
    placed: &'static str,
}

impl Words {
    /// The words the page is written in, in `language`.
    fn of(language: Language) -> &'static Self {
        language::pick(&WORDS, language)
    }
}

/// The page in English.
const ENGLISH: Words = Words {
    letters: "Text filled with a gradient, through a shading pattern.",
    shape: "A radial gradient, painted by its pattern at 70 % opacity.",
    placed: "A gradient laid flat, stood upright by its pattern's matrix, edged in radial.",
};

/// The page in French.
const FRENCH: Words = Words {
    letters: "Du texte rempli par un dégradé, au moyen d’un motif.",
    shape: "Un dégradé radial, peint par son motif à 70 % d’opacité.",
    placed: "Un dégradé couché, redressé par la matrice de son motif, bordé en radial.",
};

/// The page in Dutch.
const DUTCH: Words = Words {
    letters: "Tekst gevuld met een verloop, via een arceringspatroon.",
    shape: "Een radiaal verloop, geschilderd door zijn patroon op 70 % dekking.",
    placed: "Een plat verloop, rechtgezet door de patroonmatrix; radiale rand.",
};

/// The page in Portuguese.
const PORTUGUESE: Words = Words {
    letters: "Texto preenchido com um degradé, por um padrão de sombreado.",
    shape: "Um degradé radial, pintado pelo seu padrão a 70 % de opacidade.",
    placed: "Um degradé deitado, endireitado pela matriz do padrão, orlado em radial.",
};

/// The page in Italian.
const ITALIAN: Words = Words {
    letters: "Testo riempito con un gradiente, tramite un motivo di sfumatura.",
    shape: "Un gradiente radiale, dipinto dal suo motivo al 70 % di opacità.",
    placed: "Un gradiente disteso, raddrizzato dalla matrice del motivo; bordo radiale.",
};

/// The page in Spanish.
const SPANISH: Words = Words {
    letters: "Texto relleno con un degradado, mediante un patrón de sombreado.",
    shape: "Un degradado radial, pintado por su patrón al 70 % de opacidad.",
    placed: "Un degradado tumbado, enderezado por la matriz del patrón; borde radial.",
};

/// The page in German.
const GERMAN: Words = Words {
    letters: "Text, über ein Schattierungsmuster mit einem Verlauf gefüllt.",
    shape: "Ein radialer Verlauf, vom Muster mit 70 % Deckkraft gemalt.",
    placed: "Ein flacher Verlauf, von der Mustermatrix aufgerichtet, radial umrandet.",
};

/// Every language the example is written in. A language is added by writing its
/// own set of words and naming it here.
static WORDS: [(Language, &Words); 7] = [
    (Language::English, &ENGLISH),
    (Language::French, &FRENCH),
    (Language::German, &GERMAN),
    (Language::Spanish, &SPANISH),
    (Language::Italian, &ITALIAN),
    (Language::Portuguese, &PORTUGUESE),
    (Language::Dutch, &DUTCH),
];

/// Draws one line of text with its baseline at `(x, y)`, in whatever fill
/// colour is in force.
fn text(
    content: &mut Content,
    font: &FontHandle,
    size: f64,
    x: f64,
    y: f64,
    line: &str,
) -> Result<(), hqf_pdf::Error> {
    content.begin_text();
    content.set_font(font, size)?;
    content.text_origin(x, y)?;
    content.show_glyphs(&font.glyphs(line));
    content.end_text();
    Ok(())
}

/// A gradient from red to blue along the pattern's horizontal axis, `height`
/// long, turned upright by the pattern's matrix and stood at `(x, y)`.
fn turned_upright(x: f64, y: f64, height: f64) -> ShadingPattern {
    ShadingPattern::new(
        Axial::rgb(
            (0.0, 0.0),
            (height, 0.0),
            Rgb::new(0.85, 0.12, 0.12),
            Rgb::new(0.12, 0.2, 0.8),
        )
        .extend(ExtendEnds {
            before: true,
            after: true,
        }),
    )
    .matrix([0.0, 1.0, -1.0, 0.0, x, y])
}

fn main() -> std::process::ExitCode {
    failure::reported(run())
}

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let language = Language::from_environment()?;
    let words = Words::of(language);

    let mut args = env::args().skip(1);
    // A named file is written as named; the default one carries the language,
    // so the two languages do not overwrite each other in `tmp/`.
    let out = args
        .next()
        .unwrap_or_else(|| language.file_name(&out::default_path("gradient_fill")));
    let font_path = args.next().map_or_else(default_font, PathBuf::from);

    let font = Font::parse(fs::read(&font_path)?)?;
    let mut doc = Document::new();
    doc.set_license(licence::licensed());
    let handle = doc.add_font(font);

    let left = 70.0;

    // A three-stop gradient spanning the width of the headline, set as a fill
    // pattern so the letters show it through their shapes.
    let headline_size = 84.0;
    let headline_baseline = 660.0;
    let headline_width = handle.measure(HEADLINE, headline_size);
    let ramp = Stitching::new(
        [0.0, 1.0],
        vec![
            Exponential::new([0.0, 1.0], 1.0)
                .endpoints(vec![0.85, 0.12, 0.12], vec![0.12, 0.55, 0.2])
                .into(),
            Exponential::new([0.0, 1.0], 1.0)
                .endpoints(vec![0.12, 0.55, 0.2], vec![0.12, 0.2, 0.8])
                .into(),
        ],
        vec![0.5],
        vec![[0.0, 1.0], [0.0, 1.0]],
    );
    let letters = doc.add_shading_pattern(Shading::from(Axial::new(
        (left, headline_baseline),
        (left + headline_width, headline_baseline),
        DeviceSpace::Rgb,
        ramp,
    )?))?;

    // A radial gradient filling a panel below the headline, painted under a
    // state of the pattern's own.
    let (px, py, pw, ph) = (left, 350.0, 455.0, 190.0);
    let see_through = doc.add_ext_gstate(ExtGState::new().fill_alpha(0.7));
    let panel = doc.add_shading_pattern(
        ShadingPattern::new(
            Radial::rgb(
                (px + pw / 2.0, py + ph / 2.0, 0.0),
                (px + pw / 2.0, py + ph / 2.0, pw / 2.0),
                Rgb::new(1.0, 1.0, 1.0),
                Rgb::new(0.1, 0.15, 0.45),
            )
            .extend(ExtendEnds {
                before: false,
                after: true,
            }),
        )
        .state(&see_through),
    )?;

    // A gradient laid from red to blue along the pattern's own horizontal axis,
    // over the band's height; the matrix turns that axis upright and stands it
    // at the band's foot.
    let (bx, by, bw, bh) = (left, 150.0, 455.0, 120.0);
    let upright = doc.add_shading_pattern(turned_upright(bx, by, bh))?;

    let mut content = Content::new();

    // The headline, its letters filled with the gradient.
    content.save_state();
    content.set_fill_pattern(&letters);
    text(
        &mut content,
        &handle,
        headline_size,
        left,
        headline_baseline,
        HEADLINE,
    )?;
    content.restore_state();

    // A caption, in plain black.
    text(
        &mut content,
        &handle,
        13.0,
        left,
        headline_baseline - 26.0,
        words.letters,
    )?;

    // The panel, filled with the radial gradient.
    content.save_state();
    content.set_fill(Paint::pattern(&panel))?;
    content.rect(px, py, pw, ph)?.fill();
    content.restore_state();

    text(&mut content, &handle, 12.0, px, py - 16.0, words.shape)?;

    // The band, filled with the turned gradient and edged with the panel's.
    content.save_state();
    content.set_fill(Paint::Pattern(upright))?;
    content.set_stroke_pattern(&panel);
    content.set_line_width(6.0)?;
    content.rect(bx, by, bw, bh)?.fill_and_stroke();
    content.restore_state();

    text(&mut content, &handle, 12.0, bx, by - 16.0, words.placed)?;

    let mut page = Page::a4();
    page.content = content.into_bytes();
    doc.add_page(page)?;

    let bytes = doc.to_bytes()?;
    if let Some(parent) = Path::new(&out).parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(&out, &bytes)?;
    println!("wrote {out}: {} bytes", bytes.len());
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{WORDS, default_font, language};

    /// The lines two languages are allowed to write the same way. There are
    /// none: every word is a word of the language it is written in.
    const SPARED: [&str; 0] = [];

    #[test]
    fn every_language_draws_the_page_in_its_own_words() {
        let untranslated = language::untranslated_lines(&WORDS, &SPARED);

        assert!(
            untranslated.is_empty(),
            "the page says these in more than one language: {untranslated:?}"
        );
    }

    /// A caption is set on one line at 12 points, and runs no further than the
    /// panel above it.
    #[test]
    fn every_caption_stays_as_wide_as_the_panels() {
        let mut doc = hqf_pdf::Document::new();
        let font = doc.add_font(
            hqf_pdf::Font::parse(
                std::fs::read(default_font()).expect("the committed font is there"),
            )
            .expect("the committed font parses"),
        );
        for (_, words) in WORDS {
            for (size, caption) in [
                (13.0, words.letters),
                (12.0, words.shape),
                (12.0, words.placed),
            ] {
                let width = font.measure(caption, size);
                assert!(width <= 455.0, "{caption:?} needs {width} of 455");
            }
        }
    }
}