File indexing completed on 2025-02-02 04:11:27
0001 /* 0002 * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best> 0003 * 0004 * SPDX-License-Identifier: GPL-3.0-or-later 0005 */ 0006 0007 #include "font_loader.hpp" 0008 0009 #include <set> 0010 0011 #include <QFile> 0012 #include <QNetworkReply> 0013 #include <QNetworkRequest> 0014 #include <QNetworkAccessManager> 0015 #include <QRegularExpression> 0016 0017 #include "io/svg/css_parser.hpp" 0018 #include "model/document.hpp" 0019 #include "model/assets/pending_asset.hpp" 0020 0021 0022 0023 0024 class glaxnimate::gui::font::FontLoader::Private 0025 { 0026 public: 0027 struct QueueItem 0028 { 0029 int id = -2; 0030 QString name_alias; 0031 QUrl url; 0032 QUrl parent_url = {}; 0033 }; 0034 0035 QNetworkAccessManager downloader; 0036 std::vector<model::CustomFont> fonts; 0037 int resolved = 0; 0038 std::vector<QueueItem> queued; 0039 std::set<QNetworkReply*> active_replies; 0040 bool loading = false; 0041 FontLoader* parent; 0042 0043 void load_file(const QueueItem& item) 0044 { 0045 QFile file(item.url.toLocalFile()); 0046 if ( !file.open(QFile::ReadOnly) ) 0047 parent->error(i18n("Could not open file %1", file.fileName()), item.id); 0048 else 0049 parse(item.name_alias, item.id, file.readAll(), item.parent_url, item.url); 0050 0051 mark_resolved(); 0052 } 0053 0054 void load_data(const QueueItem& item) 0055 { 0056 auto info = item.url.path().split(";"); 0057 if ( info.empty() || !info.back().startsWith("base64,") ) 0058 parent->error(i18n("Invalid data URL"), item.id); 0059 else 0060 parse(item.name_alias, item.id, QByteArray::fromBase64(info.back().mid(7).toLatin1(), QByteArray::Base64UrlEncoding), item.parent_url, item.url); 0061 mark_resolved(); 0062 } 0063 0064 void load_item(const QueueItem& item) 0065 { 0066 if ( item.url.isLocalFile() ) 0067 load_file(item); 0068 else if ( item.url.scheme() == "data" ) 0069 load_data(item); 0070 else 0071 request(item); 0072 0073 } 0074 0075 void request(const QueueItem& item) 0076 { 0077 QNetworkRequest request(item.url); 0078 request.setMaximumRedirectsAllowed(3); 0079 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); 0080 0081 QNetworkReply* response = downloader.get(request); 0082 response->setProperty("css_url", item.parent_url); 0083 response->setProperty("id", item.id); 0084 response->setProperty("name_alias", item.name_alias); 0085 active_replies.insert(response); 0086 } 0087 0088 void mark_resolved() 0089 { 0090 resolved++; 0091 Q_EMIT parent->fonts_loaded(resolved); 0092 if ( resolved >= int(queued.size()) ) 0093 Q_EMIT parent->finished(); 0094 } 0095 0096 void handle_response(QNetworkReply *reply) 0097 { 0098 if ( reply->error() || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200 ) 0099 { 0100 auto reason = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); 0101 Q_EMIT parent->error(reason, reply->property("id").toInt()); 0102 } 0103 else 0104 { 0105 parse( 0106 reply->property("name_alias").toString(), 0107 reply->property("id").toInt(), 0108 reply->readAll(), 0109 reply->property("css").toUrl(), 0110 reply->url() 0111 ); 0112 reply->close(); 0113 } 0114 0115 active_replies.erase(reply); 0116 mark_resolved(); 0117 } 0118 0119 void parse(const QString& name_alias, int id, const QByteArray& data, const QUrl& css_url, const QUrl& reply_url) 0120 { 0121 switch ( model::CustomFontDatabase::font_data_format(data) ) 0122 { 0123 case model::FontFileFormat::TrueType: 0124 case model::FontFileFormat::OpenType: 0125 { 0126 model::CustomFont font = model::CustomFontDatabase::instance().add_font(name_alias, data); 0127 font.set_source_url(reply_url.toString()); 0128 font.set_css_url(css_url.toString()); 0129 fonts.push_back(font); 0130 if ( id != -1 ) 0131 Q_EMIT parent->success(id); 0132 break; 0133 } 0134 case model::FontFileFormat::Unknown: 0135 parse_css(id, data, reply_url); 0136 break; 0137 default: 0138 parent->error(i18n("Font format not supported for %1", reply_url.toString()), id); 0139 } 0140 } 0141 0142 void parse_css(int id, const QByteArray& data, const QUrl& css_url) 0143 { 0144 if ( !data.contains("@font-face") ) 0145 return; 0146 0147 std::vector<io::svg::detail::CssStyleBlock> blocks; 0148 io::svg::detail::CssParser parser(blocks); 0149 parser.parse(QString(data)); 0150 static QRegularExpression url(R"(url\s*\(\s*(['"]?)([^'")]+)(\1)\s*\))"); 0151 std::set<QString> urls; 0152 0153 for ( auto& block : blocks ) 0154 { 0155 if ( block.selector.at_rule() == "@font-face" ) 0156 { 0157 auto match = url.match(block.style["src"]); 0158 if ( match.hasMatch() ) 0159 { 0160 QString url = match.captured(2); 0161 if ( urls.insert(url).second ) 0162 { 0163 QString fam = block.style["font-family"]; 0164 if ( fam.size() > 1 && (fam[0] == '"' || fam[0] == '\'') ) 0165 fam = fam.mid(1, fam.size() - 2); 0166 queue(QueueItem{-1, fam, QUrl(url), css_url}); 0167 } 0168 } 0169 } 0170 } 0171 0172 0173 Q_EMIT parent->fonts_queued(queued.size()); 0174 0175 if ( id != -1 ) 0176 Q_EMIT parent->success(id); 0177 } 0178 0179 void queue(const QueueItem& item) 0180 { 0181 for ( const auto& other : queued ) 0182 if ( other.url == item.url ) 0183 return; 0184 0185 queued.push_back(item); 0186 0187 if ( loading ) 0188 { 0189 load_item(item); 0190 Q_EMIT parent->fonts_queued(queued.size()); 0191 } 0192 } 0193 }; 0194 0195 glaxnimate::gui::font::FontLoader::FontLoader() 0196 : d(std::make_unique<Private>()) 0197 { 0198 d->parent = this; 0199 d->downloader.setParent(this); 0200 connect(&d->downloader, &QNetworkAccessManager::finished, this, [this](QNetworkReply *reply){ 0201 d->handle_response(reply); 0202 }); 0203 } 0204 0205 glaxnimate::gui::font::FontLoader::~FontLoader() 0206 { 0207 } 0208 0209 void glaxnimate::gui::font::FontLoader::clear() 0210 { 0211 for ( auto reply : d->active_replies ) 0212 reply->abort(); 0213 0214 d->active_replies.clear(); 0215 d->fonts.clear(); 0216 d->resolved = 0; 0217 d->queued.clear(); 0218 d->loading = false; 0219 0220 Q_EMIT fonts_queued(0); 0221 Q_EMIT fonts_loaded(0); 0222 } 0223 0224 void glaxnimate::gui::font::FontLoader::load_queue() 0225 { 0226 d->loading = true; 0227 for ( const auto& url : d->queued ) 0228 d->load_item(url); 0229 0230 Q_EMIT fonts_queued(d->queued.size()); 0231 Q_EMIT fonts_loaded(0); 0232 } 0233 0234 void glaxnimate::gui::font::FontLoader::queue(const QString& name_alias, const QUrl& url, int id) 0235 { 0236 d->queue({id, name_alias, url}); 0237 } 0238 0239 int glaxnimate::gui::font::FontLoader::queued_total() const 0240 { 0241 return d->queued.size(); 0242 } 0243 0244 const std::vector<glaxnimate::model::CustomFont> & glaxnimate::gui::font::FontLoader::fonts() const 0245 { 0246 return d->fonts; 0247 } 0248 0249 void glaxnimate::gui::font::FontLoader::queue_data(const QString& name_alias, const QByteArray& data, int id) 0250 { 0251 d->parse(name_alias, id, data, {}, {}); 0252 } 0253 0254 0255 void glaxnimate::gui::font::FontLoader::cancel() 0256 { 0257 if ( d->loading ) 0258 { 0259 for ( const auto& reply : d->active_replies ) 0260 reply->abort(); 0261 clear(); 0262 } 0263 } 0264 0265 bool glaxnimate::gui::font::FontLoader::loading() const 0266 { 0267 return d->loading; 0268 } 0269 0270 void glaxnimate::gui::font::FontLoader::queue_pending(model::Document* document, bool reload_loaded) 0271 { 0272 connect(this, &FontLoader::success, document, &model::Document::mark_asset_loaded); 0273 0274 for ( const auto& pending : document->pending_assets() ) 0275 { 0276 if ( reload_loaded || !pending.loaded ) 0277 { 0278 if ( pending.url.isValid() ) 0279 queue(pending.name_alias, pending.url, pending.id); 0280 else if ( !pending.data.isEmpty() ) 0281 queue_data(pending.name_alias, pending.data, pending.id); 0282 } 0283 } 0284 }