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 | //! Pours one long passage into three columns, flowing from the foot of each to
//! the head of the next.
//!
//! The passage is broken once to the column width, then set column by column:
//! each column fits as many of the lines as its height allows, draws that
//! slice, and the next column takes up where it stopped. A thin rule outlines
//! each column so the text can be seen reaching its foot and carrying on.
//!
//! The passage is held in `Words`, once per language, and `HQF_PDF_LANG` picks
//! which one is poured. The measure, the gap and the height are not language:
//! they are the same three columns whoever reads the page.
//!
//! Usage: `cargo run --example write_columns -- tmp/columns.pdf [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_columns -- tmp/colonnes.pdf`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::layout::Align;
use hqf_pdf::{Document, Font, Page, TextFlow};
#[path = "shared/out.rs"]
mod out;
#[path = "shared/licence.rs"]
mod licence;
#[path = "shared/language.rs"]
mod language;
use language::Language;
/// The font the example draws with when none is given on the command line: the
/// one committed for the tests, so that the example runs on any machine.
fn default_font() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fonts")
.join("DejaVuSans.ttf")
}
/// The words the page is written in, one set per language.
#[derive(Debug)]
struct Words {
/// The passage poured through the columns, long enough to reach the third.
passage: &'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 {
passage: "A flow too long for one column is set column by column. \
Each column takes as many of the broken lines as its height allows, draws them, \
and hands the rest to the column beside it, which takes up exactly where the \
last one stopped. The words are broken once, to the width one column has, so \
every column is set to the same measure. A line that would fall below a \
column's foot opens the next column instead, and the reading runs down one \
column and on into the top of the next with no line lost and none set twice. \
This is what sets a newspaper's text, a report's two columns, or the narrow \
measure a wide page reads best in.",
};
/// The page in French.
const FRENCH: Words = Words {
passage: "Un texte trop long pour une seule colonne se compose colonne \
par colonne. Chaque colonne prend autant de lignes coupées que sa hauteur le \
permet, les dessine, et passe le reste à sa voisine, qui reprend là où la \
précédente s'est arrêtée. Les mots ne sont coupés qu'une fois, à la largeur \
d'une colonne, et toutes sont composées à la même mesure. Une ligne qui \
tomberait sous le pied d'une colonne ouvre la suivante, et la lecture descend \
une colonne puis remonte en haut de la prochaine sans qu'aucune ligne soit \
perdue ni composée deux fois. C'est ainsi que se composent le \
texte d'un journal, les deux colonnes d'un rapport, ou la colonne étroite qui \
rend une page large lisible.",
};
/// 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); 2] =
[(Language::English, &ENGLISH), (Language::French, &FRENCH)];
/// The left edge of the first column.
const LEFT: f64 = 70.0;
/// The top edge every column shares.
const TOP: f64 = 720.0;
/// The width of one column.
const COLUMN: f64 = 145.0;
/// The gap between one column and the next.
const GAP: f64 = 25.0;
/// How far down from `TOP` a column reaches.
const HEIGHT: f64 = 130.0;
/// How many columns the passage is poured through.
const COLUMNS: usize = 3;
fn main() -> 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("columns")));
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 flow = TextFlow::new(&handle, 10.0)
.leading(14.0)
.align(Align::Justify);
let lines = flow.break_lines(words.passage, COLUMN);
let mut c = Content::new();
let mut from = 0;
let mut x = LEFT;
for _ in 0..COLUMNS {
let taken = flow.fit(&lines, HEIGHT, from);
c.begin_text();
flow.draw(&mut c, &lines[from..from + taken], x, TOP, COLUMN)?;
c.end_text();
c.save_state();
c.set_stroke_rgb(0.8, 0.8, 0.8)?
.set_line_width(0.5)?
.rect(x, TOP - HEIGHT, COLUMN, HEIGHT)?
.stroke();
c.restore_state();
from += taken;
x += COLUMN + GAP;
}
let mut page = Page::a4();
page.content = c.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 hqf_pdf::layout::Align;
use hqf_pdf::{Document, Font, TextFlow};
use super::{COLUMN, COLUMNS, HEIGHT, WORDS, default_font, language};
/// The lines two languages are allowed to write the same way. The page
/// holds one passage, and no two languages write it alike.
const SPARED: [&str; 0] = [];
/// How many of `passage`'s lines each of the columns takes, at the measure
/// and the height the page sets them to.
fn poured(passage: &str) -> (usize, Vec<usize>) {
let mut doc = Document::new();
let font = Font::parse(std::fs::read(default_font()).expect("the committed font is there"))
.expect("the committed font parses");
let handle = doc.add_font(font);
let flow = TextFlow::new(&handle, 10.0)
.leading(14.0)
.align(Align::Justify);
let lines = flow.break_lines(passage, COLUMN);
let mut from = 0;
let mut taken = Vec::with_capacity(COLUMNS);
for _ in 0..COLUMNS {
let fitted = flow.fit(&lines, HEIGHT, from);
taken.push(fitted);
from += fitted;
}
(lines.len(), taken)
}
#[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 passage that stops short of the last column shows nothing the column
/// before it did not, and one longer than the three loses its last lines at
/// the foot of the page with nothing on it to say so.
#[test]
fn every_language_pours_a_passage_that_fills_the_columns_and_no_more() {
for (named, words) in WORDS {
let (lines, taken) = poured(words.passage);
let code = named.code();
assert_eq!(
taken.iter().sum::<usize>(),
lines,
"the {code} passage leaves lines under the foot of the last column"
);
assert!(
taken[COLUMNS - 1] > 0,
"the {code} passage is set before the last column is opened"
);
}
}
}
|