У инженеров, которые проектируют электронные устройства, документация в виде PDF пользуется большой популярностью. Вспомним, к примеру, даташиты к электронным компонентам. Поэтому и наши САПР поставляются с комплектом PDF-файлов, которые рассказывают про особенности установки и эксплуатации. Долгое время документация выпускалась только так: вместе с каждым релизом продукта в виде набора PDF. Это было устроено не из соображений удобства, а в силу ограничений самого процесса, и со временем эти ограничения стали ощутимы.

В этой статье мы расскажем, как развивали систему документации, сохранив за техническими писателями привычный инструмент, какие трудности возникли с производительностью генератора сайта и как в итоге появился портал docs.eremex.ru. При этом привычный инженерам формат PDF мы сохранили: новый портал не заменяет его, а дополняет, и документация по-прежнему доступна в виде файлов для тех, кому так удобнее.

Как было: документация как часть релиза, а не как сервис

Help&Manual представляет собой нишевый, но довольно популярный среди технических писателей инструмент для авторинга: WYSIWYG-редактор, единый проект, экспорт в разные форматы. Мы использовали его много лет и на выходе получали документацию в виде набора PDF-файлов, по одному или нескольким файлам на продукт.

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

  • Поиск внутри PDF неудобен. Стандартный поиск по Ctrl+F работает только внутри одного файла и плохо справляется даже с этим, особенно когда документ разрастается до сотен страниц.

  • PDF оставался невидимым для поисковых систем и нейросетей. Пользователь, искавший решение проблемы с нашим продуктом, не находил наших же документов: поисковая выдача показывала форумы и сторонние обсуждения. По той же причине ни один AI-ассистент не располагал сведениями о наших продуктах, поскольку содержимое статичных PDF-файлов поисковые системы индексируют неохотно.

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

Стало очевидно, что проблема не в Help&Manual как редакторе: писатели к нему привыкли, и переучивать их не было необходимости. Проблема состояла в том, каким образом контент превращался в готовую документацию и доходил до пользователя.

Шаг первый: автоматический выход из Help&Manual

Главным условием было сохранить процесс работы технических писателей в неизменном виде. Help&Manual хранит каждую статью в виде XML-файла, а значит, контент можно трансформировать программно, без участия человека.

Так появился конвертер, представляющий собой консольное приложение на .NET Core, которое мы написали с использованием XSLT-трансформаций. На входе он принимает XML-файлы Help&Manual, на выходе формирует Markdown. Перенос всех статей оказался полностью автоматическим: ни одна из более чем двух тысяч статей не переписывалась вручную.

Сейчас портал насчитывает около двух с половиной тысяч статей, и все они продолжают создаваться и обновляться в прежнем редакторе.

Для сборки сайта из получившегося Markdown был выбран MkDocs, на тот момент наиболее очевидный и проверенный вариант для документации в формате docs-as-code. Некоторое время эта схема работала исправно.

Как устроен конвертер изнутри

Рассмотрим техническую сторону подробнее.

Запуск XSLT из C#

XSLT-файл загружается из ресурсов сборки, после чего трансформация выполняется через XslCompiledTransform, который компилирует XSLT в байт-код при загрузке, что обеспечивает высокую скорость при обработке большого количества файлов. Каждый XML-файл из Help&Manual читается через XmlReader и записывается через XmlWriter в кодировке UTF-8 без BOM. Последнее существенно: MkDocs и Zensical чувствительны к наличию BOM в файлах.

XslCompiledTransform transformer = new XslCompiledTransform();
transformer.Load(xsltFile);

var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
XmlWriterSettings settings = new XmlWriterSettings
{
    ConformanceLevel = ConformanceLevel.Fragment,
    OmitXmlDeclaration = true,
    Encoding = utf8NoBom
};

using (XmlReader xmlReader = XmlReader.Create(stringReader))
using (XmlWriter writer = XmlWriter.Create(targetFilePath, settings))
{
    transformer.Transform(xmlReader, CreateArguments(...), writer);
}

Extension-объекты: связь между C# и XSLT

Чистый XSLT 1.0 не хранит состояние между вызовами шаблонов, а нам требовалась сквозная нумерация рисунков и таблиц по всей статье. Решением стали extension-объекты, то есть обычные C#-классы, которые регистрируются в XsltArgumentList и становятся доступны из XSLT по пространству имён.

private static XsltArgumentList CreateArguments(...)
{
    XsltArgumentList args = new XsltArgumentList();

    // Счётчик номеров рисунков
    MutableCounter imageCounter = new MutableCounter();
    args.AddExtensionObject("urn:xslt-extensions", imageCounter);

    // Счётчик номеров таблиц
    MutableCounter tablesCounter = new MutableCounter();
    args.AddExtensionObject("urn:xslt-extensions-tables", tablesCounter);

    // Транслитератор имён файлов рисунков
    RussianToEnglishConverter rus = new RussianToEnglishConverter();
    rus.Prefix = imagesPrefix; // префикс продукта, например "index"
    args.AddExtensionObject("urn:xslt-extensions-images", rus);

    // Конвертер имён топиков и резолвер ссылок
    TopicNameconverter topic = new TopicNameconverter(sourceDir, dirs);
    args.AddExtensionObject("urn:xslt-extensions-files", topic);

    return args;
}

В XSLT эти объекты объявляются через xmlns и используются как обычные функции:

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:imagecounter="urn:xslt-extensions"
    xmlns:tablecounter="urn:xslt-extensions-tables"
    xmlns:images="urn:xslt-extensions-images"
    xmlns:files="urn:xslt-extensions-files"
    exclude-result-prefixes="imagecounter tablecounter images files">

Нумерация рисунков

Это наиболее показательная часть с точки зрения взаимодействия C# и XSLT. Help&Manual использует собственный плейсхолдер <%HMFIGURECOUNTER%> в подписях к рисункам. При конвертации его необходимо заменить на порядковый номер.

Класс MutableCounter намеренно сделан простым: он хранит счётчик и предоставляет два метода.

public class MutableCounter
{
    public int Value { get; set; } = 1;

    public int Increment()
    {
        Value++;
        return Value;
    }

    public int GetValue() => Value;
}

В XSLT шаблон обработки рисунка выглядит следующим образом:

<xsl:template match="image">
    <!-- Сначала получаем текущее значение счётчика без инкремента,
         чтобы определить, есть ли у рисунка подпись -->
    <xsl:variable name="currentimagetitle">
        <xsl:value-of select="files:ReplaceCounters(caption, imagecounter:GetValue())"/>
    </xsl:variable>

    <xsl:variable name="imagetarget">
        <xsl:value-of select="concat($imagesPath, images:Convert(@src))"/>
    </xsl:variable>

    <xsl:choose>
        <!-- SVG-рисунки обрабатываются отдельно, вставляются как HTML-тег -->
        <xsl:when test="contains($imagetarget, 'svg')">
            <image src="{$imagetarget}"/>
        </xsl:when>

        <!-- Рисунок с подписью: инкрементируем счётчик и формируем figure -->
        <xsl:when test="$currentimagetitle != ''">
            <xsl:variable name="imagetitle">
                <xsl:value-of select="files:ReplaceCounters(caption, imagecounter:Increment())"/>
            </xsl:variable>
            <xsl:text disable-output-escaping="yes">&lt;figure&gt;</xsl:text>
            <img src="{$imagetarget}" alt="{$imagetitle}"/>
            <xsl:text disable-output-escaping="yes">&lt;figcaption&gt;</xsl:text>
            <xsl:value-of select="$imagetitle"/>
            <xsl:text disable-output-escaping="yes">&lt;/figcaption&gt;&lt;/figure&gt;</xsl:text>
        </xsl:when>

        <!-- Рисунок без подписи: простой Markdown-синтаксис -->
        <xsl:otherwise>
            <xsl:value-of select="concat('![](', $imagetarget, ')')"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

Логика построена на двух методах счётчика. Сначала вызывается imagecounter:GetValue(), чтобы проверить наличие подписи, не изменяя счётчик. Если подпись есть, вызывается imagecounter:Increment(), который увеличивает значение и возвращает новое. Благодаря этому счётчик растёт только для пронумерованных рисунков.

Транслитерация имён файлов рисунков

Help&Manual допускает имена файлов рисунков на русском языке, что недопустимо в URL. Поэтому все имена файлов при копировании проходят через транслитератор:

// Кириллица в латиницу, пробелы в подчёркивания
// "Окно настроек.png" в "indexokno_nastroek.png"
var targetFileName = RussianToEnglishConverter.ConvertWithPrefix(
    Path.GetFileName(imageFile),
    databaseName.Name  // префикс продукта, например "index" для Delta Design
);

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

Отдельные шаблоны

Заголовок статьи преобразуется в Markdown-frontmatter с YAML и заголовок уровня #:

<xsl:template match="header">
    <xsl:text>---&#10;</xsl:text>
    <xsl:text>search:&#10;</xsl:text>
    <xsl:text>  exclude: true&#10;</xsl:text>
    <xsl:text>---&#10;&#10;</xsl:text>
    <xsl:value-of select="concat('# ', normalize-space(para/text))"/>
    <xsl:text>&#10;</xsl:text>
</xsl:template>

Ссылки между статьями преобразуются в реальные пути к Markdown-файлам. Help&Manual хранит ссылки как пути к XML-файлам, а конвертер транслирует их в пути с расширением .md через метод TopicNameconverter.Convert(), который читает элемент <title> из XML-файла и транслитерирует его в имя файла:

<xsl:template match="link">
    <xsl:variable name="linkTarget">
        <xsl:when test="@href[string-length() > 0]">
            <xsl:value-of select="files:Convert(@href)"/>
            <xsl:if test="not(contains(@href, '.'))">
                <xsl:text>.md</xsl:text>
            </xsl:if>
        </xsl:when>
        <!-- якорные ссылки внутри статьи -->
        <xsl:when test="@anchor[string-length() > 0]">
            <xsl:text>#</xsl:text>
            <xsl:value-of select="@anchor"/>
        </xsl:when>
    </xsl:variable>
    <xsl:value-of select="concat('[', $linkTitle, '](', $linkTarget, ')')"/>
</xsl:template>

Подсказки (tips, warnings, notes) обрабатываются одним из самых содержательных шаблонов. В Help&Manual они оформляются как таблица из двух столбцов, где в первом столбце расположена иконка (warning.png, info.png, idea.png). Конвертер распознаёт этот паттерн по структуре таблицы и преобразует его в синтаксис admonition-блоков Zensical:

<xsl:template match="table[
    @colcount='2' and @rowcount='1'
    and not(thead)
    and tr/td[1]/para/image/@src != ''
]">
    <xsl:variable name="hinttype"
        select="substring-before(tr/td[1]/para/image/@src, '.')"/>
    <!-- "warning.png" в "warning", далее в "!!! warning" -->
    <xsl:value-of select="concat('!!! ', $hinttype, ' ', files:Localize($hinttype))"/>
    <xsl:text>&#xa;&#xa;    </xsl:text>
    <xsl:apply-templates select="tr/td[2]"/>
</xsl:template>

Фильтрация статей по статусу

Конвертер обрабатывает только статьи со статусом «Завершен» в атрибуте корневого XML-элемента. Это позволяет техническим писателям держать черновики в том же репозитории и в том же проекте Help&Manual: на сайт они не попадут до тех пор, пока статус не будет выставлен.

private static bool AllowProcessXML(string file, string xmlContent)
{
    var status = TopicNameconverter.ExtractStatus(xmlContent);
    return status == "Завершен";
}

Шаг второй: пределы производительности MkDocs

Документация Eremex охватывает не один продукт, а целую линейку: Delta Design, SimPCB Lite, Enterprise Server, Simtera IC, DeltaCAM. В сумме это около двух с половиной тысяч статей. На таком объёме MkDocs начал заметно замедляться: полная сборка сайта занимала более пяти минут.

Для CI, где сборка запускается на каждый коммит, пять минут составляют существенную задержку. Каждое изменение одной строки в одной статье означало пятиминутное ожидание перед тем, как результат становился доступен на внутреннем сайте.

В качестве альтернативы был выбран Zensical, более новый статический генератор документации. Поскольку промежуточным форматом уже служил Markdown, переход с одного генератора на другой не потребовал повторной миграции контента, ограничившись перенастройкой сборки. Результат оказался следующим:

  • сборка сайта сократилась с приблизительно пяти минут до приблизительно одной минуты;

  • полнотекстовый поиск на сайте стал быстрее и точнее, чем у MkDocs.

Как это работает сейчас

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

Единственное, чему потребовалось обучить писателей, это работа с Git. Они фиксируют изменения в репозитории, и далее процесс выполняется автоматически:

  1. По коммиту запускается CI.

  2. Конвертер на .NET Core и XSLT преобразует XML-файлы Help&Manual в Markdown.

  3. Zensical собирает из Markdown статический сайт.

  4. CI собирает Docker-образ со статическим сайтом и публикует его в GitLab Container Registry.

  5. Watchtower на внутреннем сервере обнаруживает новый образ и автоматически перезапускает контейнер.

  6. Обновлённая версия становится доступна на внутреннем сайте документации.

На внутреннем сайте команда проверяет новые и изменённые статьи, убеждаясь, что контент отображается корректно, конвертация прошла без ошибок, а форматирование соответствует задуманному.

Публичная версия docs.eremex.ru обновляется вручную, синхронно с релизом продукта. Это осознанное решение: выпуск продукта и выпуск документации к нему остаются согласованным событием, однако теперь публикация представляет собой полностью контролируемый шаг, а не сжатую по срокам пересборку PDF.

Технический писатель
        │  пишет в Help&Manual
        ▼
   XML-файлы (Help&Manual)
        │  git commit
        ▼
        CI (GitLab)
        ├─ XSLT-конвертер: XML в Markdown
        ├─ Zensical: Markdown в статический сайт (~1 мин)
        └─ Docker build, push в GitLab Container Registry
        ▼
  GitLab Container Registry
        │  Watchtower опрашивает раз в минуту
        ▼
  Внутренний сервер (Docker, Watchtower)
        │  автоматический pull и restart контейнера
        ▼
  Внутренний сайт документации
        │  проверка командой
        ▼
  Публикация вручную, синхронно с релизом продукта
        ▼
     docs.eremex.ru

Развёртывание через Docker и Watchtower

Для автоматического обновления внутреннего сайта не потребовалось писать deployment-скрипты и настраивать SSH-доступ с CI на сервер. Вместо этого применяется Watchtower, служба, которая отслеживает обновления Docker-образов в реестре и перезапускает контейнеры при появлении новой версии.

Вся инфраструктура сервера описана в одном файле docker-compose.yml:

services:
  docs.app:
    image: registry.gitlab.eremex.ru/eremex/docs.app
    container_name: docs.app
    restart: always

  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_POLL_INTERVAL=60  # проверять обновления раз в минуту
    command: docs.app --label-enable
    restart: unless-stopped

Конфигурация состоит из двух сервисов, каждый из которых выполняет одну задачу.

docs.app содержит статический сайт документации. CI собирает этот контейнер при каждом коммите и публикует его в GitLab Container Registry.

watchtower обеспечивает автоматическое обновление. Он монтирует Docker-сокет (/var/run/docker.sock), что позволяет ему управлять другими контейнерами от имени хоста. Раз в минуту, согласно параметру WATCHTOWER_POLL_INTERVAL, Watchtower проверяет реестр. Если образ docs.app обновился, служба загружает новую версию и перезапускает контейнер. Такой подход не требует ни вебхуков, ни SSH, ни deployment-скрипта на сервере.

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

Что изменилось

Измеримых показателей у нас немного, однако изменения ощутимы и для команды, и для пользователей.

Скорость сборки. Пять минут на MkDocs против одной минуты на Zensical составляют пятикратное ускорение, и при объёме около двух с половиной тысяч статей эта разница проявляется на каждом коммите.

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

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

Документация вошла в поисковую выдачу и в ответы нейросетей. Контент, прежде недоступный для Google и AI-ассистентов, теперь индексируется как обычный веб-сайт. На практике это означает, что AI-ассистенты дают более содержательные и точные ответы по нашим продуктам, поскольку получили доступ к первоисточнику, а не только к фрагментам обсуждений на сторонних форумах.

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

Что дальше: контекстная справка в Delta Design

Новая система документации открыла возможность, недоступную при работе с PDF, а именно контекстную справку непосредственно внутри Delta Design.

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

С набором PDF-файлов такое решение было неосуществимо: у отдельных тем не было ни постоянных URL, ни механизма точечной навигации внутри файла. Сайт на основе Markdown, где каждой статье соответствует отдельная страница, предоставляет иные возможности: у каждой темы есть постоянный адрес, и привязать к нему идентификатор формы технически несложно.

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

deltadesign://options
deltadesign://project/settings

Такая ссылка в статье документации при нажатии будет открывать соответствующее окно или диалог в запущенном экземпляре Delta Design. Пользователь, читающий статью о настройке экспорта, сможет открыть нужное окно нажатием на ссылку в тексте, не разыскивая соответствующий пункт меню самостоятельно.

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

Пока это планы. Реализуемыми их делает то обстоятельство, что документация теперь существует как полноценный веб-сайт с постоянными URL и открытой структурой.

Итоги

Основной вывод из нашего опыта состоит в том, что для модернизации публикации документации не обязательно менять инструмент, в котором она создаётся. Технические писатели остались в привычном для них редакторе Help&Manual, тогда как преобразования произошли на уровне инфраструктуры: автоматический конвертер вместо ручного экспорта, Git вместо пересылки файлов, CI вместо ручной сборки, Docker с Watchtower вместо развёртывания по SSH, современный статический генератор вместо устаревшего механизма выпуска PDF.

Второй вывод заключается в том, что выбор инструмента для самой трансформации также не является окончательным. Мы перешли с Help&Manual на MkDocs, считая этот этап завершающим, однако на объёме более двух тысяч статей MkDocs достиг своих пределов. Переход на Zensical потребовался не вследствие ошибки в начале пути, а потому, что требования возросли вместе с объёмом документации. Решение, оптимальное для двухсот статей, не обязано оставаться таковым для двух с половиной тысяч.

Комментарии (8)


  1. Emelian
    25.06.2026 12:12

    Help&Manual хранит каждую статью в виде XML-файла, а значит, контент можно трансформировать программно, без участия человека.

    Ну, если я правильно понял, имея оригинальный текст статьи в xml (либо html), то, преобразовать его в pdf или тот же html для сайта не должно быть большой технической проблемой. Тогда непонятно, почему страницы с фрагментами ваших статей обновляются так долго (порядка нескольких секунд)?

    Куда интересней задача преобразования цифрового pdf в форматированный html-файл. На самом деле, существуют опенсорс на Гитхабе, типа, PdfToHtml (для непосредственного отображения pdf-страниц в вебе). На его базе также существует масса онлайн-ресурсов, которые преобразуют pdf-страницы в «чистый» html.

    Но, как всегда в подобных случаях: «Дьявол кроется в деталях». Подобные html-страницы слишком перегружены информацией, настолько, что приходится делать собственный «велосипед» для получения упрощенного форматирования, например, для целей создания двуязычных книг ради изучения иностранного языка. Вот пример:

    Двуязычная французская грамматика – горизонтальное представление
    Двуязычная французская грамматика – горизонтальное представление
    Двуязычная французская грамматика – вертикальное представление
    Двуязычная французская грамматика – вертикальное представление

    Хорошо, что это не ваша книга. У вас простое «использование документа» уже является нарушением. Т.е., вы позволяете скопировать ваш pdf-файл на компьютер, но, как только я это сделал, хотя бы просто любопытства ради (тем более, что вы сами поощряете это), то уже автоматически становлюсь нарушителем, судя по вашему грозному «Руководству Пользователя»? Т.е., ничего нельзя, а что можно то?


    1. xtraroman Автор
      25.06.2026 12:12

      У портала документации действительно прописаны некоторые ограничения на использование материалов (https://docs.eremex.ru/legal/). Я не юрист и не могу их комментировать. Если у вас есть идея, относящаяся к нашим статьям, напишите нам. Я уверен, всё можно решить.

      Все наши статьи в исходном виде хранятся в XML-файлах Help&Manual, поэтому нам не пришлось возиться с извлечением информации из PDF-файлов. В общем случае это очень сложная задача. Мне встречались PDF-файлы, в которых текст был представлен в виде растрового изображения. Также в некоторых случаях текст в PDF может быть преобразован в набор кривых. Поэтому в общем случае извлечение контента из PDF — это сложная задача, сравнимая по сложности с распознаванием документа. Хорошо, что нам не пришлось этим заниматься.


      1. Emelian
        25.06.2026 12:12

        У портала документации действительно прописаны некоторые ограничения на использование материалов

        Это не «некоторые ограничения» это абсолютные ограничения! Теоретически, судя по вашей «Правовой информации», вы можете привлечь к ответственности любого, кто просто воспользовался вашей ссылкой и скопировал себе на компьютер любой файл («воспроизведение» и «копирование», как минимум). Более того, если пользователь только зашел на ваш сайт он уже «нарушитель» ибо браузер автоматически скопировал в кэш компьютера вашу html-страницу (содержащую вашу интеллектуальную собственность). Естественно, посмотреть эту html-страницу и, не дай Бог, скопировать оттуда, для своих коммерческих целей, допустим, какой-нибудь скрипт – святотатство! Даже если этот скрипт делает что-то нехорошее на моем компьютере. Тем более, что я его не просил загружаться. Но, все равно виноват? Иначе говоря, сайту – все права, а пользователю – никаких!

        Обычно, использование авторской информации для личных целей не запрещают, только коммерческое использование чужих материалов может быть наказуемо. Вы же «перестраховались» на все тысячу процентов.

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

        Сложная? Согласен, сам уже вожусь не первую неделю. Только эта тема мне понравилась, поскольку pdf рендерится не последовательно, как все «нормальные» документы, а в виде произвольно отображаемых прямоугольных областей. Соответственно, если прямоугольники, допустим, A и B, не пересекаются, то результат не зависит от порядка их отображения, т.е. A•B == B•A (коммутативность). Часто это тупо нарушает линейную последовательность текста. Во избежание чего приходится сортировать текст (уровня символов) по их координатам. Но и это не всегда помогает, если текст содержит подстрочные и надстрочные символы (которые оформляются прямоугольниками для тех же самых шрифтов, но меньшего размера при непредсказуемом порядке следования).

        Что касается растровых изображений, то это просто прямоугольники с графикой. В принципе, как вы пишите, pdf может состоять только из них. Но тогда можно использовать библиотеки распознавания символов в изображениях, в том же Питоне (вроде Тэзэракта). Да, это ненадежный вариант. Я для устранения дефектов распознавания использую собственную (неопубликованную) программу «МедиаГекст» (для работы с видео, звуком и обычными изображениями, например, извлеченными из pdf/djvu-файлов):

        Программа для ручного распознавания встроенных субтитров видео
        Программа для ручного распознавания встроенных субтитров видео
         Программа для ручного распознавания текста на изображениях
        Программа для ручного распознавания текста на изображениях

        Также в некоторых случаях текст в PDF может быть преобразован в набор кривых.

        Это что-то вроде векторной графики. Обычно ее немного и проще распознать вручную либо вообще игнорировать, как и обычные изображения, в упомянутой мной книге по французской грамматике.

        В вашем случае, действительно все проще, если не считать непонятную задержку при рендеринге фрагментов статей в html-формате. Это проблемы сервера?


      1. PereslavlFoto
        25.06.2026 12:12

        Если у вас есть идея, относящаяся к нашим статьям, напишите нам.

        Пишу. Есть идея.

        Везде, где у вас написано “нельзя”, напишите “можно”.

        Где у вас “запрещено”, напишите “разрешаем”.

        Это очень просто. Попробуйте сами.


        1. xtraroman Автор
          25.06.2026 12:12

          Какую именно проблему нужно решить? Вы хотите портал с нашей документацией разместить в закрытой сети предприятия?


          1. PereslavlFoto
            25.06.2026 12:12

            Проблема хорошо обозначена выше в комментариях. Вы слишком многое запретили.


  1. xtraroman Автор
    25.06.2026 12:12

    Вы же «перестраховались» на все тысячу процентов.

    Спасибо, что обратили внимание. Мы проверим этот момент с коллегами.

    Я для устранения дефектов распознавания использую собственную (неопубликованную) программу «МедиаГекст».

    Скриншоты выглядят отлично. Видно, что проделана большая работа.

    Если не считать непонятную задержку при рендеринге фрагментов статей в HTML-формате. Это проблемы сервера?

    В статье я говорил про затраты времени на приготовление всего сайта. Имеем на входе набор XML-файлов Help&Manual, а на выходе должен получиться сайт. Этот процесс состоит из нескольких стадий (как я это понимаю):

    1. XSLT-преобразование XML → MD.

    2. MkDocs / Zensical готовят из большого набора MD-файлов HTML-файлы с учётом заданных стилей и шаблонов, добавляется код Яндекс.Метрики в каждую страницу.

    3. Кроме собственно контента сайта в виде HTML, надо правильно приготовить навигационную часть сайта (то, что будет показываться в левой колонке).

    4. Есть ещё стадия приготовления индекса для полноценного поиска по сайту.

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


    1. Emelian
      25.06.2026 12:12

      Скриншоты выглядят отлично. Видно, что проделана большая работа.

      Спасибо! Надеюсь, после оптимизации программы, как дойдут до этого руки, опубликовать её в опенсорсе.

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

      Не уверен, что вы организовали все оптимально. Возьмите любой сайт, где отображаются pdf-страницы (как правило, с помощью PdfToHtml) целиком, а не фрагментами, как у вас. Они рендерятся достаточно быстро, хотя html-кода там не меряно. Да и «навигационная часть сайта (то, что будет показываться в левой колонке)» там выглядит более симпатично, чем у вас. Т.е., все это – уже давно существующий опенсорсный функционал.

      А то, что у вас много внутренней работы на сайте, так и оставьте ее внутренней, а не выносите наружу. Ну, а если вам влом делать оптимизацию вашего сайта, то кто вас заставит? По своему длительному программистскому опыту, на производственном предприятии, убедился, что какой бы плохой ни была программа, всегда найдутся пользователи, у которых к ней не будет никаких претензий.

      Надеюсь, что таких пользователей вашего сайта – большинство!