/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "iv/markdown/iv_markdown_microtex.h" #include "base/base_file_utilities.h" #include "platform/qt/graphic_qt.h" #include "latex.h" #include #include #include #include #include #include #include #include #include #include #ifndef Q_OS_MAC void InitIvMarkdownMicrotexBundledResource() { Q_INIT_RESOURCE(bundled); } #endif // !Q_OS_MAC namespace Iv::Markdown { namespace { constexpr auto kBytesPerPixel = int64(4); constexpr auto kMaxFormulaImageBytes = int64(128) * 1024 * 1024; constexpr auto kFormulaForegroundRgba = 0xFFFFFFFFU; std::once_flag MicrotexInitOnce; bool MicrotexInitialized = false; QString MicrotexInitError; struct PreparedMicrotexRequest { QString trimmedTex; MathKind kind = MathKind::Display; int textSize = 0; int renderWidthCap = 0; int renderHeightCap = 0; int metricTextSize = 0; int metricRenderWidthCap = 0; int metricRenderHeightCap = 0; }; struct ParsedMicrotexFormula { std::unique_ptr render; MeasuredFormula measured; }; [[nodiscard]] QString ExceptionText(const std::exception &exception) { return QString::fromUtf8(exception.what()); } [[nodiscard]] std::wstring ToWide(const QString &value) { auto result = std::wstring(); result.reserve(size_t(value.size())); for (const auto ch : value) { result.push_back(wchar_t(ch.unicode())); } return result; } [[nodiscard]] bool PhysicalImageRejected(int64 width, int64 height) { if (width <= 0 || height <= 0) { return true; } return (width > std::numeric_limits::max()) || (height > std::numeric_limits::max()) || (width * height > (kMaxFormulaImageBytes / kBytesPerPixel)); } [[nodiscard]] bool ComputePhysicalSize( QSize logicalSize, int ratio, QSize *physicalSize) { const auto width = int64(logicalSize.width()) * ratio; const auto height = int64(logicalSize.height()) * ratio; if (PhysicalImageRejected(width, height)) { return false; } if (physicalSize) { *physicalSize = QSize(int(width), int(height)); } return true; } [[nodiscard]] QString PreparedTeX(MathKind kind, const QString &trimmedTex) { auto result = trimmedTex; if (kind == MathKind::Display) { result = u"\\displaystyle "_q + result; } return result; } [[nodiscard]] bool TooLargeFailure(const QString &error) { return (error == u"render-width-cap-exceeded"_q) || (error == u"logical-size-cap-exceeded"_q) || (error == u"physical-image-cap-exceeded"_q); } [[nodiscard]] bool ScaleMetricValue( int value, int *scaled) { if (!scaled) { return false; } const auto scaledValue = int64(value) * kFormulaExactMetricScale; if (scaledValue > std::numeric_limits::max()) { return false; } *scaled = int(scaledValue); return true; } [[nodiscard]] int RoundedLogicalMetric(int scaledValue) { return (scaledValue > 0) ? ((scaledValue + kFormulaExactMetricScale - 1) / kFormulaExactMetricScale) : 0; } [[nodiscard]] FormulaExactMetrics ExtractExactMetrics( tex::TeXRender &render) { const auto scaledSize = QSize( render.getWidth(), render.getHeight()); const auto scaledAscent = std::clamp( int(std::lround(render.getBaseline() * scaledSize.height())), 0, scaledSize.height()); const auto insets = render.getInsets(); return { .scaledSize = scaledSize, .scaledAscent = scaledAscent, .scaledInsets = QMargins( insets.left, insets.top, insets.right, insets.bottom), }; } void FillMeasuredMetrics( MeasuredFormula *measured, const FormulaExactMetrics &exact) { if (!measured) { return; } measured->exact = exact; measured->logicalSize = QSize( RoundedLogicalMetric(exact.scaledSize.width()), RoundedLogicalMetric(exact.scaledSize.height())); const auto scaledDepth = std::max( exact.scaledSize.height() - exact.scaledAscent, 0); measured->logicalDepth = std::clamp( RoundedLogicalMetric(scaledDepth), 0, measured->logicalSize.height()); } void FinalizeFailure(MeasuredFormula *result) { if (!result || result->success) { return; } const auto tooLarge = TooLargeFailure(result->error); result->overflow = tooLarge; result->tooLarge = tooLarge; } [[nodiscard]] bool PrepareRequest( const MicrotexMeasureRequest &request, PreparedMicrotexRequest *prepared, MeasuredFormula *result) { if (!prepared || !result) { return false; } if (!EnsureMicrotexInitialized(&result->error)) { return false; } if (request.textSize <= 0) { result->error = u"invalid-text-size"_q; return false; } if (request.renderWidthCap <= 0) { result->error = u"invalid-render-width"_q; return false; } if (request.renderHeightCap <= 0) { result->error = u"invalid-render-height"_q; return false; } const auto trimmedTex = request.trimmedTex.trimmed(); if (trimmedTex.isEmpty()) { result->error = u"empty-tex"_q; return false; } auto metricTextSize = 0; auto metricRenderWidthCap = 0; auto metricRenderHeightCap = 0; if (!ScaleMetricValue(request.textSize, &metricTextSize)) { result->error = u"text-size-overflow"_q; return false; } if (!ScaleMetricValue( request.renderWidthCap, &metricRenderWidthCap)) { result->error = u"render-width-overflow"_q; return false; } if (!ScaleMetricValue( request.renderHeightCap, &metricRenderHeightCap)) { result->error = u"render-height-overflow"_q; return false; } *prepared = { .trimmedTex = trimmedTex, .kind = request.kind, .textSize = request.textSize, .renderWidthCap = request.renderWidthCap, .renderHeightCap = request.renderHeightCap, .metricTextSize = metricTextSize, .metricRenderWidthCap = metricRenderWidthCap, .metricRenderHeightCap = metricRenderHeightCap, }; return true; } [[nodiscard]] ParsedMicrotexFormula ParseFormula( const PreparedMicrotexRequest &request) { auto result = ParsedMicrotexFormula(); try { auto render = std::unique_ptr(tex::LaTeX::parse( ToWide(PreparedTeX(request.kind, request.trimmedTex)), request.metricRenderWidthCap, float(request.metricTextSize), float(request.metricTextSize) * 0.25f, kFormulaForegroundRgba)); if (!render) { result.measured.error = u"parse-returned-null"_q; return result; } FillMeasuredMetrics(&result.measured, ExtractExactMetrics(*render)); const auto logicalSize = result.measured.logicalSize; if (logicalSize.width() <= 0 || logicalSize.height() <= 0) { result.measured.error = u"invalid-render-size"_q; return result; } if (logicalSize.width() > request.renderWidthCap || logicalSize.height() > request.renderHeightCap) { result.measured.error = u"logical-size-cap-exceeded"_q; result.measured.overflow = true; result.measured.tooLarge = true; return result; } result.measured.success = true; result.render = std::move(render); return result; } catch (const std::exception &exception) { result.measured.error = ExceptionText(exception); return result; } catch (...) { result.measured.error = u"unknown-exception"_q; return result; } } } // namespace bool EnsureMicrotexInitialized(QString *error) { std::call_once(MicrotexInitOnce, [] { try { #ifdef Q_OS_MAC // Use resources from the .app bundle on macOS. base::RegisterBundledResources(u"external_microtex_bundled.rcc"_q); #else // Q_OS_MAC InitIvMarkdownMicrotexBundledResource(); #endif // Q_OS_MAC tex::LaTeX::initBundled(); MicrotexInitialized = true; } catch (const std::exception &exception) { MicrotexInitError = ExceptionText(exception); } catch (...) { MicrotexInitError = u"unknown-exception"_q; } }); if (!MicrotexInitialized && error) { *error = MicrotexInitError; } else if (MicrotexInitialized && error) { error->clear(); } return MicrotexInitialized; } MeasuredFormula MeasureWithMicrotex(const MicrotexMeasureRequest &request) { auto prepared = PreparedMicrotexRequest(); auto result = MeasuredFormula(); if (!PrepareRequest(request, &prepared, &result)) { FinalizeFailure(&result); return result; } return ParseFormula(prepared).measured; } MicrotexRenderResult RenderWithMicrotex(const MicrotexRenderRequest &request) { auto result = MicrotexRenderResult(); auto prepared = PreparedMicrotexRequest(); if (!PrepareRequest( MicrotexMeasureRequest{ .trimmedTex = request.trimmedTex, .kind = request.kind, .textSize = request.textSize, .renderWidthCap = request.renderWidthCap, .renderHeightCap = request.renderHeightCap, }, &prepared, &result.measured)) { FinalizeFailure(&result.measured); return result; } if (request.devicePixelRatio <= 0) { result.measured.error = u"invalid-device-pixel-ratio"_q; return result; } auto parsed = ParseFormula(prepared); result.measured = std::move(parsed.measured); if (!result.measured.success) { return result; } auto physicalSize = QSize(); if (!ComputePhysicalSize( result.measured.logicalSize, request.devicePixelRatio, &physicalSize)) { result.measured.success = false; result.measured.error = u"physical-image-cap-exceeded"_q; result.measured.overflow = true; result.measured.tooLarge = true; return result; } auto image = QImage( physicalSize, QImage::Format_ARGB32_Premultiplied); if (image.isNull()) { result.measured.success = false; result.measured.error = u"image-allocation-failed"_q; return result; } image.setDevicePixelRatio(request.devicePixelRatio); image.fill(Qt::transparent); { QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::TextAntialiasing, true); painter.scale( 1. / double(kFormulaExactMetricScale), 1. / double(kFormulaExactMetricScale)); tex::Graphics2D_qt graphics(&painter); parsed.render->draw(graphics, 0, 0); } result.image = std::move(image); return result; } bool MicrotexBackendLinked() { return true; } } // namespace Iv::Markdown