1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2018 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the tools applications of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include "mainwindow.h" |
30 | #include "ui_mainwindow.h" |
31 | #include "distancefieldmodel.h" |
32 | |
33 | #include <QtCore/qdir.h> |
34 | #include <QtCore/qdatastream.h> |
35 | #include <QtCore/qmath.h> |
36 | #include <QtCore/qendian.h> |
37 | #include <QtCore/qbuffer.h> |
38 | #include <QtGui/qdesktopservices.h> |
39 | #include <QtGui/qrawfont.h> |
40 | #include <QtWidgets/qmessagebox.h> |
41 | #include <QtWidgets/qlabel.h> |
42 | #include <QtWidgets/qprogressbar.h> |
43 | #include <QtWidgets/qfiledialog.h> |
44 | #include <QtWidgets/qinputdialog.h> |
45 | |
46 | #include <QtCore/private/qunicodetables_p.h> |
47 | #include <QtGui/private/qdistancefield_p.h> |
48 | #include <QtQuick/private/qsgareaallocator_p.h> |
49 | #include <QtQuick/private/qsgadaptationlayer_p.h> |
50 | |
51 | QT_BEGIN_NAMESPACE |
52 | |
53 | static void openHelp() |
54 | { |
55 | QDesktopServices::openUrl(url: QUrl(QLatin1String("http://doc.qt.io/qt-5/qtdistancefieldgenerator-index.html" ))); |
56 | } |
57 | |
58 | MainWindow::MainWindow(QWidget *parent) |
59 | : QMainWindow(parent) |
60 | , ui(new Ui::MainWindow) |
61 | , m_settings(qApp->organizationName(), qApp->applicationName()) |
62 | , m_model(new DistanceFieldModel(this)) |
63 | , m_statusBarLabel(nullptr) |
64 | , m_statusBarProgressBar(nullptr) |
65 | { |
66 | ui->setupUi(this); |
67 | ui->lvGlyphs->setModel(m_model); |
68 | |
69 | ui->actionHelp->setShortcut(QKeySequence::HelpContents); |
70 | |
71 | m_statusBarLabel = new QLabel(this); |
72 | m_statusBarLabel->setText(tr(s: "Ready" )); |
73 | ui->statusbar->addPermanentWidget(widget: m_statusBarLabel); |
74 | |
75 | m_statusBarProgressBar = new QProgressBar(this); |
76 | ui->statusbar->addPermanentWidget(widget: m_statusBarProgressBar); |
77 | m_statusBarProgressBar->setVisible(false); |
78 | |
79 | if (m_settings.contains(QStringLiteral("fontDirectory" ))) |
80 | m_fontDir = m_settings.value(QStringLiteral("fontDirectory" )).toString(); |
81 | else |
82 | m_fontDir = QDir::currentPath(); |
83 | |
84 | qRegisterMetaType<glyph_t>(typeName: "glyph_t" ); |
85 | qRegisterMetaType<QPainterPath>(typeName: "QPainterPath" ); |
86 | |
87 | restoreGeometry(geometry: m_settings.value(QStringLiteral("geometry" )).toByteArray()); |
88 | |
89 | setupConnections(); |
90 | } |
91 | |
92 | MainWindow::~MainWindow() |
93 | { |
94 | delete ui; |
95 | } |
96 | |
97 | void MainWindow::open(const QString &path) |
98 | { |
99 | m_fileName.clear(); |
100 | m_fontFile = path; |
101 | m_fontDir = QFileInfo(path).absolutePath(); |
102 | m_settings.setValue(QStringLiteral("fontDirectory" ), value: m_fontDir); |
103 | |
104 | ui->lwUnicodeRanges->clear(); |
105 | ui->lwUnicodeRanges->setDisabled(true); |
106 | ui->action_Save->setDisabled(true); |
107 | ui->action_Save_as->setDisabled(true); |
108 | ui->tbSave->setDisabled(true); |
109 | ui->action_Open->setDisabled(true); |
110 | m_model->setFont(path); |
111 | } |
112 | |
113 | void MainWindow::closeEvent(QCloseEvent * /*event*/) |
114 | { |
115 | m_settings.setValue(QStringLiteral("geometry" ), value: saveGeometry()); |
116 | } |
117 | |
118 | void MainWindow::setupConnections() |
119 | { |
120 | connect(sender: ui->action_Open, signal: &QAction::triggered, receiver: this, slot: &MainWindow::openFont); |
121 | connect(sender: ui->actionE_xit, signal: &QAction::triggered, qApp, slot: &QApplication::quit); |
122 | connect(sender: ui->action_Save, signal: &QAction::triggered, receiver: this, slot: &MainWindow::save); |
123 | connect(sender: ui->action_Save_as, signal: &QAction::triggered, receiver: this, slot: &MainWindow::saveAs); |
124 | connect(sender: ui->tbSave, signal: &QToolButton::clicked, receiver: this, slot: &MainWindow::save); |
125 | connect(sender: ui->tbSelectAll, signal: &QToolButton::clicked, receiver: this, slot: &MainWindow::selectAll); |
126 | connect(sender: ui->actionSelect_all, signal: &QAction::triggered, receiver: this, slot: &MainWindow::selectAll); |
127 | connect(sender: ui->actionSelect_string, signal: &QAction::triggered, receiver: this, slot: &MainWindow::selectString); |
128 | connect(sender: ui->actionHelp, signal: &QAction::triggered, context: this, slot: openHelp); |
129 | connect(sender: ui->actionAbout_App, signal: &QAction::triggered, receiver: this, slot: &MainWindow::about); |
130 | connect(sender: ui->actionAbout_Qt, signal: &QAction::triggered, context: this, slot: [this]() { |
131 | QMessageBox::aboutQt(parent: this); |
132 | }); |
133 | connect(sender: ui->lwUnicodeRanges, signal: &QListWidget::itemSelectionChanged, receiver: this, slot: &MainWindow::updateUnicodeRanges); |
134 | |
135 | connect(sender: ui->lvGlyphs->selectionModel(), |
136 | signal: &QItemSelectionModel::selectionChanged, |
137 | receiver: this, |
138 | slot: &MainWindow::updateSelection); |
139 | connect(sender: m_model, signal: &DistanceFieldModel::startGeneration, receiver: this, slot: &MainWindow::startProgressBar); |
140 | connect(sender: m_model, signal: &DistanceFieldModel::stopGeneration, receiver: this, slot: &MainWindow::stopProgressBar); |
141 | connect(sender: m_model, signal: &DistanceFieldModel::distanceFieldGenerated, receiver: this, slot: &MainWindow::updateProgressBar); |
142 | connect(sender: m_model, signal: &DistanceFieldModel::stopGeneration, receiver: this, slot: &MainWindow::populateUnicodeRanges); |
143 | connect(sender: m_model, signal: &DistanceFieldModel::error, receiver: this, slot: &MainWindow::displayError); |
144 | } |
145 | |
146 | void MainWindow::saveAs() |
147 | { |
148 | QString fileName = QFileDialog::getSaveFileName(parent: this, |
149 | caption: tr(s: "Save distance field-enriched file" ), |
150 | dir: m_fontDir, |
151 | filter: tr(s: "Font files (*.ttf *.otf);;All files (*)" )); |
152 | if (!fileName.isEmpty()) { |
153 | m_fileName = fileName; |
154 | m_fontDir = QFileInfo(m_fileName).absolutePath(); |
155 | m_settings.setValue(QStringLiteral("fontDirectory" ), value: m_fontDir); |
156 | save(); |
157 | } |
158 | } |
159 | |
160 | |
161 | # pragma pack(1) |
162 | struct |
163 | { |
164 | quint32 ; |
165 | quint16 ; |
166 | quint16 ; |
167 | quint16 ; |
168 | quint16 ; |
169 | }; |
170 | |
171 | struct TableRecord |
172 | { |
173 | quint32 tag; |
174 | quint32 checkSum; |
175 | quint32 offset; |
176 | quint32 length; |
177 | }; |
178 | |
179 | struct |
180 | { |
181 | quint8 ; |
182 | quint8 ; |
183 | quint16 ; |
184 | quint32 ; |
185 | quint8 ; |
186 | quint8 ; |
187 | quint32 ; |
188 | }; |
189 | |
190 | struct QtdfGlyphRecord |
191 | { |
192 | quint32 glyphIndex; |
193 | quint32 textureOffsetX; |
194 | quint32 textureOffsetY; |
195 | quint32 textureWidth; |
196 | quint32 textureHeight; |
197 | quint32 xMargin; |
198 | quint32 yMargin; |
199 | qint32 boundingRectX; |
200 | qint32 boundingRectY; |
201 | quint32 boundingRectWidth; |
202 | quint32 boundingRectHeight; |
203 | quint16 textureIndex; |
204 | }; |
205 | |
206 | struct QtdfTextureRecord |
207 | { |
208 | quint32 allocatedX; |
209 | quint32 allocatedY; |
210 | quint32 allocatedWidth; |
211 | quint32 allocatedHeight; |
212 | quint8 padding; |
213 | }; |
214 | |
215 | struct Head |
216 | { |
217 | quint16 majorVersion; |
218 | quint16 minorVersion; |
219 | quint32 fontRevision; |
220 | quint32 checkSumAdjustment; |
221 | }; |
222 | # pragma pack() |
223 | |
224 | #define PAD_BUFFER(buffer, size) \ |
225 | { \ |
226 | int paddingNeed = size % 4; \ |
227 | if (paddingNeed > 0) { \ |
228 | const char padding[3] = { 0, 0, 0 }; \ |
229 | buffer.write(padding, 4 - paddingNeed); \ |
230 | } \ |
231 | } |
232 | |
233 | #define ALIGN_OFFSET(offset) \ |
234 | { \ |
235 | int paddingNeed = offset % 4; \ |
236 | if (paddingNeed > 0) \ |
237 | offset += 4 - paddingNeed; \ |
238 | } |
239 | |
240 | #define TO_FIXED_POINT(value) \ |
241 | ((int)(value*qreal(65536))) |
242 | |
243 | void MainWindow::save() |
244 | { |
245 | QModelIndexList list = ui->lvGlyphs->selectionModel()->selectedIndexes(); |
246 | if (list.isEmpty()) { |
247 | QMessageBox::warning(parent: this, |
248 | title: tr(s: "Nothing to save" ), |
249 | text: tr(s: "No glyphs selected for saving." ), |
250 | buttons: QMessageBox::Ok); |
251 | return; |
252 | } |
253 | |
254 | if (m_fileName.isEmpty()) { |
255 | saveAs(); |
256 | return; |
257 | } |
258 | |
259 | QFile inFile(m_fontFile); |
260 | if (!inFile.open(flags: QIODevice::ReadOnly)) { |
261 | QMessageBox::warning(parent: this, |
262 | title: tr(s: "Can't read original font" ), |
263 | text: tr(s: "Cannot open '%s' for reading. The original font file must remain in place until the new file has been saved." ).arg(a: m_fontFile), |
264 | buttons: QMessageBox::Ok); |
265 | return; |
266 | } |
267 | |
268 | QByteArray output; |
269 | quint32 headOffset = 0; |
270 | |
271 | { |
272 | QBuffer outBuffer(&output); |
273 | outBuffer.open(openMode: QIODevice::WriteOnly); |
274 | |
275 | uchar *inData = inFile.map(offset: 0, size: inFile.size()); |
276 | if (inData == nullptr) { |
277 | QMessageBox::warning(parent: this, |
278 | title: tr(s: "Can't map input file" ), |
279 | text: tr(s: "Unable to memory map input file '%s'." ).arg(a: m_fontFile)); |
280 | return; |
281 | } |
282 | |
283 | uchar *end = inData + inFile.size(); |
284 | if (inData + sizeof(FontDirectoryHeader) > end) { |
285 | QMessageBox::warning(parent: this, |
286 | title: tr(s: "Can't read font directory" ), |
287 | text: tr(s: "Input file seems to be invalid or corrupt." ), |
288 | buttons: QMessageBox::Ok); |
289 | return; |
290 | } |
291 | |
292 | FontDirectoryHeader ; |
293 | memcpy(dest: &fontDirectoryHeader, src: inData, n: sizeof(FontDirectoryHeader)); |
294 | quint16 numTables = qFromBigEndian(source: fontDirectoryHeader.numTables) + 1; |
295 | fontDirectoryHeader.numTables = qToBigEndian(source: numTables); |
296 | { |
297 | quint16 searchRange = qFromBigEndian(source: fontDirectoryHeader.searchRange); |
298 | if (searchRange / 16 < numTables) { |
299 | quint16 pot = (searchRange / 16) * 2; |
300 | searchRange = pot * 16; |
301 | fontDirectoryHeader.searchRange = qToBigEndian(source: searchRange); |
302 | fontDirectoryHeader.rangeShift = qToBigEndian(source: numTables * 16 - searchRange); |
303 | |
304 | quint16 entrySelector = 0; |
305 | while (pot > 1) { |
306 | pot >>= 1; |
307 | entrySelector++; |
308 | } |
309 | fontDirectoryHeader.entrySelector = qToBigEndian(source: entrySelector); |
310 | } |
311 | } |
312 | |
313 | outBuffer.write(data: reinterpret_cast<char *>(&fontDirectoryHeader), |
314 | len: sizeof(FontDirectoryHeader)); |
315 | |
316 | QVarLengthArray<QPair<quint32, quint32>> offsetLengthPairs; |
317 | offsetLengthPairs.reserve(asize: numTables - 1); |
318 | |
319 | // Copy the offset table, updating offsets |
320 | TableRecord *offsetTable = reinterpret_cast<TableRecord *>(inData + sizeof(FontDirectoryHeader)); |
321 | quint32 currentOffset = sizeof(FontDirectoryHeader) + sizeof(TableRecord) * numTables; |
322 | for (int i = 0; i < numTables - 1; ++i) { |
323 | ALIGN_OFFSET(currentOffset) |
324 | |
325 | quint32 originalOffset = qFromBigEndian(source: offsetTable->offset); |
326 | quint32 length = qFromBigEndian(source: offsetTable->length); |
327 | offsetLengthPairs.append(t: qMakePair(x: originalOffset, y: length)); |
328 | if (offsetTable->tag == qToBigEndian(MAKE_TAG('h', 'e', 'a', 'd'))) |
329 | headOffset = currentOffset; |
330 | |
331 | TableRecord newTableRecord; |
332 | memcpy(dest: &newTableRecord, src: offsetTable, n: sizeof(TableRecord)); |
333 | newTableRecord.offset = qToBigEndian(source: currentOffset); |
334 | outBuffer.write(data: reinterpret_cast<char *>(&newTableRecord), len: sizeof(TableRecord)); |
335 | |
336 | offsetTable++; |
337 | currentOffset += length; |
338 | } |
339 | |
340 | if (headOffset == 0) { |
341 | QMessageBox::warning(parent: this, |
342 | title: tr(s: "Invalid font file" ), |
343 | text: tr(s: "Font file does not have 'head' table." ), |
344 | buttons: QMessageBox::Ok); |
345 | return; |
346 | } |
347 | |
348 | QByteArray qtdf = createSfntTable(); |
349 | if (qtdf.isEmpty()) |
350 | return; |
351 | |
352 | { |
353 | ALIGN_OFFSET(currentOffset) |
354 | |
355 | TableRecord qtdfRecord; |
356 | qtdfRecord.offset = qToBigEndian(source: currentOffset); |
357 | qtdfRecord.length = qToBigEndian(source: qtdf.length()); |
358 | qtdfRecord.tag = qToBigEndian(MAKE_TAG('q', 't', 'd', 'f')); |
359 | quint32 checkSum = 0; |
360 | const quint32 *start = reinterpret_cast<const quint32 *>(qtdf.constData()); |
361 | const quint32 *end = reinterpret_cast<const quint32 *>(qtdf.constData() + qtdf.length()); |
362 | while (start < end) |
363 | checkSum += *(start++); |
364 | qtdfRecord.checkSum = qToBigEndian(source: checkSum); |
365 | |
366 | outBuffer.write(data: reinterpret_cast<char *>(&qtdfRecord), |
367 | len: sizeof(TableRecord)); |
368 | } |
369 | |
370 | // Copy all font tables |
371 | for (const QPair<quint32, quint32> &offsetLengthPair : offsetLengthPairs) { |
372 | PAD_BUFFER(outBuffer, output.size()) |
373 | outBuffer.write(data: reinterpret_cast<char *>(inData + offsetLengthPair.first), |
374 | len: offsetLengthPair.second); |
375 | } |
376 | |
377 | PAD_BUFFER(outBuffer, output.size()) |
378 | outBuffer.write(data: qtdf); |
379 | } |
380 | |
381 | // Clear 'head' checksum and calculate new check sum adjustment |
382 | Head *head = reinterpret_cast<Head *>(output.data() + headOffset); |
383 | head->checkSumAdjustment = 0; |
384 | |
385 | quint32 checkSum = 0; |
386 | const quint32 *start = reinterpret_cast<const quint32 *>(output.constData()); |
387 | const quint32 *end = reinterpret_cast<const quint32 *>(output.constData() + output.length()); |
388 | while (start < end) |
389 | checkSum += *(start++); |
390 | |
391 | head->checkSumAdjustment = qToBigEndian(source: 0xB1B0AFBA - checkSum); |
392 | |
393 | QFile outFile(m_fileName); |
394 | if (!outFile.open(flags: QIODevice::WriteOnly)) { |
395 | QMessageBox::warning(parent: this, |
396 | title: tr(s: "Can't write to file" ), |
397 | text: tr(s: "Cannot open the file '%s' for writing" ).arg(a: m_fileName), |
398 | buttons: QMessageBox::Ok); |
399 | return; |
400 | } |
401 | |
402 | outFile.write(data: output); |
403 | } |
404 | |
405 | QByteArray MainWindow::createSfntTable() |
406 | { |
407 | QModelIndexList list = ui->lvGlyphs->selectionModel()->selectedIndexes(); |
408 | Q_ASSERT(!list.isEmpty()); |
409 | |
410 | QByteArray ret; |
411 | { |
412 | QBuffer buffer(&ret); |
413 | buffer.open(openMode: QIODevice::WriteOnly); |
414 | |
415 | QtdfHeader ; |
416 | header.majorVersion = 5; |
417 | header.minorVersion = 12; |
418 | header.pixelSize = qToBigEndian(source: quint16(qRound(d: m_model->pixelSize()))); |
419 | |
420 | const quint8 padding = 2; |
421 | qreal scaleFactor = qreal(1) / QT_DISTANCEFIELD_SCALE(narrowOutlineFont: m_model->doubleGlyphResolution()); |
422 | const int radius = QT_DISTANCEFIELD_RADIUS(narrowOutlineFont: m_model->doubleGlyphResolution()) |
423 | / QT_DISTANCEFIELD_SCALE(narrowOutlineFont: m_model->doubleGlyphResolution()); |
424 | |
425 | quint32 textureSize = ui->sbMaximumTextureSize->value(); |
426 | |
427 | // Since we are using a single area allocator that spans all textures, we need |
428 | // to split the textures one row before the actual maximum size, otherwise |
429 | // glyphs that fall on the edge between two textures will expand the texture |
430 | // they are assigned to, and this will end up being larger than the max. |
431 | textureSize -= quint32(qCeil(v: m_model->pixelSize() * scaleFactor) + radius * 2 + padding * 2); |
432 | header.textureSize = qToBigEndian(source: textureSize); |
433 | |
434 | header.padding = padding; |
435 | header.flags = m_model->doubleGlyphResolution() ? 1 : 0; |
436 | header.numGlyphs = qToBigEndian(source: quint32(list.size())); |
437 | buffer.write(data: reinterpret_cast<char *>(&header), |
438 | len: sizeof(QtdfHeader)); |
439 | |
440 | // Maximum height allocator to find optimal number of textures |
441 | QVector<QRect> allocatedAreaPerTexture; |
442 | |
443 | struct GlyphData { |
444 | QSGDistanceFieldGlyphCache::TexCoord texCoord; |
445 | QRectF boundingRect; |
446 | QSize glyphSize; |
447 | int textureIndex; |
448 | }; |
449 | QVector<GlyphData> glyphDatas; |
450 | glyphDatas.resize(asize: m_model->rowCount()); |
451 | |
452 | int textureCount = 0; |
453 | |
454 | { |
455 | QTransform scaleDown; |
456 | scaleDown.scale(sx: scaleFactor, sy: scaleFactor); |
457 | |
458 | { |
459 | bool foundOptimalSize = false; |
460 | while (!foundOptimalSize) { |
461 | allocatedAreaPerTexture.clear(); |
462 | |
463 | QSGAreaAllocator allocator(QSize(textureSize, textureSize * (++textureCount))); |
464 | |
465 | int i; |
466 | for (i = 0; i < list.size(); ++i) { |
467 | int glyphIndex = list.at(i).row(); |
468 | GlyphData &glyphData = glyphDatas[glyphIndex]; |
469 | |
470 | QPainterPath path = m_model->path(row: glyphIndex); |
471 | glyphData.boundingRect = scaleDown.mapRect(path.boundingRect()); |
472 | int glyphWidth = qCeil(v: glyphData.boundingRect.width()) + radius * 2; |
473 | int glyphHeight = qCeil(v: glyphData.boundingRect.height()) + radius * 2; |
474 | |
475 | glyphData.glyphSize = QSize(glyphWidth + padding * 2, glyphHeight + padding * 2); |
476 | |
477 | if (glyphData.glyphSize.width() > qint32(textureSize) |
478 | || glyphData.glyphSize.height() > qint32(textureSize)) { |
479 | QMessageBox::warning(parent: this, |
480 | title: tr(s: "Glyph too large for texture" ), |
481 | text: tr(s: "Glyph %1 is too large to fit in texture of size %2." ) |
482 | .arg(a: glyphIndex).arg(a: textureSize)); |
483 | return QByteArray(); |
484 | } |
485 | |
486 | QRect rect = allocator.allocate(size: glyphData.glyphSize); |
487 | if (rect.isNull()) |
488 | break; |
489 | |
490 | glyphData.textureIndex = rect.y() / textureSize; |
491 | while (glyphData.textureIndex >= allocatedAreaPerTexture.size()) |
492 | allocatedAreaPerTexture.append(t: QRect(0, 0, 1, 1)); |
493 | |
494 | allocatedAreaPerTexture[glyphData.textureIndex] |= QRect(rect.x(), |
495 | rect.y() % textureSize, |
496 | rect.width(), |
497 | rect.height()); |
498 | |
499 | glyphData.texCoord.xMargin = QT_DISTANCEFIELD_RADIUS(narrowOutlineFont: m_model->doubleGlyphResolution()) / qreal(QT_DISTANCEFIELD_SCALE(narrowOutlineFont: m_model->doubleGlyphResolution())); |
500 | glyphData.texCoord.yMargin = QT_DISTANCEFIELD_RADIUS(narrowOutlineFont: m_model->doubleGlyphResolution()) / qreal(QT_DISTANCEFIELD_SCALE(narrowOutlineFont: m_model->doubleGlyphResolution())); |
501 | glyphData.texCoord.x = rect.x() + padding; |
502 | glyphData.texCoord.y = rect.y() % textureSize + padding; |
503 | glyphData.texCoord.width = glyphData.boundingRect.width(); |
504 | glyphData.texCoord.height = glyphData.boundingRect.height(); |
505 | |
506 | glyphDatas.append(t: glyphData); |
507 | } |
508 | |
509 | foundOptimalSize = i == list.size(); |
510 | if (foundOptimalSize) |
511 | buffer.write(data: allocator.serialize()); |
512 | } |
513 | } |
514 | } |
515 | |
516 | QVector<QDistanceField> textures; |
517 | textures.resize(asize: textureCount); |
518 | |
519 | for (int textureIndex = 0; textureIndex < textureCount; ++textureIndex) { |
520 | textures[textureIndex] = QDistanceField(allocatedAreaPerTexture.at(i: textureIndex).width(), |
521 | allocatedAreaPerTexture.at(i: textureIndex).height()); |
522 | |
523 | QRect rect = allocatedAreaPerTexture.at(i: textureIndex); |
524 | |
525 | QtdfTextureRecord record; |
526 | record.allocatedX = qToBigEndian(source: rect.x()); |
527 | record.allocatedY = qToBigEndian(source: rect.y()); |
528 | record.allocatedWidth = qToBigEndian(source: rect.width()); |
529 | record.allocatedHeight = qToBigEndian(source: rect.height()); |
530 | record.padding = padding; |
531 | buffer.write(data: reinterpret_cast<char *>(&record), |
532 | len: sizeof(QtdfTextureRecord)); |
533 | } |
534 | |
535 | { |
536 | for (int i = 0; i < list.size(); ++i) { |
537 | int glyphIndex = list.at(i).row(); |
538 | QImage image = m_model->distanceField(row: glyphIndex); |
539 | |
540 | const GlyphData &glyphData = glyphDatas.at(i: glyphIndex); |
541 | |
542 | QtdfGlyphRecord glyphRecord; |
543 | glyphRecord.glyphIndex = qToBigEndian(source: glyphIndex); |
544 | glyphRecord.textureOffsetX = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.x)); |
545 | glyphRecord.textureOffsetY = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.y)); |
546 | glyphRecord.textureWidth = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.width)); |
547 | glyphRecord.textureHeight = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.height)); |
548 | glyphRecord.xMargin = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.xMargin)); |
549 | glyphRecord.yMargin = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.yMargin)); |
550 | glyphRecord.boundingRectX = qToBigEndian(TO_FIXED_POINT(glyphData.boundingRect.x())); |
551 | glyphRecord.boundingRectY = qToBigEndian(TO_FIXED_POINT(glyphData.boundingRect.y())); |
552 | glyphRecord.boundingRectWidth = qToBigEndian(TO_FIXED_POINT(glyphData.boundingRect.width())); |
553 | glyphRecord.boundingRectHeight = qToBigEndian(TO_FIXED_POINT(glyphData.boundingRect.height())); |
554 | glyphRecord.textureIndex = qToBigEndian(source: quint16(glyphData.textureIndex)); |
555 | buffer.write(data: reinterpret_cast<char *>(&glyphRecord), len: sizeof(QtdfGlyphRecord)); |
556 | |
557 | int expectedWidth = qCeil(v: glyphData.texCoord.width + glyphData.texCoord.xMargin * 2); |
558 | image = image.copy(x: -padding, y: -padding, |
559 | w: expectedWidth + padding * 2, |
560 | h: image.height() + padding * 2); |
561 | |
562 | uchar *inBits = image.scanLine(0); |
563 | uchar *outBits = textures[glyphData.textureIndex].scanLine(int(glyphData.texCoord.y) - padding) |
564 | + int(glyphData.texCoord.x) - padding; |
565 | for (int y = 0; y < image.height(); ++y) { |
566 | memcpy(dest: outBits, src: inBits, n: image.width()); |
567 | inBits += image.bytesPerLine(); |
568 | outBits += textures[glyphData.textureIndex].width(); |
569 | } |
570 | } |
571 | } |
572 | |
573 | for (int i = 0; i < textures.size(); ++i) { |
574 | const QDistanceField &texture = textures.at(i); |
575 | const QRect &allocatedArea = allocatedAreaPerTexture.at(i); |
576 | buffer.write(data: reinterpret_cast<const char *>(texture.constBits()), |
577 | len: allocatedArea.width() * allocatedArea.height()); |
578 | } |
579 | |
580 | PAD_BUFFER(buffer, ret.size()) |
581 | } |
582 | |
583 | return ret; |
584 | } |
585 | |
586 | void MainWindow::writeFile() |
587 | { |
588 | Q_ASSERT(!m_fileName.isEmpty()); |
589 | |
590 | QFile file(m_fileName); |
591 | if (file.open(flags: QIODevice::WriteOnly)) { |
592 | |
593 | } else { |
594 | QMessageBox::warning(parent: this, |
595 | title: tr(s: "Can't open file for writing" ), |
596 | text: tr(s: "Unable to open file '%1' for writing" ).arg(a: m_fileName), |
597 | buttons: QMessageBox::Ok); |
598 | } |
599 | } |
600 | |
601 | void MainWindow::openFont() |
602 | { |
603 | QString fileName = QFileDialog::getOpenFileName(parent: this, |
604 | caption: tr(s: "Open font file" ), |
605 | dir: m_fontDir, |
606 | filter: tr(s: "Fonts (*.ttf *.otf);;All files (*)" )); |
607 | if (!fileName.isEmpty()) |
608 | open(path: fileName); |
609 | } |
610 | |
611 | void MainWindow::updateProgressBar() |
612 | { |
613 | m_statusBarProgressBar->setValue(m_statusBarProgressBar->value() + 1); |
614 | updateSelection(); |
615 | } |
616 | |
617 | void MainWindow::startProgressBar(quint16 glyphCount) |
618 | { |
619 | ui->action_Open->setDisabled(false); |
620 | m_statusBarLabel->setText(tr(s: "Generating" )); |
621 | m_statusBarProgressBar->setMaximum(glyphCount); |
622 | m_statusBarProgressBar->setMinimum(0); |
623 | m_statusBarProgressBar->setValue(0); |
624 | m_statusBarProgressBar->setVisible(true); |
625 | } |
626 | |
627 | void MainWindow::stopProgressBar() |
628 | { |
629 | m_statusBarLabel->setText(tr(s: "Ready" )); |
630 | m_statusBarProgressBar->setVisible(false); |
631 | } |
632 | |
633 | void MainWindow::selectAll() |
634 | { |
635 | QModelIndexList list = ui->lvGlyphs->selectionModel()->selectedIndexes(); |
636 | if (list.size() == ui->lvGlyphs->model()->rowCount()) |
637 | ui->lvGlyphs->clearSelection(); |
638 | else |
639 | ui->lvGlyphs->selectAll(); |
640 | } |
641 | |
642 | void MainWindow::updateSelection() |
643 | { |
644 | QModelIndexList list = ui->lvGlyphs->selectionModel()->selectedIndexes(); |
645 | QString label; |
646 | if (list.size() == ui->lvGlyphs->model()->rowCount()) |
647 | label = tr(s: "Deselect &All" ); |
648 | else |
649 | label = tr(s: "Select &All" ); |
650 | |
651 | ui->tbSelectAll->setText(label); |
652 | ui->actionSelect_all->setText(label); |
653 | |
654 | if (m_model != nullptr && ui->lwUnicodeRanges->count() > 0) { |
655 | // Ignore selection changes until we are done |
656 | disconnect(sender: ui->lwUnicodeRanges, signal: &QListWidget::itemSelectionChanged, receiver: this, slot: &MainWindow::updateUnicodeRanges); |
657 | |
658 | QSet<int> selectedGlyphIndexes; |
659 | for (const QModelIndex &modelIndex : list) |
660 | selectedGlyphIndexes.insert(value: modelIndex.row()); |
661 | |
662 | QList<DistanceFieldModel::UnicodeRange> unicodeRanges = m_model->unicodeRanges(); |
663 | std::sort(first: unicodeRanges.begin(), last: unicodeRanges.end()); |
664 | |
665 | Q_ASSERT(ui->lwUnicodeRanges->count() == unicodeRanges.size()); |
666 | for (int i = 0; i < unicodeRanges.size(); ++i) { |
667 | DistanceFieldModel::UnicodeRange unicodeRange = unicodeRanges.at(i); |
668 | QListWidgetItem *item = ui->lwUnicodeRanges->item(row: i); |
669 | |
670 | QList<glyph_t> glyphIndexes = m_model->glyphIndexesForUnicodeRange(range: unicodeRange); |
671 | Q_ASSERT(!glyphIndexes.isEmpty()); |
672 | |
673 | item->setSelected(true); |
674 | for (glyph_t glyphIndex : glyphIndexes) { |
675 | if (!selectedGlyphIndexes.contains(value: glyphIndex)) { |
676 | item->setSelected(false); |
677 | break; |
678 | } |
679 | } |
680 | } |
681 | |
682 | connect(sender: ui->lwUnicodeRanges, signal: &QListWidget::itemSelectionChanged, receiver: this, slot: &MainWindow::updateUnicodeRanges); |
683 | } |
684 | } |
685 | |
686 | void MainWindow::updateUnicodeRanges() |
687 | { |
688 | if (m_model == nullptr) |
689 | return; |
690 | |
691 | disconnect(sender: ui->lvGlyphs->selectionModel(), |
692 | signal: &QItemSelectionModel::selectionChanged, |
693 | receiver: this, |
694 | slot: &MainWindow::updateSelection); |
695 | |
696 | QItemSelection selectedItems; |
697 | |
698 | for (int i = 0; i < ui->lwUnicodeRanges->count(); ++i) { |
699 | QListWidgetItem *item = ui->lwUnicodeRanges->item(row: i); |
700 | if (item->isSelected()) { |
701 | DistanceFieldModel::UnicodeRange unicodeRange = item->data(role: Qt::UserRole).value<DistanceFieldModel::UnicodeRange>(); |
702 | QList<glyph_t> glyphIndexes = m_model->glyphIndexesForUnicodeRange(range: unicodeRange); |
703 | |
704 | for (glyph_t glyphIndex : glyphIndexes) { |
705 | QModelIndex index = m_model->index(row: glyphIndex); |
706 | selectedItems.select(topLeft: index, bottomRight: index); |
707 | } |
708 | } |
709 | } |
710 | |
711 | ui->lvGlyphs->selectionModel()->clearSelection(); |
712 | if (!selectedItems.isEmpty()) |
713 | ui->lvGlyphs->selectionModel()->select(selection: selectedItems, command: QItemSelectionModel::Select); |
714 | |
715 | connect(sender: ui->lvGlyphs->selectionModel(), |
716 | signal: &QItemSelectionModel::selectionChanged, |
717 | receiver: this, |
718 | slot: &MainWindow::updateSelection); |
719 | } |
720 | |
721 | void MainWindow::populateUnicodeRanges() |
722 | { |
723 | QList<DistanceFieldModel::UnicodeRange> unicodeRanges = m_model->unicodeRanges(); |
724 | std::sort(first: unicodeRanges.begin(), last: unicodeRanges.end()); |
725 | |
726 | for (DistanceFieldModel::UnicodeRange unicodeRange : unicodeRanges) { |
727 | QString name = m_model->nameForUnicodeRange(range: unicodeRange); |
728 | QListWidgetItem *item = new QListWidgetItem(name, ui->lwUnicodeRanges); |
729 | item->setData(role: Qt::UserRole, value: unicodeRange); |
730 | } |
731 | |
732 | ui->lwUnicodeRanges->setDisabled(false); |
733 | ui->action_Save->setDisabled(false); |
734 | ui->action_Save_as->setDisabled(false); |
735 | ui->tbSave->setDisabled(false); |
736 | } |
737 | |
738 | void MainWindow::displayError(const QString &errorString) |
739 | { |
740 | QMessageBox::warning(parent: this, title: tr(s: "Error when parsing font file" ), text: errorString, buttons: QMessageBox::Ok); |
741 | } |
742 | |
743 | void MainWindow::selectString() |
744 | { |
745 | QString s = QInputDialog::getText(parent: this, |
746 | title: tr(s: "Select glyphs for string" ), |
747 | label: tr(s: "String to parse:" )); |
748 | if (!s.isEmpty()) { |
749 | QVector<uint> ucs4String = s.toUcs4(); |
750 | for (uint ucs4 : ucs4String) { |
751 | glyph_t glyph = m_model->glyphIndexForUcs4(ucs4); |
752 | if (glyph != 0) { |
753 | ui->lvGlyphs->selectionModel()->select(index: m_model->index(row: glyph), |
754 | command: QItemSelectionModel::Select); |
755 | } |
756 | } |
757 | } |
758 | } |
759 | |
760 | void MainWindow::about() |
761 | { |
762 | QMessageBox *msgBox = new QMessageBox(this); |
763 | msgBox->setAttribute(Qt::WA_DeleteOnClose); |
764 | msgBox->setWindowTitle(tr(s: "About Qt Distance Field Generator" )); |
765 | msgBox->setText(tr(s: "<h3>Qt Distance Field Generator</h3>" |
766 | "<p>Version %1.<br/>" |
767 | "The Qt Distance Field Generator tool allows " |
768 | "to prepare a font cache for Qt applications.</p>" |
769 | "<p>Copyright (C) %2 The Qt Company Ltd.</p>" ) |
770 | .arg(a: QLatin1String(QT_VERSION_STR)) |
771 | .arg(a: QLatin1String("2019" ))); |
772 | msgBox->show(); |
773 | } |
774 | |
775 | QT_END_NAMESPACE |
776 | |