-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathEngineTests.cpp
More file actions
300 lines (264 loc) · 10.7 KB
/
Copy pathEngineTests.cpp
File metadata and controls
300 lines (264 loc) · 10.7 KB
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
// Checks the measurements that were verified against the real device.
//
// The crux is font scaling: Qt converts point sizes against the device DPI. Scaling
// the coordinate system on top of that (600 dpi PDF) makes the text come out DPI/72
// too large while every other measurement looks right. So this does not merely do
// arithmetic — it rasterises the produced PDF and measures the ink.
#include "Engine.h"
#include "Status.h"
#include <QGuiApplication>
#include <QImage>
#include <QProcess>
#include <QTemporaryDir>
#include <cmath>
#include <cstdio>
using namespace ptouch;
namespace {
int failures = 0;
void check(bool condition, const QString &what)
{
std::printf("%s %s\n", condition ? "ok " : "FAIL", qPrintable(what));
if (!condition)
++failures;
}
void checkNear(double actual, double expected, double tolerance, const QString &what)
{
const bool ok = std::fabs(actual - expected) <= tolerance;
std::printf("%s %s (%.2f, expected %.2f ± %.2f)\n",
ok ? "ok " : "FAIL", qPrintable(what), actual, expected, tolerance);
if (!ok)
++failures;
}
// Rasterises the PDF at 10 pixels per millimetre and measures the actual ink.
struct Ink {
bool valid = false;
double pageWidthMm = 0, pageHeightMm = 0;
double acrossMm = 0, alongMm = 0;
double marginTopMm = 0, marginBottomMm = 0;
double marginLeadMm = 0, marginTrailMm = 0;
};
Ink measurePdf(const QString &pdfPath, const QString &workDir)
{
Ink ink;
const QString stem = workDir + QStringLiteral("/raster");
QProcess pdftoppm;
pdftoppm.start(QStringLiteral("pdftoppm"),
{QStringLiteral("-r"), QStringLiteral("254"), QStringLiteral("-gray"),
QStringLiteral("-png"), pdfPath, stem});
if (!pdftoppm.waitForFinished(30000) || pdftoppm.exitCode() != 0)
return ink; // without poppler-utils this part is skipped
QImage image(stem + QStringLiteral("-1.png"));
if (image.isNull())
image = QImage(stem + QStringLiteral("-01.png"));
if (image.isNull())
return ink;
image = image.convertToFormat(QImage::Format_Grayscale8);
const int w = image.width(), h = image.height();
int minX = w, maxX = -1, minY = h, maxY = -1;
for (int y = 0; y < h; ++y) {
const uchar *line = image.constScanLine(y);
for (int x = 0; x < w; ++x) {
if (line[x] < 128) {
minX = std::min(minX, x); maxX = std::max(maxX, x);
minY = std::min(minY, y); maxY = std::max(maxY, y);
}
}
}
if (maxX < 0)
return ink;
ink.valid = true;
ink.pageWidthMm = w / 10.0;
ink.pageHeightMm = h / 10.0;
ink.acrossMm = (maxX - minX + 1) / 10.0;
ink.alongMm = (maxY - minY + 1) / 10.0;
ink.marginTopMm = minX / 10.0;
ink.marginBottomMm = (w - 1 - maxX) / 10.0;
ink.marginLeadMm = minY / 10.0;
ink.marginTrailMm = (h - 1 - maxY) / 10.0;
return ink;
}
void testTapeTable()
{
check(tapes().size() == 8, "eight tape widths known");
check(tapeFor(12.0) != nullptr, "12 mm is a known tape width");
check(tapeFor(13.0) == nullptr, "13 mm is not a known tape width");
check(tapeIndex(24.0) == 6, "24 mm sits between 21 and 36 mm");
Spec s;
s.tapeMm = 12.0;
checkNear(s.tapePt(), 34.0, 0.01, "12 mm tape is 34 pt wide");
checkNear(s.printablePt() / PtPerMm, 10.73, 0.05, "12 mm tape: printable height");
s.tapeMm = 24.0;
checkNear(s.printablePt() / PtPerMm, 18.06, 0.05, "24 mm tape: printable height");
// The head is the limit whenever the tape could take more: 36 mm tape would
// allow 192 dots, but a 128 dot head reaches only 18.06 mm of it.
s.tapeMm = 36.0;
s.model = QStringLiteral("PT-P710BT");
checkNear(s.printablePt() / PtPerMm, 18.06, 0.05, "128 dot head limits wide tape");
s.model = QStringLiteral("PT-P900Wc");
checkNear(s.printablePt() / PtPerMm, 32.0, 0.6, "360 dpi head prints wider");
s.tapeMm = 12.0;
s.model = QStringLiteral("PT-2300"); // 112 dot head
checkNear(s.printablePt() / PtPerMm, 10.73, 0.05, "narrow head, narrow tape unaffected");
}
void testAutoSizeFillsTape()
{
// Automatic sizing has to use the printable height rather than the font
// metrics, otherwise the text stays noticeably smaller than it could be.
for (const double width : {12.0, 24.0}) {
Spec s;
s.tapeMm = width;
s.text = QStringLiteral("Workshop");
const Layout l = computeLayout(s);
checkNear(l.inkHeight, s.printablePt(), 0.6,
QStringLiteral("%1 mm: text fills the printable height").arg(width));
check(l.warnings.isEmpty(), QStringLiteral("%1 mm: no warning").arg(width));
}
}
void testMultiLineShrinks()
{
Spec one, two;
one.text = QStringLiteral("Fuse box");
two.text = QStringLiteral("Fuse box\nF3 16A");
const Layout l1 = computeLayout(one);
const Layout l2 = computeLayout(two);
check(l2.sizePt < l1.sizePt, "two lines yield a smaller font");
checkNear(l2.inkHeight, two.printablePt(), 0.6, "two lines fill the tape height");
}
void testFixedLength()
{
Spec s;
s.text = QStringLiteral("Left");
s.autoLength = false;
s.lengthMm = 60;
const Layout l = computeLayout(s);
checkNear(l.lengthPt / PtPerMm, 60.0, 0.01, "fixed length is honoured");
check(!l.overflow, "short text fits the fixed length");
}
void testOverflowDetected()
{
Spec s;
s.text = QStringLiteral("A very long text that will not fit");
s.autoLength = false;
s.autoSize = false;
s.sizePt = 30;
s.lengthMm = 20;
const Layout l = computeLayout(s);
check(l.overflow, "overlong text is detected");
check(!l.warnings.isEmpty(), "overlong text produces a warning");
}
void testPdfGeometry(const QString &workDir)
{
Spec s;
s.tapeMm = 12.0;
s.text = QStringLiteral("Workshop");
const Layout l = computeLayout(s);
const QString pdf = workDir + QStringLiteral("/label.pdf");
const PageSize page = writePdf(pdf, s, l);
check(page.widthPt == 34, "PDF page is 34 pt wide");
checkNear(page.heightPt / PtPerMm, l.lengthPt / PtPerMm, 0.5,
"PDF page length matches the layout");
const Ink ink = measurePdf(pdf, workDir);
if (!ink.valid) {
std::printf("skipped: pdftoppm not available\n");
return;
}
checkNear(ink.pageWidthMm, 12.0, 0.15, "rasterised page is 12 mm wide");
checkNear(ink.acrossMm, 10.6, 0.8, "ink uses the printable tape height");
checkNear(ink.marginTopMm, ink.marginBottomMm, 0.5, "text is centred on the tape");
// The measured margin is the configured one plus the glyph's side bearing,
// which depends on the first and last letter — hence the generous tolerance.
checkNear(ink.marginLeadMm, s.marginMm, 1.0, "leading side margin as configured");
checkNear(ink.marginTrailMm, s.marginMm, 1.0, "trailing side margin as configured");
}
void testGlyphDetection()
{
// Plain text and outline symbols draw as vectors; anything only the colour
// bitmap font provides has to take the image path or the PDF loses it.
Spec plain;
plain.text = QStringLiteral("Workshop 12");
check(!needsRasterGlyphs(plain), "plain text draws as outlines");
Spec star;
star.text = QStringLiteral("★ Shelf");
check(!needsRasterGlyphs(star), "outline symbols draw as outlines");
Spec emoji;
emoji.text = QStringLiteral("🔧 Tool");
check(needsRasterGlyphs(emoji), "pictographs need the image path");
// The image has to carry actual ink, otherwise the emoji silently vanishes.
const Layout l = computeLayout(emoji);
const QImage image = renderToImage(emoji, l, 300);
int dark = 0;
for (int y = 0; y < image.height(); ++y)
for (int x = 0; x < image.width(); ++x)
if (qGray(image.pixel(x, y)) < 200)
++dark;
check(dark > 500, "rendered image contains the pictograph");
check(image.format() == QImage::Format_Grayscale8, "image is greyscale for the tape");
}
void testArtwork()
{
// A barcode has to be sized by module width, not by available space: below
// roughly a quarter millimetre per bar no scanner reads it.
Spec bc;
bc.tapeMm = 12.0;
bc.text = QStringLiteral("Stock");
bc.codeType = CodeType::Code128;
bc.codeData = QStringLiteral("ABC-12345");
const Layout lbc = computeLayout(bc);
check(!lbc.artwork.isNull(), "barcode is rendered");
const double moduleMm = lbc.artworkWidth / std::max(1, lbc.artwork.width()) / PtPerMm;
checkNear(moduleMm, 0.28, 0.05, "barcode modules are wide enough to scan");
check(lbc.lengthPt > bc.marginMm * 2 * PtPerMm + lbc.artworkWidth,
"label grew to hold barcode and text");
// Code 128 set B is ASCII only; anything else has to be refused rather than
// silently mangled.
QString error;
check(renderCode(CodeType::Code128, QStringLiteral("Grüße"), &error).isNull()
&& !error.isEmpty(), "non-ASCII is rejected for Code 128");
if (qrAvailable()) {
Spec qr;
qr.tapeMm = 24.0;
qr.codeType = CodeType::Qr;
qr.codeData = QStringLiteral("https://example.org");
const Layout lqr = computeLayout(qr);
check(!lqr.artwork.isNull(), "QR code is rendered");
check(lqr.artwork.width() == lqr.artwork.height(), "QR code is square");
checkNear(lqr.artworkWidth, qr.printablePt(), 1.0, "QR code fills the tape height");
} else {
std::printf("skipped: built without libqrencode\n");
}
}
void testStatusDecoding()
{
QByteArray frame(StatusLength, '\0');
frame[10] = 12; // tape width
frame[11] = 0x01; // laminated
Status s = decodeStatus(frame);
check(s.ok, "clean status counts as ready");
check(s.tapeMm == 12, "tape width is read from byte 10");
check(s.mediaType == QStringLiteral("laminated"), "media type is recognised");
frame[8] = 0x01; // no tape
s = decodeStatus(frame);
check(!s.ok, "an error bit invalidates the status");
check(s.errors.contains(QStringLiteral("no tape inserted")), "error text recognised");
s = decodeStatus(QByteArray(8, '\0'));
check(!s.error.isEmpty(), "a short reply is rejected");
}
} // namespace
int main(int argc, char *argv[])
{
qputenv("QT_QPA_PLATFORM", "offscreen");
QGuiApplication app(argc, argv);
QTemporaryDir dir;
testTapeTable();
testAutoSizeFillsTape();
testMultiLineShrinks();
testFixedLength();
testOverflowDetected();
testPdfGeometry(dir.path());
testGlyphDetection();
testArtwork();
testStatusDecoding();
std::printf("\n%s\n", failures == 0 ? "all checks passed"
: qPrintable(QStringLiteral("%1 check(s) failed").arg(failures)));
return failures == 0 ? 0 : 1;
}