1 /*
2 * LibrePCB - Professional EDA for everyone!
3 * Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
4 * https://librepcb.org/
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 /*******************************************************************************
21 * Includes
22 ******************************************************************************/
23 #include "strokefont.h"
24
25 #include <fontobene-qt5/font.h>
26 #include <fontobene-qt5/glyphlistaccessor.h>
27
28 #include <QtConcurrent/QtConcurrent>
29 #include <QtCore>
30
31 /*******************************************************************************
32 * Namespace
33 ******************************************************************************/
34 namespace librepcb {
35
36 namespace fb = fontobene;
37
38 /*******************************************************************************
39 * Constructors / Destructor
40 ******************************************************************************/
41
StrokeFont(const FilePath & fontFilePath,const QByteArray & content)42 StrokeFont::StrokeFont(const FilePath& fontFilePath,
43 const QByteArray& content) noexcept
44 : QObject(nullptr), mFilePath(fontFilePath) {
45 // load the font in another thread because it takes some time to load it
46 qDebug() << "Start loading font" << mFilePath.toNative();
47 mFuture = QtConcurrent::run([content]() {
48 QTextStream s(content);
49 return fb::Font(s);
50 });
51 connect(&mWatcher, &QFutureWatcher<fb::Font>::finished, this,
52 &StrokeFont::fontLoaded);
53 mWatcher.setFuture(mFuture);
54 }
55
~StrokeFont()56 StrokeFont::~StrokeFont() noexcept {
57 }
58
59 /*******************************************************************************
60 * Getters
61 ******************************************************************************/
62
getLetterSpacing() const63 Ratio StrokeFont::getLetterSpacing() const noexcept {
64 accessor(); // block until the font is loaded.
65 return Ratio::fromNormalized(mFont->header.letterSpacing / 9);
66 }
67
getLineSpacing() const68 Ratio StrokeFont::getLineSpacing() const noexcept {
69 accessor(); // block until the font is loaded.
70 return Ratio::fromNormalized(mFont->header.lineSpacing / 9);
71 }
72
73 /*******************************************************************************
74 * General Methods
75 ******************************************************************************/
76
stroke(const QString & text,const PositiveLength & height,const Length & letterSpacing,const Length & lineSpacing,const Alignment & align,Point & bottomLeft,Point & topRight) const77 QVector<Path> StrokeFont::stroke(const QString& text,
78 const PositiveLength& height,
79 const Length& letterSpacing,
80 const Length& lineSpacing,
81 const Alignment& align, Point& bottomLeft,
82 Point& topRight) const noexcept {
83 accessor(); // block until the font is loaded. TODO: abort instead of
84 // waiting?
85 QVector<Path> paths;
86 Length totalWidth;
87 QVector<QPair<QVector<Path>, Length>> lines =
88 strokeLines(text, height, letterSpacing, totalWidth);
89 Length totalHeight = height + lineSpacing * (lines.count() - 1);
90 for (int i = 0; i < lines.count(); ++i) {
91 Point pos(0, 0);
92 if (align.getH() == HAlign::left()) {
93 pos.setX(Length(0));
94 } else if (align.getH() == HAlign::right()) {
95 pos.setX((totalWidth - lines.at(i).second) - totalWidth);
96 } else {
97 pos.setX(lines.at(i).second / -2);
98 }
99 if (align.getV() == VAlign::bottom()) {
100 pos.setY(lineSpacing * (lines.count() - i - 1));
101 } else if (align.getV() == VAlign::top()) {
102 pos.setY(-height - lineSpacing * i);
103 } else {
104 Length h = lineSpacing * (lines.count() - i - 1);
105 pos.setY(h - (totalHeight / 2));
106 }
107 foreach (const Path& p, lines.at(i).first) {
108 paths.append(p.translated(pos));
109 }
110 }
111
112 if (align.getH() == HAlign::left()) {
113 bottomLeft.setX(0);
114 topRight.setX(totalWidth);
115 } else if (align.getH() == HAlign::right()) {
116 bottomLeft.setX(-totalWidth);
117 topRight.setX(0);
118 } else {
119 bottomLeft.setX(-totalWidth / 2);
120 topRight.setX(totalWidth / 2);
121 }
122 if (align.getV() == VAlign::bottom()) {
123 bottomLeft.setY(0);
124 topRight.setY(totalHeight);
125 } else if (align.getV() == VAlign::top()) {
126 bottomLeft.setY(-totalHeight);
127 topRight.setY(0);
128 } else {
129 bottomLeft.setY(-totalHeight / 2);
130 topRight.setY(totalHeight / 2);
131 }
132
133 return paths;
134 }
135
strokeLines(const QString & text,const PositiveLength & height,const Length & letterSpacing,Length & width) const136 QVector<QPair<QVector<Path>, Length>> StrokeFont::strokeLines(
137 const QString& text, const PositiveLength& height,
138 const Length& letterSpacing, Length& width) const noexcept {
139 QVector<QPair<QVector<Path>, Length>> result;
140 foreach (const QString& line, text.split('\n')) {
141 QPair<QVector<Path>, Length> pair;
142 pair.first = strokeLine(line, height, letterSpacing, pair.second);
143 result.append(pair);
144 if (pair.second > width) width = pair.second;
145 }
146 return result;
147 }
148
strokeLine(const QString & text,const PositiveLength & height,const Length & letterSpacing,Length & width) const149 QVector<Path> StrokeFont::strokeLine(const QString& text,
150 const PositiveLength& height,
151 const Length& letterSpacing,
152 Length& width) const noexcept {
153 QVector<Path> paths;
154 Length offset = 0;
155 width = 0; // same as offset, but without last letter spacing
156 for (int i = 0; i < text.length(); ++i) {
157 Length glyphSpacing;
158 QVector<Path> glyphPaths = strokeGlyph(text.at(i), height, glyphSpacing);
159 if (!glyphPaths.isEmpty()) {
160 Point bottomLeft, topRight;
161 computeBoundingRect(glyphPaths, bottomLeft, topRight);
162 Length shift =
163 (i == 0) ? -bottomLeft.getX() : 0; // left-align first character
164 foreach (const Path& p, glyphPaths) {
165 paths.append(p.translated(Point(offset + shift, Length(0))));
166 }
167 width = offset + topRight.getX() +
168 shift; // do *not* count glyph spacing as width!
169 offset = width + glyphSpacing + letterSpacing;
170 } else if (glyphSpacing != 0) {
171 // it's a whitespace-only glyph -> count additional glyph spacing as width
172 width = offset + glyphSpacing;
173 offset = width + letterSpacing;
174 }
175 }
176 return paths;
177 }
178
strokeGlyph(const QChar & glyph,const PositiveLength & height,Length & spacing) const179 QVector<Path> StrokeFont::strokeGlyph(const QChar& glyph,
180 const PositiveLength& height,
181 Length& spacing) const noexcept {
182 try {
183 qreal glyphSpacing = 0;
184 QVector<fb::Polyline> polylines =
185 accessor().getAllPolylinesOfGlyph(glyph.unicode(),
186 &glyphSpacing); // can throw
187 spacing = convertLength(height, glyphSpacing);
188 return polylines2paths(polylines, height);
189 } catch (const fb::Exception& e) {
190 qWarning() << "Failed to load stroke font glyph" << glyph;
191 spacing = 0;
192 return QVector<Path>();
193 }
194 }
195
196 /*******************************************************************************
197 * Private Methods
198 ******************************************************************************/
199
fontLoaded()200 void StrokeFont::fontLoaded() noexcept {
201 accessor(); // trigger the message about loading succeeded or failed
202 }
203
accessor() const204 const fb::GlyphListAccessor& StrokeFont::accessor() const noexcept {
205 if (!mFont) {
206 try {
207 mFont.reset(new fb::Font(mFuture.result())); // can throw
208 qDebug() << "Successfully loaded font" << mFilePath.toNative() << "with"
209 << mFont->glyphs.count() << "glyphs";
210 } catch (const fb::Exception& e) {
211 mFont.reset(new fb::Font());
212 qCritical() << "Failed to load font" << mFilePath.toNative();
213 qCritical() << "Error:" << e.msg();
214 }
215
216 mGlyphListCache.reset(new fb::GlyphListCache(mFont->glyphs));
217 mGlyphListCache->setReplacementGlyph(
218 0xFFFD); // U+FFFD REPLACEMENT CHARACTER
219 mGlyphListCache->addReplacements(
220 {0x00B5, 0x03BC}); // MICRO SIGN <-> GREEK SMALL LETTER MU
221 mGlyphListCache->addReplacements(
222 {0x2126, 0x03A9}); // OHM SIGN <-> GREEK CAPITAL LETTER OMEGA
223
224 mGlyphListAccessor.reset(new fb::GlyphListAccessor(*mGlyphListCache));
225 }
226 return *mGlyphListAccessor;
227 }
228
polylines2paths(const QVector<fb::Polyline> & polylines,const PositiveLength & height)229 QVector<Path> StrokeFont::polylines2paths(
230 const QVector<fb::Polyline>& polylines,
231 const PositiveLength& height) noexcept {
232 QVector<Path> paths;
233 foreach (const fb::Polyline& p, polylines) {
234 if (p.isEmpty()) continue;
235 paths.append(polyline2path(p, height));
236 }
237 return paths;
238 }
239
polyline2path(const fb::Polyline & p,const PositiveLength & height)240 Path StrokeFont::polyline2path(const fb::Polyline& p,
241 const PositiveLength& height) noexcept {
242 Path path;
243 foreach (const fb::Vertex& v, p) { path.addVertex(convertVertex(v, height)); }
244 return path;
245 }
246
convertVertex(const fb::Vertex & v,const PositiveLength & height)247 Vertex StrokeFont::convertVertex(const fb::Vertex& v,
248 const PositiveLength& height) noexcept {
249 return Vertex(
250 Point::fromMm(v.scaledX(height->toMm()), v.scaledY(height->toMm())),
251 Angle::fromDeg(v.scaledBulge(180)));
252 }
253
convertLength(const PositiveLength & height,qreal length) const254 Length StrokeFont::convertLength(const PositiveLength& height,
255 qreal length) const noexcept {
256 return Length(height->toNm() * length / 9);
257 }
258
computeBoundingRect(const QVector<Path> & paths,Point & bottomLeft,Point & topRight)259 void StrokeFont::computeBoundingRect(const QVector<Path>& paths,
260 Point& bottomLeft,
261 Point& topRight) noexcept {
262 QRectF rect = Path::toQPainterPathPx(paths, false).boundingRect();
263 bottomLeft = Point::fromPx(rect.bottomLeft());
264 topRight = Point::fromPx(rect.topRight());
265 }
266
267 /*******************************************************************************
268 * End of File
269 ******************************************************************************/
270
271 } // namespace librepcb
272