Технічна стаття

Рендеринг мозаїчних патернів 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 у потоці. Тоді відмова проявиться десь несподівано: форма 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 для варіантів nonzero, будує шлях 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, той самий охоронець, що використовується для рекурсії форми 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