company_banner

TeX в SVG: опенсорс-решение в помощь веб-разработчикам образовательных проектов

    Привет! Меня зовут Костя Мамаев, я занимаюсь фронтенд-разработкой в поиске Яндекса. Некоторое время назад мы вместе с другими ребятами из команды помогали образовательным проектам компании. Среди прочего пришлось решить, казалось бы, простую задачку: отображать на экране и распечатывать на бумаге формулы, закодированные в популярном формате TeX. Звучит, как дело пяти минут, но в результате трёх подходов к снаряду появился полноценный микросервис для серверного рендеринга формул в svg и png. В статье расскажу, зачем мы пошли этим путём и почему ни один из существующих проектов не подошёл «из коробки».

    Результаты нашей работы могут быть полезны и другим разработчикам, помогающим школьникам и учителям, поэтому готовый микросервис ждёт вас на гитхабе Яндекса. По ссылке весь джентльменский набор: Docker-контейнер, документация, открытый код.


    В математических, физических и химических задачах (например, в материалах для подготовки к ЕГЭ и ОГЭ) много формул. Мы хотели, чтобы работать с ними было удобно. Качество отображения должно быть высоким, а формат — универсальным: мог выразить любую формулу, был простым в хранении и понятным за пределами Яндекса. Ещё важно, чтобы формулы выглядели одинаково и не «ехали» на разных экранах, а их обработка не перегружала CPU и расходовала энергию экономично — это особенно актуально для мобильных устройств.

    Подход первый: фронтендерский. KaTeX

    Показывать сырую TeX-разметку опрометчиво — вряд ли её сможет прочесть пользователь без кандидатской степени, поэтому сначала нужно её приготовить. Первое решение, которое приходит на ум — конвертировать на стороне клиента, будь то что-то встроенное или библиотека для рендеринга TeX.

    Немногие вспомнят, что в браузерах уже есть поддержка математических формул — MathML. Но, как показало беглое исследование, формат давно забыт и пользователями, и вендорами браузеров. Пришлось двигаться дальше. Оказалось, выбор инструментов не так уж велик: монструозный, но всеядный и расширяемый MathJaX или относительно молодой и быстрый KaTeX. Как говорится, из двух зол — выбирай меньшее. Решили попробовать KaTeX.

    Плюсы:

    • Realtime-отрисовка формул. Пользователь пишет формулу и сразу видит, что будет на выходе.

    • Формирование разметки на клиентской стороне — не нужно ничего хранить.

    Минусы:

    • Каким бы легковесным ни был KaTeX по сравнению MathJaX, он всё равно оставался слишком большим по стандартам Яндекса.

    • Артефакты realtime-генерации: мигание на странице и долгий рендеринг, особенно на мобильных устройствах.

    • Проблемы при печати (о них поговорим чуть позже).

    • KaTeX отдаёт html-разметку, так что скопировать формулу как формулу не получится.

    • Двойная работа при использовании SSR. Сначала генерируем html на сервере, а потом на клиенте.

    Подход второй: браузерный. Puppeteer

    Результат, полученный с KaTeX, не устроил — хотелось избавиться от недостатков первой реализации. Пришла идея сделать всё на сервере без realtime-генерации формул. В этом случае можно выбрать более мощную библиотеку — MathJaX. Она умеет «из коробки» превращать TeX в разметку, MathML и svg. Здорово, не правда ли? Оставалось написать обвязку в виде сервера, парочки web-хуков для добавления заданий на обработку и сложить всё в s3. Это оказалось не так просто. Возникли проблемы с печатью:

    • выяснилось, что svg не копируется через ctrl+c ctrl+v в Microsoft Word,

    • ломалось выравнивание формул в тексте.

    Мы много общались с учителями, репетиторами, готовящими школьников к ЕГЭ и ОГЭ, и преподавателями вузов. Большинство из них распечатывает на бумаге материалы к занятиям. Это подтверждали интервью, проведённые коллегами: все респонденты-репетиторы рассказывали, что печатают задания для учеников. Значит, закрывать глаза на эти проблемы нельзя.

    Здесь пригодился Puppeteer. Мы добавили конвертацию из svg в png, чтобы реализовать копирование в Word. А это, стоит заметить, один из самых востребованных сценариев среди преподавателей. Но, как только появился браузер, сервис стал тормозить: генерация одной формулы занимала секунды, а пересоздание всех формул (у нас их было 110 469) — дни или даже недели.

    Подход третий: серверный. MathJax, sharp и немного смекалки

    Чтобы получить решение, которое работает быстро, не нагружает даже слабенькие устройства и даёт приемлемое качество изображения, оставалось решить две проблемы:

    • ускорить генерацию png,

    • починить выравнивание формул.

    Первая проблема решалась просто, хотя пришлось перепробовать множество вариантов, чтобы получить нужный результат. Puppeteer был безжалостно выпилен, а вместо него — добавлена маленькая и быстрая библиотека sharp. Да, решение породило внешнюю зависимость, но сервис запускается в Docker-контейнере, так что это было приемлемо. Скорость генерации заметно выросла.

    А вот с «поехавшими» формулами в тексте пришлось побороться. Было предположение, что выравнивание по центру или базовой линии поможет, но оно работало хорошо только для однострочных или идеально симметричных формул. MathJax сам генерирует правильные отступы в svg-файлах (правда, в ex, но это не беда), но для оптимизации отрисовки выносит одинаковые символы в defs. Они конфликтуют между вставленными svg, и выглядит это, будто сломалась головка струйного принтера. Дополнительный минус — раздутая разметка и невозможность повторного использования одинаковых svg на странице. Обращение к png выглядело жестом отчаяния: проблемы примерно те же, а качество графики хуже.

    Решение оказалось достаточно простым:

    1. генерируем svg-файл,

    2. получаем из него атрибуты со стилями (это же обычный xml, не так ли?),

    3. удаляем атрибуты,

    4. генерируем png из svg,

    5. сохраняем в метаинформации файла данные о стилях, нужных для корректного отображения картинки.

    Готово!

    Результаты и выводы

    Теперь формулы копируются, масштабируются, лениво загружаются, идеально выровнены, а CPU пользователя не нагревается до состояния сковородки. Удалось добиться скоростных улучшений. Размер десктопных и тачевых бандлов уменьшился на 64.5кб (37%).

    Обработка страницы варианта ЕГЭ, на которой нужно было отрендерить 94 формулы:

    • с использованием Katex: 7594 ms

    • без Katex: 6584 ms

    • финальный вариант: 1010 ms

    Эта же страница, но на слабом CPU и с медленным 3G:

    • с использованием Katex: 40229 ms

    • без Katex: 37524 ms

    • финальный вариант: 2705 ms

    Чему я научился?

    • Даже самая тривиальная задача может оказаться сложнее, чем представлялось вначале.

    • Svg не копируется через буфер обмена.

    • Конвертировать svg в png с помощью браузера — это долго.

    • Не стоит забывать о метаинформации. 

    Надеюсь, этот проект принесёт пользу и другим разработчикам, помогающим школьникам, студентам и учителям. Если вы один из них — встретимся на гитхабе.

    Яндекс
    Как мы делаем Яндекс

    Comments 23

      0

      Я думал, mathjax победил.


      Вот например, и HTML и PDF: https://pashev.me/posts/texas

        +2
        В финале так и есть, но не на клиенте, а на сервере. На клиенте это, увы, дорого. Про pdf интереснее, по ссылке которую ты указан, не совсем понятно как она генерируется.
        +1
        Спасибо за тестирование на слабых компьютерах!
          0
          Но, как только появился браузер, сервис стал тормозить: генерация одной формулы занимала секунды

          Именно генерация формулы, а не запуск Puppeteer? А то как раз на это секунды требуются.


          Сам недавно для сохранения в pdf поднял express сервер с мини-очередью — чтобы держать браузер запущенным, с одной вкладкой — убрал оверхед ~3.5 секунды. Бонусом получил кэширование и отсутствие параллельно запущенных нод

            0
            но это не всегда то, что хочется увидеть. Хочется параллельно, масштабируемо и чтобы без оверхеда по памяти.
            Решение с браузером с твоей конфигурацией имеет ограничение по пропускной способности одна вкладка и один браузер. Можно конечно поднять несколько нод с браузерами внутри, но это будет очень дорого по памяти. Плюсом идет оверхед по холодному запуску браузера, при поднятии новой ноды с браузером.

            p.s. кстати если тебя интересует рендеринг в pdf, то рекомендую посмотреть в сторону wkhtmltopdf. Но пусть и старый, но работает сильно быстрее. У нас есть отдельное решение для этого, но это тема отдельного поста :)
              0

              Для меня ограничение в одну вкладку было необходимо. Для вашей же задачи, возможно, вполне подойдёт ограничение в N вкладок, которые смогут параллельно обрабатывать очередь, размер N будет зависеть только от конфигурации железа. Масштабировать можно будет и горизонтально — через балансировщик.


              А насчёт wkhtml, mpdf, phantomjs — спасибо, но нет, если требуется что-то большее, чем "картинка + текст хоть как-нибудь", я лучше заверстаю нормально для современного браузера — со всеми его плюшками и любыми фронтенд-либами — и получу 1-в-1 то, что вижу при ctrl+p -> print to pdf

                0

                Ты прав, все зависит от того что именно тебе необходимо и какие есть требования по скорости. Кстати был неплохой доклад на RND.JS 8 от Виталия Слободина на эту тему, возможно, уже есть какие-нибудь классные результаты ;)

            0
            Можно ещё было глянуть как это делается в MediaWiki (движке Википедии) — там же тоже можно вставлять формулы на TeX'е и показываются они как рисунки.
              +3

              Да и мы сделали это :) Решение от Wikimedia использует внутри себя PhantomJs, что было слишком медленно для нас. Но некоторые решения там интересные, при желании можно посмотреть, вот репозиторий

              0

              Недостаёт картинок отрисовки формул «до» и «после». Что значит «сломалось выравнивание»? И не все знают, как выглядят артефакты при «сломе головки струйного принтера».

                +2

                Резонно :)
                Выравнивание ломалось примерно так:
                поломанное выравнивание
                Про «слом головки струйного принтера», увы, не удалось найти оригинальной иллюстрации, но основной смысл в том, что svg'ка разлеталась в разные стороны из-за коллизий названий symbols разных формул. И, естественно, чем больше формул на странице, тем вероятнее коллизии.

                0

                Когда мне нужно было решить подобную задачу (преобразовать LaTeX-формулу в SVG), то я воспользовался библиотекой, которую сейчас мейнтейню — WPF-Math.


                Причём функция, генерирующая SVG, у нас там получилась вообще в качестве одного из случайных побочных результатов разработки.

                  +1
                  Формулы в png умеет делать Гугл. Вот пример:
                  http://chart.apis.google.com/chart?cht=tx&chl=\displaystyle \int_{-\infty}^{\infty}e^{-x^{2}} \;dx={\pi}

                  Кстати, html с такими формулами «открывается вордом».

                  Генерация формул на стороне сервера в swg и png уже реализована тут http://i.upmath.me/. Там открытый исходный код и всё такое.

                  Автор этого есть на Хабре: parpalak.

                    0

                    Выглядит здорово, но я не нашел репозиторий, чтобы это можно было использовать у себя и накрутить что-то свое, типа кэширования, предпроцессинга и прочих штук.


                    Если проект действительно опенсоурсный, то было бы интересно как это работает там.

                      0
                      Спросите у автора, там емайл есть внизу страницы i.upmath.me.
                      Или тут tex.s2cms.ru
                      Или тут written.ru
                      или тут written.ru/articles/technologies/site_building/latex_for_web
                        +1
                          0

                          Действительно интересное решение.
                          Есть несколько моментов которые необходимо проверить и один который неприменим в Яндексе :)


                          Надо проверить:


                          1. Надо проверить насколько это будет быстро, потому что если я правильно понял, то это решение напрямую обращается к TexLive, потом преобразует из DVI в SVG и так далее.
                          2. Понять какое есть точки расширения, к примеру для добавления возможности добавить доступность формуле

                          Неприменимо для нас:


                          1. Это поддержка inline формул. Да, это решение поддерживает такие формулы, но только для современных браузеров с поддержкой css переменных как минимум.
                            0

                            Да, обращается напрямую к TeX Live.


                            Про css-переменные, вы имеете в виду последний коммит? Он относится только к js-скрипту, который подменяет разметку формул в html-коде страниц через двойные доллары на сами картинки. Фича нужна только для небольшого масштабирования формул, чтобы размер шрифта на картинке соответствовал размеру шрифта на странице.


                            Серверная часть, которая генерирует svg- и png-картинки, такие как эта, работает без каких-либо css-переменных.

                              0

                              Всё так, но MathJax, в свою очередь, имеет еще и выравнивать формулы относительно baseline текста, что как никак кстати подходит для inline-формул. Если я правильно понял в Вашем решении выравнивание работает тоже через css переменные.


                              Плюс заметил артефакты в IE 11, что для нас очень важно.

                                0

                                Для выравнивания задается vertical-align. Переменные тут не нужны. Svg-картинки знают о положении базовой линии. Раньше я использовал встраивание картинок через object и передачу информации о базовой линии через postMessage. Потом переделал на fetch, потому что вставка через object стала тормозить в Хроме. Fetch не поддерживается в IE11, но старая реализация работает в IE11, для кроссбраузерности можно использовать ее: https://github.com/parpalak/i.upmath.me/commit/ec56935f904caffdcba11509c3353fc24881a2a6


                                Или пойти по истории вглубь, старые версии скрипта работают и на IE8.


                                Выше уже приводили, кстати, ссылку на описание того, как это всё работает: https://written.ru/articles/technologies/site_building/latex_for_web

                      0

                      Сталкивался с подобной задачей.


                      • Попробовал KaTeX. Синхронный и очень быстрый. Всё замечательно, но мало что умеет. Пришлось сразу отказаться. ЕМНИП оно только в HTML умеет. Но могу ошибаться, это было пару лет назад. Мне нужен был SVG
                      • Попробовал MathJax. Ужасное API, отсутствие поддержки es6 модулей (во всяком случае на тот момент). И вообще опыт преимущественно негативный был. Однако он умеет в HTML-инъекции (мне нужно было через <ForeignObject/> вставлять в формулы <input/>-ы. А MathJax это умеет
                      • Попробовал TexLive на стороне сервера в отдельном докер-контейнере. Это было тяжко. Там около 1.5 GiB одна только его сборка. Зато она просто всемогущая. И химические формулые разных видов, и разные математические расширения и чего там только нет. Всё кроме <ForeignObject/>-ов.

                      В итоге пришёл к гибридной схеме:


                      • Потребитель получает готовые SVG избавленные от неуникальных ID. Копирование в буфер обмена не требовалось.
                      • В админке используется и TexLive (запрос к backend-у) и MathJax. MathJax для интерактивных вставок, а TexLive для всего остального (в особенности для хитрого LaTeX синтаксиса вроде химических формул).
                      • При сохранении документа всё принудительно конвертируется в SVG и сохраняется на диск. Сами LaTeX формулы, разумеется, не теряются.
                      • Для размера используется единица измерения ex. Это очень удобно когда формула должна выглядеть однородно с текстом в inline-режиме.

                      Сборка TexLive была вынесена в отдельный репозиторий, т.к. одна только сборка docker контейнера отнимала легко 40+ минут.

                        0

                        Судя по алгоритму очень похоже на то что предлагает aafin, возможно имеет смысл подумать о использовании решения i.upmath.me :) Ну за исключением динамики — тут видимо альтернатив для MathJax немного.

                          0

                          Посмотрел — там внутри как раз TexLive :) Правда с PHP. У меня вместо PHP был простенький express.js server на 150 строчек. Эдакий wrapper поверх texlive и ещё пару cli-утилит.

                      Only users with full accounts can post comments. Log in, please.