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