#include "mathtext.hpp" #include "latinmodern-fonts.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace ZShell::llm { namespace { constexpr int kRenderMargin = 2; constexpr unsigned int kResolutionDpi = 96; constexpr int kCacheLimit = 512; struct LatinModern { bool roman = false; bool math = false; }; const LatinModern& loadLatinModern() { static const LatinModern fonts = [] { LatinModern result; const auto add = [](const unsigned char* data, size_t size, const QString& fileName, const QString& family) { const QString path = QDir::tempPath() + QLatin1Char('/') + fileName; { QFile f(path); if (!f.open(QIODevice::WriteOnly) || f.write( reinterpret_cast(data), static_cast(size)) != static_cast(size)) return false; } const int key = QFontDatabase::addApplicationFont(path); if (key < 0) return false; return QFontDatabase::applicationFontFamilies(key).contains(family); }; result.roman = add(lmfont::lmroman10_regular, sizeof(lmfont::lmroman10_regular), QStringLiteral("lmroman10-regular.otf"), QStringLiteral("LMRoman10")) && add(lmfont::lmroman10_italic, sizeof(lmfont::lmroman10_italic), QStringLiteral("lmroman10-italic.otf"), QStringLiteral("LMRoman10")) && add(lmfont::lmroman10_bold, sizeof(lmfont::lmroman10_bold), QStringLiteral("lmroman10-bold.otf"), QStringLiteral("LMRoman10")) && add(lmfont::lmroman10_bolditalic, sizeof(lmfont::lmroman10_bolditalic), QStringLiteral("lmroman10-bolditalic.otf"), QStringLiteral("LMRoman10")); result.math = add(lmfont::latinmodern_math, sizeof(lmfont::latinmodern_math), QStringLiteral("latinmodern-math.otf"), QStringLiteral("Latin Modern Math")); return result; }(); return fonts; } void configureLatinModern(JKQTMathText& renderer) { const LatinModern& fonts = loadLatinModern(); if (fonts.roman) renderer.setFontRomanAndMath( QStringLiteral("LMRoman10"), JKQTMathTextFontEncoding::MTFEUnicode); if (fonts.math) { renderer.setFontMathRoman( QStringLiteral("Latin Modern Math"), JKQTMathTextFontEncoding::MTFEUnicode); renderer.setFallbackFontSymbols( QStringLiteral("Latin Modern Math"), JKQTMathTextFontEncoding::MTFEUnicode); } } } // namespace LlmMathText::LlmMathText(QObject* parent) : QObject(parent) { loadLatinModern(); } void LlmMathText::setLatex(const QString& value) { if (m_latex == value) return; m_latex = value; reRender(); } void LlmMathText::setColor(const QColor& value) { if (m_color == value) return; m_color = value; reRender(); } void LlmMathText::setFontPointSize(double value) { if (qFuzzyCompare(m_fontPointSize, value)) return; m_fontPointSize = value; reRender(); } void LlmMathText::setDevicePixelRatio(qreal value) { if (qFuzzyCompare(m_devicePixelRatio, value)) return; m_devicePixelRatio = value; reRender(); } namespace { struct MathRender { bool ok = false; QImage image; QString id; qreal width = 0; qreal height = 0; }; struct MathStore { QMutex mutex; QHash entries; QStringList order; QHash idIndex; QHash idRefs; qint64 bytes = 0; quint64 nextId = 1; }; MathStore& mathStore() { static MathStore store; return store; } qint64 imageBytes(const QImage& image) { return image.isNull() ? 0 : static_cast(image.sizeInBytes()); } void retainMathId(const QString& id) { if (id.isEmpty()) return; auto& store = mathStore(); QMutexLocker locker(&store.mutex); ++store.idRefs[id]; } void releaseMathId(const QString& id) { if (id.isEmpty()) return; auto& store = mathStore(); QMutexLocker locker(&store.mutex); auto it = store.idRefs.find(id); if (it == store.idRefs.end()) return; if (--it.value() <= 0) store.idRefs.erase(it); } } // namespace namespace { constexpr qint64 kMaxCacheBytes = 64 * 1024 * 1024; class MathImageProvider : public QQuickImageProvider { public: MathImageProvider() : QQuickImageProvider(QQuickImageProvider::Image) {} QImage requestImage( const QString& id, QSize* size, const QSize& requestedSize) override { Q_UNUSED(requestedSize) auto& store = mathStore(); QMutexLocker locker(&store.mutex); const auto keyIt = store.idIndex.constFind(id); if (keyIt == store.idIndex.constEnd()) return {}; const QString key = keyIt.value(); const auto entryIt = store.entries.constFind(key); if (entryIt == store.entries.constEnd()) return {}; if (size) *size = entryIt->image.size(); const qsizetype pos = store.order.indexOf(key); if (pos >= 0) store.order.move(pos, store.order.size() - 1); return entryIt->image; } }; void ensureImageProvider(QObject* owner) { if (auto* engine = qmlEngine(owner)) if (!engine->imageProvider(QStringLiteral("llmmath"))) engine->addImageProvider( QStringLiteral("llmmath"), new MathImageProvider()); } } // namespace void LlmMathText::reRender() { ensureImageProvider(this); ++m_requestId; if (m_latex.trimmed().isEmpty()) { releaseMathId(m_imageId); m_imageId.clear(); m_width = 0; m_height = 0; m_ok = false; Q_EMIT changed(); return; } const QString key = m_latex + QLatin1Char(0x1f) + m_color.name() + QLatin1Char(0x1f) + QString::number(m_fontPointSize) + QLatin1Char(0x1f) + QString::number(m_devicePixelRatio); bool cacheHit = false; QString hitId; qreal hitW = 0; qreal hitH = 0; bool hitOk = false; { auto& store = mathStore(); QMutexLocker locker(&store.mutex); const auto it = store.entries.constFind(key); if (it != store.entries.constEnd()) { cacheHit = true; hitId = it->id; hitW = it->width; hitH = it->height; hitOk = it->ok; } } if (cacheHit) { releaseMathId(m_imageId); retainMathId(hitId); m_imageId = hitId; m_width = hitW; m_height = hitH; m_ok = hitOk; Q_EMIT changed(); return; } if (m_inFlight) return; m_inFlight = true; const QString latex = m_latex; const QColor color = m_color; const double pointSize = m_fontPointSize; const qreal dpr = m_devicePixelRatio; QThreadPool::globalInstance()->start( [this, id = m_requestId, key, latex, color, pointSize, dpr]() { JKQTMathText renderer(nullptr, true); configureLatinModern(renderer); MathRender render; renderer.setFontPointSize(pointSize); renderer.setFontColor(color); if (renderer.parse( latex, JKQTMathText::LatexParser, JKQTMathText::DefaultParseOptions)) { { QImage scratch(1, 1, QImage::Format_ARGB32); QPainter painter(&scratch); const QSizeF size = renderer.getSize(painter); render.width = size.width() + 2 * kRenderMargin; render.height = size.height() + 2 * kRenderMargin; } QPointer sizeGuard(this); const qreal sizeW = render.width; const qreal sizeH = render.height; QMetaObject::invokeMethod( QCoreApplication::instance(), [sizeGuard, id, sizeW, sizeH]() { LlmMathText* self = sizeGuard; if (!self || id != self->m_requestId) return; self->m_width = sizeW; self->m_height = sizeH; Q_EMIT self->changed(); }, Qt::QueuedConnection); const QImage image = renderer.drawIntoImage( false, QColor(Qt::transparent), kRenderMargin, dpr, kResolutionDpi); if (!image.isNull()) { render.image = image; render.width = image.width() / dpr; render.height = image.height() / dpr; render.ok = true; } } QPointer guard(this); QMetaObject::invokeMethod( QCoreApplication::instance(), [guard, id, key, render = std::move(render)]() mutable { LlmMathText* self = guard; if (!self) return; self->m_inFlight = false; if (id != self->m_requestId) { self->reRender(); return; } if (render.ok) { auto& store = mathStore(); QMutexLocker locker(&store.mutex); const bool isNew = !store.entries.contains(key); if (isNew) render.id = QString::number(store.nextId++); else render.id = store.entries[key].id; store.idIndex.insert(render.id, key); if (isNew) { const qint64 newBytes = imageBytes(render.image); while (store.order.size() >= kCacheLimit || store.bytes + newBytes > kMaxCacheBytes) { bool evicted = false; for (int i = 0; i < store.order.size(); ++i) { const QString oldKey = store.order.at(i); const auto oldIt = store.entries.constFind(oldKey); if (oldIt == store.entries.constEnd()) { store.order.removeAt(i); continue; } if (store.idRefs.value(oldIt->id, 0) > 0) continue; store.order.removeAt(i); store.bytes -= imageBytes(oldIt->image); store.idIndex.remove(oldIt->id); store.entries.remove(oldKey); evicted = true; break; } if (!evicted) break; } store.entries.insert(key, render); store.order.append(key); store.bytes += imageBytes(render.image); } } releaseMathId(self->m_imageId); retainMathId(render.id); self->m_imageId = render.id; self->m_width = render.width; self->m_height = render.height; self->m_ok = render.ok; Q_EMIT self->changed(); }, Qt::QueuedConnection); }); } LlmMathText::~LlmMathText() { releaseMathId(m_imageId); } } // namespace ZShell::llm