Loading files
/*
* LibrePCB - Professional EDA for everyone!
* Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
* https://librepcb.org/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*******************************************************************************
* Includes
******************************************************************************/
#include "strokefont.h"
#if FONTOBENE_QT5
#warning "Using legacy fontobene-qt5, please replace it by fontobene-qt."
#include <fontobene-qt5/font.h>
#include <fontobene-qt5/glyphlistaccessor.h>
#else
#include <fontobene-qt/font.h>
#include <fontobene-qt/glyphlistaccessor.h>
#endif
#include <QtConcurrent>
#include <QtCore>
/*******************************************************************************
* Namespace
******************************************************************************/
namespace librepcb {
namespace fb = fontobene;
/*******************************************************************************
* Constructors / Destructor
******************************************************************************/
StrokeFont::StrokeFont(const FilePath& fontFilePath,
const QByteArray& content) noexcept
: QObject(nullptr), mFilePath(fontFilePath) {
// load the font in another thread because it takes some time to load it
qDebug() << "Start loading stroke font " << mFilePath.toNative()
<< "in worker thread...";
mFuture = QtConcurrent::run([content]() {
QTextStream s(content);
return std::make_shared<fb::Font>(s);
});
connect(&mWatcher, &QFutureWatcher<fb::Font>::finished, this,
&StrokeFont::fontLoaded);
mWatcher.setFuture(mFuture);
}
StrokeFont::~StrokeFont() noexcept {
}
/*******************************************************************************
* Getters
******************************************************************************/
Ratio StrokeFont::getLetterSpacing() const noexcept {
accessor(); // block until the font is loaded.
return Ratio::fromNormalized(mFont->header.letterSpacing / 9);
}
Ratio StrokeFont::getLineSpacing() const noexcept {
accessor(); // block until the font is loaded.
return Ratio::fromNormalized(mFont->header.lineSpacing / 9);
}
/*******************************************************************************
* General Methods
******************************************************************************/
QVector<Path> StrokeFont::stroke(const QString& text,
const PositiveLength& height,
const Length& letterSpacing,
const Length& lineSpacing,
const Alignment& align, Point& bottomLeft,
Point& topRight) const noexcept {
accessor(); // block until the font is loaded. TODO: abort instead of
// waiting?
QVector<Path> paths;
Length totalWidth;
QVector<QPair<QVector<Path>, Length>> lines =
strokeLines(text, height, letterSpacing, totalWidth);
Length totalHeight = height + lineSpacing * (lines.count() - 1);
for (int i = 0; i < lines.count(); ++i) {
Point pos(0, 0);
if (align.getH() == HAlign::left()) {
pos.setX(Length(0));
} else if (align.getH() == HAlign::right()) {
pos.setX((totalWidth - lines.at(i).second) - totalWidth);
} else {
pos.setX(lines.at(i).second / -2);
}
if (align.getV() == VAlign::bottom()) {
pos.setY(lineSpacing * (lines.count() - i - 1));
} else if (align.getV() == VAlign::top()) {
pos.setY(-height - lineSpacing * i);
} else {
Length h = lineSpacing * (lines.count() - i - 1);
pos.setY(h - (totalHeight / 2));
}
foreach (const Path& p, lines.at(i).first) {
paths.append(p.translated(pos));
}
}
if (align.getH() == HAlign::left()) {
bottomLeft.setX(0);
topRight.setX(totalWidth);
} else if (align.getH() == HAlign::right()) {
bottomLeft.setX(-totalWidth);
topRight.setX(0);
} else {
bottomLeft.setX(-totalWidth / 2);
topRight.setX(totalWidth / 2);
}
if (align.getV() == VAlign::bottom()) {
bottomLeft.setY(0);
topRight.setY(totalHeight);
} else if (align.getV() == VAlign::top()) {
bottomLeft.setY(-totalHeight);
topRight.setY(0);
} else {
bottomLeft.setY(-totalHeight / 2);
topRight.setY(totalHeight / 2);
}
return paths;
}
QVector<QPair<QVector<Path>, Length>> StrokeFont::strokeLines(
const QString& text, const PositiveLength& height,
const Length& letterSpacing, Length& width) const noexcept {
QVector<QPair<QVector<Path>, Length>> result;
foreach (const QString& line, text.split('\n')) {
QPair<QVector<Path>, Length> pair;
pair.first = strokeLine(line, height, letterSpacing, pair.second);
result.append(pair);
if (pair.second > width) width = pair.second;
}
return result;
}
QVector<Path> StrokeFont::strokeLine(const QString& text,
const PositiveLength& height,
const Length& letterSpacing,
Length& width) const noexcept {
QVector<Path> paths;
Length offset = 0;
width = 0; // same as offset, but without last letter spacing
for (int i = 0; i < text.length(); ++i) {
Length glyphSpacing;
QVector<Path> glyphPaths = strokeGlyph(text.at(i), height, glyphSpacing);
if (!glyphPaths.isEmpty()) {
Point bottomLeft, topRight;
computeBoundingRect(glyphPaths, bottomLeft, topRight);
Length shift =
(i == 0) ? -bottomLeft.getX() : 0; // left-align first character
foreach (const Path& p, glyphPaths) {
paths.append(p.translated(Point(offset + shift, Length(0))));
}
width = offset + topRight.getX() +
shift; // do *not* count glyph spacing as width!
offset = width + glyphSpacing + letterSpacing;
} else if (glyphSpacing != 0) {
// it's a whitespace-only glyph -> count additional glyph spacing as width
width = offset + glyphSpacing;
offset = width + letterSpacing;
}
}
return paths;
}
QVector<Path> StrokeFont::strokeGlyph(const QChar& glyph,
const PositiveLength& height,
Length& spacing) const noexcept {
try {
qreal glyphSpacing = 0;
QVector<fb::Polyline> polylines =
accessor().getAllPolylinesOfGlyph(glyph.unicode(),
&glyphSpacing); // can throw
spacing = convertLength(height, glyphSpacing);
return polylines2paths(polylines, height);
} catch (const fb::Exception& e) {
qWarning().nospace() << "Failed to load stroke font glyph " << glyph << ".";
spacing = 0;
return QVector<Path>();
}
}
/*******************************************************************************
* Private Methods
******************************************************************************/
void StrokeFont::fontLoaded() noexcept {
accessor(); // trigger the message about loading succeeded or failed
}
const fb::GlyphListAccessor& StrokeFont::accessor() const noexcept {
if (!mFont) {
try {
mFont = mFuture.result(); // can throw
qDebug() << "Successfully loaded stroke font" << mFilePath.toNative()
<< "with" << mFont->glyphs.count() << "glyphs.";
} catch (const fb::Exception& e) {
mFont = std::make_shared<fb::Font>();
qCritical().nospace() << "Failed to load stroke font "
<< mFilePath.toNative() << ": " << e.msg();
}
mGlyphListCache.reset(new fb::GlyphListCache(mFont->glyphs));
mGlyphListCache->setReplacementGlyph(
0xFFFD); // U+FFFD REPLACEMENT CHARACTER
mGlyphListCache->addReplacements(
{0x00B5, 0x03BC}); // MICRO SIGN <-> GREEK SMALL LETTER MU
mGlyphListCache->addReplacements(
{0x2126, 0x03A9}); // OHM SIGN <-> GREEK CAPITAL LETTER OMEGA
mGlyphListAccessor.reset(new fb::GlyphListAccessor(*mGlyphListCache));
}
return *mGlyphListAccessor;
}
QVector<Path> StrokeFont::polylines2paths(
const QVector<fb::Polyline>& polylines,
const PositiveLength& height) noexcept {
QVector<Path> paths;
foreach (const fb::Polyline& p, polylines) {
if (p.isEmpty()) continue;
paths.append(polyline2path(p, height));
}
return paths;
}
Path StrokeFont::polyline2path(const fb::Polyline& p,
const PositiveLength& height) noexcept {
Path path;
foreach (const fb::Vertex& v, p) {
path.addVertex(convertVertex(v, height));
}
return path;
}
Vertex StrokeFont::convertVertex(const fb::Vertex& v,
const PositiveLength& height) noexcept {
return Vertex(
Point::fromMm(v.scaledX(height->toMm()), v.scaledY(height->toMm())),
Angle::fromDeg(v.scaledBulge(180)));
}
Length StrokeFont::convertLength(const PositiveLength& height,
qreal length) const noexcept {
return Length(height->toNm() * length / 9);
}
void StrokeFont::computeBoundingRect(const QVector<Path>& paths,
Point& bottomLeft,
Point& topRight) noexcept {
QRectF rect = Path::toQPainterPathPx(paths, false).boundingRect();
bottomLeft = Point::fromPx(rect.bottomLeft());
topRight = Point::fromPx(rect.topRight());
}
/*******************************************************************************
* End of File
******************************************************************************/
} // namespace librepcb
efeedbe3fThis file is pinned to the selected release. Download the original to open it in your design tool.