MuseScore/src/engraving/libmscore/scorefont.cpp

766 lines
23 KiB
C++

/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2021 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "scorefont.h"
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include "draw/painter.h"
#include "types/symnames.h"
#include "mscore.h"
#include "log.h"
using namespace Ms;
using namespace mu;
using namespace mu::draw;
using namespace mu::engraving;
static constexpr int FALLBACK_FONT_INDEX = 1; // Bravura
std::vector<ScoreFont> ScoreFont::s_scoreFonts {
ScoreFont("Leland", "Leland", ":/fonts/leland/", "Leland.otf"),
ScoreFont("Bravura", "Bravura", ":/fonts/bravura/", "Bravura.otf"),
ScoreFont("Emmentaler", "MScore", ":/fonts/mscore/", "mscore.ttf"),
ScoreFont("Gonville", "Gootville", ":/fonts/gootville/", "Gootville.otf"),
ScoreFont("MuseJazz", "MuseJazz", ":/fonts/musejazz/", "MuseJazz.otf"),
ScoreFont("Petaluma", "Petaluma", ":/fonts/petaluma/", "Petaluma.otf"),
};
std::array<uint, size_t(SymId::lastSym) + 1> ScoreFont::s_symIdCodes { { 0 } };
// =============================================
// ScoreFont
// =============================================
ScoreFont::ScoreFont(const char* name, const char* family, const char* path, const char* filename)
: m_symbols(static_cast<size_t>(SymId::lastSym) + 1),
m_name(name),
m_family(family),
m_fontPath(path),
m_filename(filename)
{
}
ScoreFont::ScoreFont(const ScoreFont& other)
{
m_loaded = false;
m_symbols = other.m_symbols;
m_name = other.m_name;
m_family = other.m_family;
m_fontPath = other.m_fontPath;
m_filename = other.m_filename;
}
// =============================================
// Properties
// =============================================
const QString& ScoreFont::name() const
{
return m_name;
}
const QString& ScoreFont::family() const
{
return m_family;
}
const QString& ScoreFont::fontPath() const
{
return m_fontPath;
}
std::list<std::pair<Sid, QVariant> > ScoreFont::engravingDefaults()
{
return m_engravingDefaults;
}
double ScoreFont::textEnclosureThickness()
{
return m_textEnclosureThickness;
}
// =============================================
// Init ScoreFonts
// =============================================
void ScoreFont::initScoreFonts()
{
QJsonObject glyphNamesJson(ScoreFont::initGlyphNamesJson());
IF_ASSERT_FAILED(!glyphNamesJson.empty()) {
LOGE() << "Could not read glyph names JSON";
return;
}
for (size_t i = 0; i < s_symIdCodes.size(); ++i) {
QString name(SymNames::nameForSymId(static_cast<SymId>(i)));
bool ok;
uint code = glyphNamesJson.value(name).toObject().value("codepoint").toString().midRef(2).toUInt(&ok, 16);
if (ok) {
s_symIdCodes[i] = code;
} else if (MScore::debugMode) {
LOGD() << "could not read codepoint for glyph " << name;
}
}
fontProvider()->insertSubstitution("Leland Text", "Bravura Text");
fontProvider()->insertSubstitution("Bravura Text", "Leland Text");
fontProvider()->insertSubstitution("MScore Text", "Leland Text");
fontProvider()->insertSubstitution("Gootville Text", "Leland Text");
fontProvider()->insertSubstitution("MuseJazz Text", "Leland Text");
fontProvider()->insertSubstitution("Petaluma Text", "MuseJazz Text");
fontProvider()->insertSubstitution("ScoreFont", "Leland Text"); // alias for current Musical Text Font
fallbackFont(); // load fallback font
}
QJsonObject ScoreFont::initGlyphNamesJson()
{
QFile file(":fonts/smufl/glyphnames.json");
if (!file.open(QIODevice::ReadOnly)) {
LOGE() << "could not open glyph names JSON file.";
return QJsonObject();
}
QJsonParseError error;
QJsonObject glyphNamesJson = QJsonDocument::fromJson(file.readAll(), &error).object();
file.close();
if (error.error != QJsonParseError::NoError) {
LOGE() << "JSON parse error in glyph names file: " << error.errorString()
<< " (offset: " << error.offset << ")";
return QJsonObject();
}
return glyphNamesJson;
}
// =============================================
// Available ScoreFonts
// =============================================
const std::vector<ScoreFont>& ScoreFont::scoreFonts()
{
return s_scoreFonts;
}
ScoreFont* ScoreFont::fontByName(const QString& name)
{
ScoreFont* font = nullptr;
for (ScoreFont& f : s_scoreFonts) {
if (f.name().toLower() == name.toLower()) { // case insensitive
font = &f;
break;
}
}
if (!font) {
LOGE() << "ScoreFont not found in list: " << name;
LOGE() << "ScoreFonts in list:";
for (const ScoreFont& f : s_scoreFonts) {
LOGE() << " " << f.name();
}
font = fallbackFont();
LOGE() << "Using fallback font " << font->name() << " instead.";
return font;
}
if (!font->m_loaded) {
font->load();
}
return font;
}
ScoreFont* ScoreFont::fallbackFont()
{
ScoreFont* font = &s_scoreFonts[FALLBACK_FONT_INDEX];
if (!font->m_loaded) {
font->load();
}
return font;
}
const char* ScoreFont::fallbackTextFont()
{
return "Bravura Text";
}
// =============================================
// Load
// =============================================
void ScoreFont::load()
{
QString facePath = m_fontPath + m_filename;
if (-1 == fontProvider()->addApplicationFont(m_family, facePath)) {
LOGE() << "fatal error: cannot load internal font: " << facePath;
return;
}
m_font.setWeight(mu::draw::Font::Normal);
m_font.setItalic(false);
m_font.setFamily(m_family);
m_font.setNoFontMerging(true);
m_font.setHinting(mu::draw::Font::Hinting::PreferVerticalHinting);
for (size_t id = 0; id < s_symIdCodes.size(); ++id) {
uint code = s_symIdCodes[id];
if (code == 0) {
continue;
}
Sym& sym = m_symbols[id];
computeMetrics(sym, code);
}
QFile metadataFile(m_fontPath + "metadata.json");
if (!metadataFile.open(QIODevice::ReadOnly)) {
LOGE() << "Failed to open glyph metadata file: " << metadataFile.fileName();
return;
}
QJsonParseError error;
QJsonObject metadataJson = QJsonDocument::fromJson(metadataFile.readAll(), &error).object();
if (error.error != QJsonParseError::NoError) {
LOGE() << "Json parse error in " << metadataFile.fileName()
<< ", offset " << error.offset << ": " << error.errorString();
return;
}
loadGlyphsWithAnchors(metadataJson.value("glyphsWithAnchors").toObject());
loadComposedGlyphs();
loadStylisticAlternates(metadataJson.value("glyphsWithAlternates").toObject());
loadEngravingDefaults(metadataJson.value("engravingDefaults").toObject());
m_loaded = true;
}
void ScoreFont::loadGlyphsWithAnchors(const QJsonObject& glyphsWithAnchors)
{
for (const QString& symName : glyphsWithAnchors.keys()) {
SymId symId = SymNames::symIdByName(symName);
if (symId == SymId::noSym) {
//! NOTE currently, Bravura contains a bunch of entries in glyphsWithAnchors
//! for glyph names that will not be found - flag32ndUpStraight, etc.
continue;
}
Sym& sym = this->sym(symId);
QJsonObject anchors = glyphsWithAnchors.value(symName).toObject();
static const std::unordered_map<QString, SmuflAnchorId> smuflAnchorIdNames {
{ "stemDownNW", SmuflAnchorId::stemDownNW },
{ "stemUpSE", SmuflAnchorId::stemUpSE },
{ "stemDownSW", SmuflAnchorId::stemDownSW },
{ "stemUpNW", SmuflAnchorId::stemUpNW },
{ "cutOutNE", SmuflAnchorId::cutOutNE },
{ "cutOutNW", SmuflAnchorId::cutOutNW },
{ "cutOutSE", SmuflAnchorId::cutOutSE },
{ "cutOutSW", SmuflAnchorId::cutOutSW },
{ "opticalCenter", SmuflAnchorId::opticalCenter },
};
for (const QString& anchorId : anchors.keys()) {
auto search = smuflAnchorIdNames.find(anchorId);
if (search == smuflAnchorIdNames.cend()) {
//LOGD() << "Unhandled SMuFL anchorId: " << anchorId;
continue;
}
QJsonArray arr = anchors.value(anchorId).toArray();
double x = arr.at(0).toDouble();
double y = arr.at(1).toDouble();
sym.smuflAnchors[search->second] = PointF(x, -y) * SPATIUM20;
}
}
}
void ScoreFont::loadComposedGlyphs()
{
static const struct ComposedGlyph {
const SymId id;
const SymIdList subSymbolIds;
} composedGlyphs[] = {
{ SymId::ornamentPrallMordent, {
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentMiddleVerticalStroke,
SymId::ornamentZigZagLineWithRightEnd
} },
{ SymId::ornamentUpPrall, {
SymId::ornamentBottomLeftConcaveStroke,
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentZigZagLineWithRightEnd
} },
{ SymId::ornamentUpMordent, {
SymId::ornamentBottomLeftConcaveStroke,
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentMiddleVerticalStroke,
SymId::ornamentZigZagLineWithRightEnd
} },
{ SymId::ornamentPrallDown, {
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentBottomRightConcaveStroke,
} },
{ SymId::ornamentDownMordent, {
SymId::ornamentLeftVerticalStroke,
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentMiddleVerticalStroke,
SymId::ornamentZigZagLineWithRightEnd
} },
{ SymId::ornamentPrallUp, {
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentTopRightConvexStroke,
} },
{ SymId::ornamentLinePrall, {
SymId::ornamentLeftVerticalStroke,
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentZigZagLineNoRightEnd,
SymId::ornamentZigZagLineWithRightEnd
} }
};
for (const ComposedGlyph& c : composedGlyphs) {
Sym& sym = this->sym(c.id);
if (!sym.isValid()) {
sym.subSymbolIds = c.subSymbolIds;
sym.bbox = bbox(c.subSymbolIds, 1.0);
}
}
}
void ScoreFont::loadStylisticAlternates(const QJsonObject& glyphsWithAlternatesObject)
{
static const struct GlyphWithAlternates {
const QString key;
const QString alternateKey;
const SymId alternateSymId;
} glyphsWithAlternates[] = {
{ QString("4stringTabClef"),
QString("4stringTabClefSerif"),
SymId::fourStringTabClefSerif
},
{ QString("6stringTabClef"),
QString("6stringTabClefSerif"),
SymId::sixStringTabClefSerif
},
{ QString("cClef"),
QString("cClefFrench"),
SymId::cClefFrench
},
{ QString("cClef"),
QString("cClefFrench20C"),
SymId::cClefFrench20C
},
{ QString("fClef"),
QString("fClefFrench"),
SymId::fClefFrench
},
{ QString("fClef"),
QString("fClef19thCentury"),
SymId::fClef19thCentury
},
{ QString("noteheadBlack"),
QString("noteheadBlackOversized"),
SymId::noteheadBlack
},
{ QString("noteheadHalf"),
QString("noteheadHalfOversized"),
SymId::noteheadHalf
},
{ QString("noteheadWhole"),
QString("noteheadWholeOversized"),
SymId::noteheadWhole
},
{ QString("noteheadDoubleWhole"),
QString("noteheadDoubleWholeOversized"),
SymId::noteheadDoubleWhole
},
{ QString("noteheadDoubleWholeSquare"),
QString("noteheadDoubleWholeSquareOversized"),
SymId::noteheadDoubleWholeSquare
},
{ QString("noteheadDoubleWhole"),
QString("noteheadDoubleWholeAlt"),
SymId::noteheadDoubleWholeAlt
},
{ QString("brace"),
QString("braceSmall"),
SymId::braceSmall
},
{ QString("brace"),
QString("braceLarge"),
SymId::braceLarge
},
{ QString("brace"),
QString("braceLarger"),
SymId::braceLarger
},
{ QString("flag1024thDown"),
QString("flag1024thDownStraight"),
SymId::flag1024thDownStraight
},
{ QString("flag1024thUp"),
QString("flag1024thUpStraight"),
SymId::flag1024thUpStraight
},
{ QString("flag128thDown"),
QString("flag128thDownStraight"),
SymId::flag128thDownStraight
},
{ QString("flag128thUp"),
QString("flag128thUpStraight"),
SymId::flag128thUpStraight
},
{ QString("flag16thDown"),
QString("flag16thDownStraight"),
SymId::flag16thDownStraight
},
{ QString("flag16thUp"),
QString("flag16thUpStraight"),
SymId::flag16thUpStraight
},
{ QString("flag256thDown"),
QString("flag256thDownStraight"),
SymId::flag256thDownStraight
},
{ QString("flag256thUp"),
QString("flag256thUpStraight"),
SymId::flag256thUpStraight
},
{ QString("flag32ndDown"),
QString("flag32ndDownStraight"),
SymId::flag32ndDownStraight
},
{ QString("flag32ndUp"),
QString("flag32ndUpStraight"),
SymId::flag32ndUpStraight
},
{ QString("flag512thDown"),
QString("flag512thDownStraight"),
SymId::flag512thDownStraight
},
{ QString("flag512thUp"),
QString("flag512thUpStraight"),
SymId::flag512thUpStraight
},
{ QString("flag64thDown"),
QString("flag64thDownStraight"),
SymId::flag64thDownStraight
},
{ QString("flag64thUp"),
QString("flag64thUpStraight"),
SymId::flag64thUpStraight
},
{ QString("flag8thDown"),
QString("flag8thDownStraight"),
SymId::flag8thDownStraight
},
{ QString("flag8thUp"),
QString("flag8thUpStraight"),
SymId::flag8thUpStraight
}
};
bool ok;
for (const GlyphWithAlternates& glyph : glyphsWithAlternates) {
const QJsonObject::const_iterator glyphIt = glyphsWithAlternatesObject.find(glyph.key);
if (glyphIt != glyphsWithAlternatesObject.end()) {
const QJsonArray alternatesArray = glyphIt.value().toObject().value("alternates").toArray();
// locate the relevant altKey in alternate array
const QJsonArray::const_iterator alternateIt
= std::find_if(alternatesArray.cbegin(), alternatesArray.cend(), [&glyph](const QJsonValue& value) {
return value.toObject().value("name") == glyph.alternateKey;
});
if (alternateIt != alternatesArray.cend()) {
Sym& sym = this->sym(glyph.alternateSymId);
uint code = alternateIt->toObject().value("codepoint").toString().midRef(2).toUInt(&ok, 16);
if (ok) {
computeMetrics(sym, code);
}
}
}
}
}
void ScoreFont::loadEngravingDefaults(const QJsonObject& engravingDefaultsObject)
{
static const std::list<std::pair<QString, Sid> > engravingDefaultsMapping = {
{ "staffLineThickness", Sid::staffLineWidth },
{ "stemThickness", Sid::stemWidth },
{ "beamThickness", Sid::beamWidth },
{ "beamSpacing", Sid::useWideBeams },
{ "legerLineThickness", Sid::ledgerLineWidth },
{ "legerLineExtension", Sid::ledgerLineLength },
{ "slurEndpointThickness", Sid::SlurEndWidth },
{ "slurMidpointThickness", Sid::SlurMidWidth },
{ "thinBarlineThickness", Sid::barWidth },
{ "thinBarlineThickness", Sid::doubleBarWidth },
{ "thickBarlineThickness", Sid::endBarWidth },
{ "dashedBarlineThickness", Sid::barWidth },
{ "barlineSeparation", Sid::doubleBarDistance },
{ "barlineSeparation", Sid::endBarDistance },
{ "repeatBarlineDotSeparation", Sid::repeatBarlineDotSeparation },
{ "bracketThickness", Sid::bracketWidth },
{ "hairpinThickness", Sid::hairpinLineWidth },
{ "octaveLineThickness", Sid::ottavaLineWidth },
{ "pedalLineThickness", Sid::pedalLineWidth },
{ "repeatEndingLineThickness", Sid::voltaLineWidth },
{ "lyricLineThickness", Sid::lyricsLineThickness },
{ "tupletBracketThickness", Sid::tupletBracketWidth }
};
for (const QString& key : engravingDefaultsObject.keys()) {
if (key == "textEnclosureThickness") {
m_textEnclosureThickness = engravingDefaultsObject.value(key).toDouble();
continue;
}
for (auto mapping : engravingDefaultsMapping) {
if (key == mapping.first) {
qreal value = engravingDefaultsObject.value(key).toDouble();
if (key == "beamSpacing") {
value = value > 0.75;
}
m_engravingDefaults.push_back({ mapping.second, value });
}
}
}
m_engravingDefaults.push_back({ Sid::MusicalTextFont, QString("%1 Text").arg(m_family) });
}
void ScoreFont::computeMetrics(ScoreFont::Sym& sym, uint code)
{
sym.code = code;
sym.bbox = fontProvider()->symBBox(m_font, code, DPI_F);
sym.advance = fontProvider()->symAdvance(m_font, code, DPI_F);
}
// =============================================
// Symbol properties
// =============================================
ScoreFont::Sym& ScoreFont::sym(SymId id)
{
return m_symbols[static_cast<size_t>(id)];
}
const ScoreFont::Sym& ScoreFont::sym(SymId id) const
{
return m_symbols.at(static_cast<size_t>(id));
}
uint ScoreFont::symCode(SymId id) const
{
const Sym& s = sym(id);
if (s.isValid()) {
return s.code;
}
// fallback: search in the common SMuFL table
return s_symIdCodes.at(static_cast<size_t>(id));
}
SymId ScoreFont::fromCode(uint code) const
{
auto it = std::find_if(m_symbols.begin(), m_symbols.end(), [code](const Sym& s) { return s.code == code; });
return static_cast<SymId>(it == m_symbols.end() ? 0 : it - m_symbols.begin());
}
static QString codeToString(uint code)
{
return QString::fromUcs4(&code, 1);
}
QString ScoreFont::toString(SymId id) const
{
return codeToString(symCode(id));
}
bool ScoreFont::isValid(SymId id) const
{
return sym(id).isValid();
}
bool ScoreFont::useFallbackFont(SymId id) const
{
return MScore::useFallbackFont && !sym(id).isValid() && this != ScoreFont::fallbackFont();
}
// =============================================
// Symbol bounding box
// =============================================
const RectF ScoreFont::bbox(SymId id, qreal mag) const
{
return bbox(id, SizeF(mag, mag));
}
const RectF ScoreFont::bbox(SymId id, const SizeF& mag) const
{
if (useFallbackFont(id)) {
return fallbackFont()->bbox(id, mag);
}
RectF r = sym(id).bbox;
return RectF(r.x() * mag.width(), r.y() * mag.height(),
r.width() * mag.width(), r.height() * mag.height());
}
const RectF ScoreFont::bbox(const SymIdList& s, qreal mag) const
{
return bbox(s, SizeF(mag, mag));
}
const RectF ScoreFont::bbox(const SymIdList& s, const SizeF& mag) const
{
RectF r;
PointF pos;
for (SymId id : s) {
r.unite(bbox(id, mag).translated(pos));
pos.rx() += advance(id, mag.width());
}
return r;
}
// =============================================
// Symbol metrics
// =============================================
qreal ScoreFont::width(SymId id, qreal mag) const
{
return bbox(id, mag).width();
}
qreal ScoreFont::height(SymId id, qreal mag) const
{
return bbox(id, mag).height();
}
qreal ScoreFont::advance(SymId id, qreal mag) const
{
if (useFallbackFont(id)) {
return fallbackFont()->advance(id, mag);
}
return sym(id).advance * mag;
}
qreal ScoreFont::width(const SymIdList& s, qreal mag) const
{
return bbox(s, mag).width();
}
PointF ScoreFont::smuflAnchor(SymId symId, SmuflAnchorId anchorId, qreal mag) const
{
if (useFallbackFont(symId)) {
return fallbackFont()->smuflAnchor(symId, anchorId, mag);
}
return const_cast<Sym&>(sym(symId)).smuflAnchors[anchorId] * mag;
}
// =============================================
// Draw
// =============================================
void ScoreFont::draw(SymId id, Painter* painter, const SizeF& mag, const PointF& pos) const
{
const Sym& sym = this->sym(id);
if (sym.isCompound()) { // is this a compound symbol?
draw(sym.subSymbolIds, painter, mag, pos);
return;
}
if (!sym.isValid()) {
if (MScore::useFallbackFont && this != ScoreFont::fallbackFont()) {
fallbackFont()->draw(id, painter, mag, pos);
} else {
LOGE() << "invalid sym: " << static_cast<size_t>(id);
}
return;
}
painter->save();
qreal size = 20.0 * MScore::pixelRatio;
m_font.setPointSizeF(size);
painter->scale(mag.width(), mag.height());
painter->setFont(m_font);
painter->drawSymbol(PointF(pos.x() / mag.width(), pos.y() / mag.height()), symCode(id));
painter->restore();
}
void ScoreFont::draw(SymId id, Painter* painter, qreal mag, const PointF& pos) const
{
draw(id, painter, SizeF(mag, mag), pos);
}
void ScoreFont::draw(SymId id, mu::draw::Painter* painter, qreal mag, const PointF& pos, int n) const
{
SymIdList list(n, id);
draw(list, painter, mag, pos);
}
void ScoreFont::draw(const SymIdList& ids, Painter* painter, qreal mag, const PointF& startPos) const
{
PointF pos(startPos);
for (SymId id : ids) {
draw(id, painter, mag, pos);
pos.setX(pos.x() + advance(id, mag));
}
}
void ScoreFont::draw(const SymIdList& ids, Painter* painter, const SizeF& mag, const PointF& startPos) const
{
PointF pos(startPos);
for (SymId id : ids) {
draw(id, painter, mag, pos);
pos.setX(pos.x() + advance(id, mag.width()));
}
}