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 }