Техническая статья

Рендеринг PDF-паттернов мозаики в Delphi с HotPDF

Штриховка, отрендеренная как один плоский серый блок, — классический сбой паттерна мозаики. HotPDF, нативный VCL-компонент PDF для Delphi и C++Builder, рисует PatternType 1, превращая текущий путь во временную обрезку и повторно проигрывая поток содержимого паттерна один раз на каждый видимый тайл, а выбор паттерна хранится в графическом состоянии и восстанавливается операторами q и Q

Симптомы приходят в двух вариантах, и они выглядят несвязанными, пока не узнаешь причину. Чертёж CAD теряет секционную штриховку и возвращается как сплошные заливки, потому что рендерер свёл паттерн к среднему цвету и закрасил им. Или штриховка вытекает наружу: подложка титульного блока, которая должна быть просто белой, подхватывает диагональные линии из детального вида несколькими путями раньше. Обе проблемы связаны с состоянием паттерна, и лишь одна из них вообще касается рисования тайлов

Почему паттерн мозаики протекает в следующий путь?

Потому что выбранное имя паттерна — часть графического состояния, а не свойство оператора, который его использовал. ISO 32000-1 §8.6.6.2 определяет цветовое пространство Pattern как такое, чьё значение цвета — это имя паттерна, передаваемое в scn или SCN, и каждый другой компонент состояния цвета сохраняется q и восстанавливается Q. Имя паттерна должно подчиняться тому же правилу. HotPDF хранит его в записи состояния как FillPatternName и StrokePatternName, рядом с семейством цветового пространства заливки и обводки, так что Q возвращает предыдущий выбор в точности так же, как возвращает предыдущую CTM

Храните это имя в локальной переменной внутри диспетчера операторов вместо этого — и оно переживёт любой Q в потоке. Тогда сбой проявляется в неожиданном месте: Form XObject, нарисованный после пути с паттерном, наследует выбор паттерна, которого его собственный поток содержимого никогда не делал, и его заливки выходят заштрихованными. Вложенные формы усугубляют это, потому что каждый уровень вложенности выталкивает и снимает состояние, которое случайная переменная игнорирует. Установка непаттернового цветового пространства через cs или CS, или выдача простого g / rg / k, тоже должна очищать имя паттерна, иначе устаревший выбор переживёт цветовое пространство, придававшее ему смысл

q
  /Pattern cs              % pattern colour space, ISO 32000-1 8.6.6.2
  /P1 scn                  % coloured tiling pattern, PaintType 1
  10 10 200 120 re f       % this rectangle is hatched
Q
0 0 300 200 re f           % must be black again, not hatched

q
  /Cs2 cs                  % [/Pattern /DeviceCMYK] array
  0 0.6 1 0 /P2 scn        % uncoloured pattern plus its underlying colour
  20 20 160 90 re f*
Q

Паттерн рисуется через обрезку, никогда как заливка

Правильная модель — вычитающая: сузить обрезку устройства до формы, которую нужно закрасить, затем выполнить содержимое паттерна внутри неё. HotPDF никогда не рисует сначала сплошное приближение и не закрашивает его поверх, потому что промежуточная заливка была бы видна сквозь зазоры между тайлами и конфликтовала бы с любой прозрачностью в содержимом тайла. §8.7.3.2 описывает паттерн мозаики как поток содержимого, воспроизводимый с фиксированными горизонтальными и вертикальными интервалами, а воспроизведение имеет смысл только на фоне обрезки, у которой уже правильная форма. Для заливок преобразование прямое: HPDFSelectFillPathClip устанавливает режим заливки полигона в ALTERNATE для f*, B* и b* и в WINDING для вариантов без чётности, строит GDI-путь и пересекает его с обрезкой через SelectClipPath. Именно эта строка делает так, что паттерновая заливка по правилу чётности оставляет те же дыры, что и сплошная заливка по правилу чётности, а это в точности то, что нужно заштрихованной области в форме бублика

Обводки — та часть, где легко ошибиться. У обведённого пути нет внутренности, поэтому пересечение самого пути с обрезкой даёт пустую область, и ничто не закрашивается. Поэтому HPDFSelectStrokePathClip сначала строит геометрическое перо из текущего состояния, используя PS_GEOMETRIC с торцом из J, соединением из j, пределом среза из M и PS_USERSTYLE, когда активен массив штриха, а затем вызывает WidenPath, чтобы преобразовать обведённый контур в заполняемую область перед обрезкой. Поведение торца, соединения, среза и штриха на пути с паттерновой обводкой затем совпадает с обычной обводкой по построению, а не благодаря второй реализации. Здесь есть два честных ограничения: ширины линий ниже одной устройственной единицы прижимаются к одному пикселю, а массив штриха усекается до шестнадцати записей — это потолок, принимаемый ExtCreatePen

Какие тайлы на самом деле видны?

Видимый диапазон получается прогоном преобразования в обратную сторону. Размещение тайлов происходит в пространстве паттерна, но единственное, что знает, какая часть страницы затрагивается, — это область обрезки устройства, находящаяся в пространстве устройства. HotPDF составляет BaseMatrix := CTM * PatternMatrix, инвертирует её и отображает четыре угла области обрезки GDI обратно через обратную матрицу. Границы по осям этих четырёх отображённых углов дают прямоугольник в пространстве паттерна, который потенциально может быть покрыт, а деление этого прямоугольника на XStep и YStep относительно BBox паттерна даёт замкнутые диапазоны индексов. Затем каждая ячейка рендерится с CTM вида CTM * PatternMatrix * Translate(i * XStep, j * YStep) и обрезается второй раз по её собственному преобразованному полигону BBox. Эта вторая обрезка важна, когда XStep меньше ширины ограничивающего прямоугольника, что и есть способ выразить перекрывающиеся рисунки тайлов; без неё соседние ячейки закрашивали бы друг друга за пределами заявленного размера. Если обрезка отдельной ячейки возвращается как NULLREGION, ячейка пропускается без токенизации или выполнения чего-либо

// Map the device clip box back into pattern space through the inverse of
// CTM * PatternMatrix, then convert those bounds into tile index ranges.
BaseMatrix := HPDFMatMul(FGSStack.State.CTM, PatternMatrix);
if not HPDFMatInvert(BaseMatrix, InverseMatrix) then Exit;   // singular: refuse
if GetClipBox(FDC, ClipRect) = ERROR then Exit;

// MinX..MaxY are the axis-aligned bounds of the four mapped clip corners.
I0 := Floor((MinX - BBox[2]) / StepXAbs);
I1 := Ceil ((MaxX - BBox[0]) / StepXAbs);
J0 := Floor((MinY - BBox[3]) / StepYAbs);
J1 := Ceil ((MaxY - BBox[1]) / StepYAbs);

PlannedTiles := Int64(I1 - I0 + 1) * Int64(J1 - J0 + 1);
if (PlannedTiles <= 0) or (PlannedTiles > FPatternTilesRemaining) then Exit;
Dec(FPatternTilesRemaining, Integer(PlannedTiles));

Бесцветные паттерны и цвет, приходящий извне

Паттерн PaintType 2 несёт форму, но не цвет, а цвет приходит вместе с именем паттерна. §8.7.3.2 указывает, что бесцветный паттерн используется только с цветовым пространством Pattern, объявляющим базовое пространство, поэтому scn сначала получает значения компонентов, а затем имя паттерна. HotPDF разрешает эти компоненты через базовое пространство, хранящееся в записи цветового пространства паттерна, а значит, бесцветную штриховку можно тонировать краской Separation или комбинацией DeviceN совершенно как любую другую заливку; механика этого разрешения разобрана в статье о рендеринге плашечных цветов Separation и DeviceN. Внутри тайла два типа закраски резко расходятся. Для PaintType 2 рендерер устанавливает флаг подавления цветовых операторов на время тайла, так что любой g, rg, k или scn в содержимом паттерна игнорируется, и каждая отметка получает цвет, заданный извне. Для PaintType 1 верно обратное: состояние заливки и обводки сбрасывается к значениям по умолчанию PDF, DeviceGray чёрный с тождественным цветовым пространством, и тайл раскрашивает себя сам. Пропуск этого сброса позволяет цвету, который случайно был текущим на операторе f, просочиться в паттерн, который должен был описывать себя сам

Почему глубина стека графического состояния должна восстанавливаться после каждого тайла?

Потому что потоку содержимого паттерна разрешено быть несбалансированным, и ущерб накапливается по ячейкам. Тайл, чей поток содержит три оператора q и два оператора Q, оставляет стек на один кадр глубже, чем он начинался. Восстанавливайте между ячейками только текущую запись состояния — и глубина будет расти, так что ячейка номер двести выполняется из кадра стека, принадлежащего ячейке номер сто девяносто девять, с той CTM и обрезкой, которую нёс тот кадр. Поэтому HotPDF снимает снимок записи состояния и глубины стека перед циклом по тайлам и вызывает RestoreSnapshot в начале каждой итерации, что усекает стек до сохранённой длины и переустанавливает сохранённое состояние за один шаг. Словарь ресурсов страницы и флаг подавления цветовых операторов восстанавливаются на той же границе, поскольку тайл может ссылаться на собственные ресурсы и не должен передавать их соседу. Состояние обрезки GDI получает то же обращение через пару SaveDC / RestoreDC вокруг каждой ячейки, так что тайл, устанавливающий собственную обрезку W n, не может сузить область, доступную следующему

Бюджеты, отказы и то, что рендерер не станет рисовать

Паттерны мозаики — самое простое место в PDF, чтобы написать файл отказа в обслуживании, поэтому лимиты здесь — жёсткие числа, а не эвристики. Вложенность паттернов ограничена глубиной 4, той же защитой, что используется для рекурсии Form XObject, что останавливает паттерн, ссылающийся сам на себя через собственный словарь ресурсов. Одна закраска пути может выполнить не более 16 384 тайлов в сумме, отсчитываемых по вложенным паттернам и сбрасываемых только при начале закраски самого внешнего паттерна. Сетка тайлов, чьё запланированное число ячеек превышает остаток этого бюджета, отклоняется целиком, до выполнения хотя бы одной ячейки

Вырожденная геометрия отклоняется, а не приближается. Отсутствующий или нулевой по площади BBox, XStep или YStep, чья величина меньше 1e-6, произведение CTM * PatternMatrix без обратной матрицы, отображённые координаты обрезки за пределами 1e9 или величина индекса свыше миллиона — всё это заставляет закраску паттерна вернуться без рисования. Результат — незакрашенная область вместо зависшего потока рендеринга, а это тот компромисс, который нужен в пакетном конвертере. Производительность обеспечивается одним решением: поток паттерна токенизируется один раз на закраску через HPDFTokenizeContentStream, и массив токенов переиспользуется для каждой видимой ячейки, так что количество тайлов умножает стоимость выполнения, но никогда — стоимость лексического разбора

Рендеринг страницы с паттерном из Delphi

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

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('assembly-drawing.pdf') > 0 then
    begin
      // Section hatching that previously flattened to a solid block now
      // replays the tile content once per visible cell.
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 200);
      if Assigned(Bmp) then
      try
        Bmp.SaveToFile('sheet1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Когда заштрихованная область по-прежнему выглядит неправильно, проверяйте три класса сбоев по порядку. Область, полностью пустая, обычно означает отказ: проверьте XStep, YStep и BBox на вырожденные значения либо посчитайте, сколько тайлов потребовала бы сетка относительно потолка в 16 384. Область, закрашенная одним плоским цветом, означает, что имя паттерна так и не дошло до оператора закраски, что указывает на порядок cs и scn в потоке. Паттерн, появляющийся там, где ему не место, означает проблему восстановления состояния, и смотреть нужно на обработку q / Q вокруг формы или пути, унаследовавшего его

Паттерны мозаики — одна из тех функций PDF, что остаются невидимыми, пока в ваш почтовый ящик не попадёт файл, которому они нужны, а затем они становятся всей работой. Если вы строите просмотрщики чертежей, конвертеры инженерных документов или рендереры отчётов на Delphi или C++Builder, полный компонент и его API рендеринга документированы на странице HotPDF Delphi PDF component