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