File indexing completed on 2025-02-02 04:11:08

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 "text.hpp"
0008 
0009 #include <QTextLayout>
0010 #include <QFontInfo>
0011 #include <QMetaEnum>
0012 
0013 #include "group.hpp"
0014 #include "path.hpp"
0015 #include "command/undo_macro_guard.hpp"
0016 #include "model/assets/assets.hpp"
0017 #include "model/custom_font.hpp"
0018 #include "math/bezier/bezier_length.hpp"
0019 
0020 GLAXNIMATE_OBJECT_IMPL(glaxnimate::model::Font)
0021 GLAXNIMATE_OBJECT_IMPL(glaxnimate::model::TextShape)
0022 
0023 
0024 class glaxnimate::model::Font::Private
0025 {
0026 public:
0027     QStringList styles;
0028     QFont query;
0029     QRawFont raw;
0030     QRawFont raw_scaled;
0031     QFontMetricsF metrics;
0032 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0033     QFontDatabase database;
0034 #endif
0035 
0036     Private() :
0037         raw(QRawFont::fromFont(query)),
0038         metrics(query)
0039     {
0040 #ifdef Q_OS_ANDROID
0041         query.setPointSizeF(32);
0042 #endif
0043 //         query.setKerning(false);
0044         upscaled_raw();
0045     }
0046 
0047     void update_data()
0048     {
0049         // disable kerning because QRawFont doesn't handle kerning properly
0050 //         query.setKerning(false);
0051         raw = QRawFont::fromFont(query);
0052 
0053         // Dynamic fonts might have weird names
0054         if ( !raw.familyName().startsWith(query.family()) )
0055         {
0056             QString family = query.family();
0057             QFont new_query = query;
0058             new_query.setFamily(family + ' ' + query.styleName());
0059             auto new_raw = QRawFont::fromFont(new_query);
0060             if ( new_raw.familyName().startsWith(family) )
0061             {
0062                 query = new_query;
0063                 raw = new_raw;
0064             }
0065         }
0066 
0067         metrics = QFontMetricsF(query);
0068         upscaled_raw();
0069     }
0070 
0071     static const QStringList& default_styles()
0072     {
0073         static QStringList styles;
0074         if ( styles.empty() )
0075         {
0076             auto meta = QMetaEnum::fromType<QFont::Weight>();
0077             for ( int i = 0; i < meta.keyCount(); i++ )
0078             {
0079                 QString key = meta.key(i);
0080                 for ( const char* style : {"", " Italic", " Oblique"} )
0081                 {
0082                     styles.push_back(key + style);
0083                 }
0084             }
0085         }
0086 
0087         return styles;
0088     }
0089 
0090     void refresh_styles(Font* parent)
0091     {
0092         if ( !raw.familyName().startsWith(query.family()) )
0093         {
0094             styles = default_styles();
0095         }
0096         else
0097         {
0098 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0099             styles = database.styles(parent->family.get());
0100 #else
0101             styles = QFontDatabase::styles(parent->family.get());
0102 #endif
0103             if ( !parent->valid_style(parent->style.get()) && !styles.empty() )
0104                 parent->style.set(styles[0]);
0105         }
0106     }
0107 
0108     // QRawFont::pathForGlyph doesn't work well, so we work around it
0109     void upscaled_raw()
0110     {
0111         QFont font =  query;
0112 #ifndef Q_OS_ANDROID
0113         font.setPointSizeF(qMin(4000., font.pointSizeF() * 1000));
0114 #endif
0115         raw_scaled = QRawFont::fromFont(font);
0116     }
0117 
0118     QPainterPath path_for_glyph(quint32  glyph, bool fix_paint)
0119     {
0120         QPainterPath path = raw_scaled.pathForGlyph(glyph);
0121 
0122         if ( fix_paint )
0123             path = path.simplified();
0124 
0125         if ( raw_scaled.pixelSize() == 0 )
0126             return path;
0127 
0128         QPainterPath dest;
0129         qreal mult = raw.pixelSize() / raw_scaled.pixelSize();
0130 
0131         std::array<QPointF, 3> data;
0132         int data_i = 0;
0133         for ( int i = 0; i < path.elementCount(); i++ )
0134         {
0135             auto element = path.elementAt(i);
0136             QPointF p = QPointF(element) * mult;
0137             switch ( element.type )
0138             {
0139                 case QPainterPath::MoveToElement:
0140                     dest.moveTo(p);
0141                     break;
0142                 case QPainterPath::LineToElement:
0143                     dest.lineTo(p);
0144                     break;
0145                 case QPainterPath::CurveToElement:
0146                     data_i = 0;
0147                     data[0] = p;
0148                     break;
0149                 case QPainterPath::CurveToDataElement:
0150                     ++data_i;
0151                     data[data_i] = p;
0152                     if ( data_i == 2 )
0153                     {
0154                         dest.cubicTo(data[0], data[1], data[2]);
0155                         data_i = -1;
0156                     }
0157                     break;
0158             }
0159         }
0160 
0161         return dest;
0162     }
0163 };
0164 
0165 glaxnimate::model::Font::Font(glaxnimate::model::Document* doc)
0166     : Object(doc), d(std::make_unique<Private>())
0167 {
0168     family.set(d->raw.familyName());
0169     style.set(d->raw.styleName());
0170     size.set(d->query.pointSize());
0171     d->refresh_styles(this);
0172     on_transfer(doc);
0173 }
0174 
0175 glaxnimate::model::Font::~Font() = default;
0176 
0177 void glaxnimate::model::Font::refresh_data ( bool update_styles )
0178 {
0179     d->query = CustomFontDatabase::instance().font(family.get(), style.get(), size.get());
0180     d->update_data();
0181     if ( update_styles )
0182         d->refresh_styles(this);
0183     Q_EMIT font_changed();
0184 }
0185 
0186 void glaxnimate::model::Font::on_font_changed()
0187 {
0188     refresh_data(false);
0189 }
0190 
0191 void glaxnimate::model::Font::on_transfer ( model::Document* doc )
0192 {
0193     if ( document() )
0194         disconnect(document()->assets()->fonts.get(), nullptr, this, nullptr);
0195 
0196     if ( doc )
0197     {
0198         connect(doc->assets()->fonts.get(), &FontList::font_added, this, [this]{
0199             refresh_data(true);
0200             document()->graphics_invalidated();
0201         });
0202     }
0203 }
0204 
0205 
0206 void glaxnimate::model::Font::on_family_changed()
0207 {
0208     refresh_data(true);
0209 }
0210 
0211 bool glaxnimate::model::Font::valid_style(const QString& style)
0212 {
0213     return d->styles.contains(style);
0214 }
0215 
0216 const QFont & glaxnimate::model::Font::query() const
0217 {
0218     return d->query;
0219 }
0220 
0221 const QRawFont & glaxnimate::model::Font::raw_font() const
0222 {
0223     return d->raw;
0224 }
0225 
0226 QStringList glaxnimate::model::Font::styles() const
0227 {
0228     return d->styles;
0229 }
0230 
0231 const QFontMetricsF & glaxnimate::model::Font::metrics() const
0232 {
0233     return d->metrics;
0234 }
0235 
0236 QString glaxnimate::model::Font::type_name_human() const
0237 {
0238     return i18n("Font");
0239 }
0240 
0241 QPainterPath glaxnimate::model::Font::path_for_glyph(quint32 glyph, glaxnimate::model::Font::CharDataCache& cache, bool fix_paint) const
0242 {
0243     auto it = cache.find(glyph);
0244 
0245     if ( it != cache.end() )
0246         return it->second;
0247 
0248     QPainterPath path = d->path_for_glyph(glyph, fix_paint);
0249     cache.emplace(glyph, path);
0250     return path;
0251 }
0252 
0253 void glaxnimate::model::Font::from_qfont(const QFont& f)
0254 {
0255     command::UndoMacroGuard g(i18n("Change Font"), document());
0256     QFontInfo finfo(f);
0257     family.set_undoable(finfo.family());
0258     style.set_undoable(finfo.styleName());
0259     size.set_undoable(f.pointSizeF());
0260 }
0261 
0262 
0263 
0264 glaxnimate::model::Font::ParagraphData glaxnimate::model::Font::layout(const QString& text) const
0265 {
0266     glaxnimate::model::Font::ParagraphData para_data;
0267 
0268     auto lines = text.split('\n');
0269     QTextLayout layout(text, d->query, nullptr);
0270 
0271     QTextOption option;
0272     option.setUseDesignMetrics(true);
0273     layout.setTextOption(option);
0274     layout.beginLayout();
0275     for ( const auto& line_size : lines )
0276     {
0277         QTextLine line = layout.createLine();
0278         if ( !line.isValid() )
0279             break;
0280         line.setNumColumns(line_size.size());
0281         line.setLeadingIncluded(true);
0282     }
0283     layout.endLayout();
0284 
0285     qreal line_y = 0;
0286     qreal yoff = -d->metrics.ascent();
0287 
0288     for ( int ln = 0; ln < layout.lineCount(); ln++ )
0289     {
0290         QTextLine line = layout.lineAt(ln);
0291 
0292         auto& line_data = para_data.emplace_back();
0293         line_data.baseline = QPointF(0, line_y);
0294         line_data.bounds = line.rect();
0295         line_data.text = lines[ln];
0296 
0297         QPointF baseline(0, line_y + yoff);
0298         for ( const auto& run : line.glyphRuns() )
0299         {
0300             auto glyphs = run.glyphIndexes();
0301             line_data.glyphs.reserve(line_data.glyphs.size() + glyphs.size());
0302             auto positions = run.positions();
0303             for ( int i = 0; i < glyphs.size(); i++ )
0304             {
0305                 line_data.glyphs.push_back({
0306                     glyphs[i],
0307                     positions[i] + baseline
0308                 });
0309             }
0310         }
0311 
0312         line_data.advance = QPointF(line.cursorToX(lines[ln].size()), 0);
0313 
0314         line_y += line_spacing();
0315     }
0316 
0317     // QRawFont way: for some reason it ignores KernedAdvances
0318     /*
0319     qreal line_y = 0;
0320     for ( const auto& line : text.split('\n') )
0321     {
0322         auto glyphs = d->raw.glyphIndexesForString(line);
0323         auto advances = d->raw.advancesForGlyphIndexes(glyphs, QRawFont::UseDesignMetrics|QRawFont::KernedAdvances);
0324 
0325         auto& line_data = para_data.emplace_back();
0326         line_data.glyphs.reserve(glyphs.size());
0327         line_data.text = line;
0328 
0329         line_data.baseline = line_data.advance = QPointF(0, line_y);
0330         for ( int i = 0; i < glyphs.size(); i++ )
0331         {
0332             line_data.glyphs.push_back({
0333                 glyphs[i],
0334                 line_data.advance,
0335             });
0336             line_data.advance += advances[i];
0337         }
0338 
0339         line_y += line_spacing();
0340     }
0341     */
0342     return para_data;
0343 }
0344 
0345 qreal glaxnimate::model::Font::line_spacing() const
0346 {
0347     // for some reason QTextLayout ignores leading()
0348     return line_spacing_unscaled() * line_height.get();
0349 }
0350 
0351 qreal glaxnimate::model::Font::line_spacing_unscaled() const
0352 {
0353     // for some reason QTextLayout ignores leading()
0354     return d->metrics.ascent() + d->metrics.descent();
0355 }
0356 
0357 
0358 QStringList glaxnimate::model::Font::families() const
0359 {
0360 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0361     return d->database.families();
0362 #else
0363     return QFontDatabase::families();
0364 #endif
0365 }
0366 
0367 QList<int> glaxnimate::model::Font::standard_sizes() const
0368 {
0369     auto list = QFontDatabase::standardSizes();
0370     int actual = d->query.pointSize();
0371     auto it = std::upper_bound(list.begin(), list.end(), actual);
0372     if ( it == list.begin() || *(it-1) != actual )
0373         list.insert(it, actual);
0374     return list;
0375 }
0376 
0377 glaxnimate::model::TextShape::TextShape(glaxnimate::model::Document* document)
0378     : ShapeElement(document)
0379 {
0380     connect(font.get(), &Font::font_changed, this, &TextShape::on_font_changed);
0381 }
0382 
0383 void glaxnimate::model::TextShape::on_text_changed()
0384 {
0385 #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
0386     shape_cache.clear();
0387 #else
0388     shape_cache = QPainterPath();
0389 #endif
0390     propagate_bounding_rect_changed();
0391 }
0392 
0393 void glaxnimate::model::TextShape::on_font_changed()
0394 {
0395     cache.clear();
0396     on_text_changed();
0397 }
0398 
0399 const QPainterPath & glaxnimate::model::TextShape::untranslated_path(FrameTime t) const
0400 {
0401     if ( shape_cache.isEmpty() )
0402     {
0403         if ( path.get() )
0404         {
0405             QString txt = text.get();
0406             txt.replace('\n', ' ');
0407             auto bezier = path->shapes(t);
0408             const int length_steps = 5;
0409 
0410             math::bezier::LengthData length_data(bezier, length_steps);
0411             for ( const auto& line : font->layout(txt) )
0412             {
0413                 for ( const auto& glyph : line.glyphs )
0414                 {
0415                     qreal x = path_offset.get_at(t) + glyph.position.x();
0416                     if ( x > length_data.length() || x < 0 )
0417                         continue;
0418 
0419                     auto glyph_shape = font->path_for_glyph(glyph.glyph, cache, true);
0420                     auto glyph_rect = glyph_shape.boundingRect();
0421 
0422                     auto start1 = length_data.at_length(x);
0423                     auto start2 = start1.descend();
0424                     auto start_p = bezier.beziers()[start1.index].split_segment_point(start2.index, start2.ratio);
0425 
0426                     auto end1 = length_data.at_length(x + glyph_rect.width());
0427                     auto end2 = end1.descend();
0428                     auto end_p = bezier.beziers()[end1.index].split_segment_point(end2.index, end2.ratio);
0429 
0430                     QTransform mat;
0431                     mat.translate(start_p.pos.x(), start_p.pos.y());
0432                     mat.rotate(qRadiansToDegrees(math::atan2(end_p.pos.y() - start_p.pos.y(), end_p.pos.x() - start_p.pos.x())));
0433                     shape_cache += mat.map(glyph_shape);
0434                 }
0435             }
0436         }
0437         else
0438         {
0439             for ( const auto& line : font->layout(text.get()) )
0440                 for ( const auto& glyph : line.glyphs )
0441                     shape_cache += font->path_for_glyph(glyph.glyph, cache, true).translated(glyph.position);
0442         }
0443     }
0444 
0445     return shape_cache;
0446 }
0447 
0448 
0449 void glaxnimate::model::TextShape::add_shapes(glaxnimate::model::FrameTime t, math::bezier::MultiBezier& bez, const QTransform& transform) const
0450 {
0451     if ( !transform.isIdentity() )
0452     {
0453         auto mb = math::bezier::MultiBezier::from_painter_path(shape_data(t));
0454         mb.transform(transform);
0455         bez.append(mb);
0456     }
0457     else
0458     {
0459         bez.append(shape_data(t));
0460     }
0461 }
0462 
0463 QPainterPath glaxnimate::model::TextShape::to_painter_path_impl(glaxnimate::model::FrameTime) const
0464 {
0465     return {};
0466 }
0467 
0468 QPainterPath glaxnimate::model::TextShape::shape_data(FrameTime t) const
0469 {
0470     // Ignore position if we have a path, it can still be moved from the group
0471     if ( path.get() )
0472         return untranslated_path(t);
0473     QPointF pos = position.get_at(t);
0474     return untranslated_path(t).translated(pos);
0475 }
0476 
0477 QIcon glaxnimate::model::TextShape::tree_icon() const
0478 {
0479     return QIcon::fromTheme("font");
0480 }
0481 
0482 QRectF glaxnimate::model::TextShape::local_bounding_rect(glaxnimate::model::FrameTime t) const
0483 {
0484     return shape_data(t).boundingRect();
0485 }
0486 
0487 QString glaxnimate::model::TextShape::type_name_human() const
0488 {
0489     return i18n("Text");
0490 }
0491 
0492 std::unique_ptr<glaxnimate::model::ShapeElement> glaxnimate::model::TextShape::to_path() const
0493 {
0494     auto group = std::make_unique<glaxnimate::model::Group>(document());
0495     group->name.set(name.get());
0496     group->group_color.set(group_color.get());
0497     group->visible.set(visible.get());
0498 
0499     Font::CharDataCache local_cache;
0500 
0501     for ( const auto& line : font->layout(text.get()) )
0502     {
0503         auto line_group = std::make_unique<glaxnimate::model::Group>(document());
0504         line_group->name.set(line.text);
0505 
0506         for ( const auto& glyph : line.glyphs )
0507         {
0508             QPainterPath p = font->path_for_glyph(glyph.glyph, local_cache, false).translated(glyph.position);
0509             math::bezier::MultiBezier bez;
0510             bez.append(p);
0511 
0512             if ( bez.beziers().size() == 1 )
0513             {
0514                 auto path = std::make_unique<glaxnimate::model::Path>(document());
0515                 path->shape.set(bez.beziers()[0]);
0516                 line_group->shapes.insert(std::move(path), 0);
0517             }
0518             else if ( bez.beziers().size() > 1 )
0519             {
0520                 auto glyph_group = std::make_unique<glaxnimate::model::Group>(document());
0521                 for ( const auto& sub : bez.beziers() )
0522                 {
0523                     auto path = std::make_unique<glaxnimate::model::Path>(document());
0524                     path->shape.set(sub);
0525                     glyph_group->shapes.insert(std::move(path), 0);
0526                 }
0527                 line_group->shapes.insert(std::move(glyph_group), 0);
0528             }
0529         }
0530 
0531         group->shapes.insert(std::move(line_group), 0);
0532     }
0533 
0534     group->set_time(time());
0535     if ( position.animated() )
0536     {
0537         for ( const auto& kf : position )
0538         {
0539             group->transform->position.set_keyframe(kf.time(), kf.get())->set_transition(kf.transition());
0540 //             group->transform->anchor_point.set_keyframe(kf.time(), kf.get())->set_transition(kf.transition());
0541         }
0542     }
0543 
0544     group->transform->position.set(position.get());
0545 //     group->transform->anchor_point.set(position.get());
0546 
0547     return group;
0548 }
0549 
0550 QPointF glaxnimate::model::TextShape::offset_to_next_character() const
0551 {
0552     auto layout = font->layout(text.get());
0553     if ( layout.empty() )
0554         return {};
0555     return layout.back().advance;
0556 }
0557 
0558 std::vector<glaxnimate::model::DocumentNode *> glaxnimate::model::TextShape::valid_paths() const
0559 {
0560     std::vector<glaxnimate::model::DocumentNode *> shapes;
0561     shapes.push_back(nullptr);
0562 
0563     for ( const auto& sib : *owner() )
0564         if ( sib.get() != this )
0565             shapes.push_back(sib.get());
0566 
0567     return shapes;
0568 }
0569 
0570 bool glaxnimate::model::TextShape::is_valid_path(glaxnimate::model::DocumentNode* node) const
0571 {
0572     if ( node == nullptr )
0573         return true;
0574 
0575     if ( node == this )
0576         return false;
0577 
0578     if ( auto shape = node->cast<glaxnimate::model::ShapeElement>() )
0579         return shape->owner_composition() == owner_composition();
0580 
0581     return false;
0582 }
0583 
0584 void glaxnimate::model::TextShape::path_changed(glaxnimate::model::ShapeElement* new_path, glaxnimate::model::ShapeElement* old_path)
0585 {
0586     on_text_changed();
0587 
0588     if ( old_path )
0589         disconnect(old_path, nullptr, this, nullptr);
0590 
0591     if ( new_path )
0592     {
0593         connect(new_path, &Object::visual_property_changed, this, &TextShape::on_text_changed);
0594         connect(new_path, &VisualNode::bounding_rect_changed, this, &TextShape::on_text_changed);
0595     }
0596 }
0597