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

PDF функции Type 2/3/4 в Delphi: експоненциални и PostScript

HotPDF, собственият VCL PDF компонент за Delphi и C++Builder, изчислява трите типа PDF функции, изградени от формули, а не от примерни таблици: експоненциална интерполация Type 2, свързване Type 3 и калкулаторни функции Type 4 на PostScript, съответстващи на ISO 32000-1 §7.10.3, §7.10.4 и §7.10.5. Type 2 смесва два изходни вектора по крива, Type 3 свързва няколко подфункции в един входен домейн, а Type 4 изпълнява ограничена програма на PostScript, която може да разклонява, сравнява и изчислява почти всичко, от което се нуждае потокът от съдържание. Ако някой от трите бъде реализиран дори малко неправилно, грешката никога не се обявява сама като дефект — появява се като градиент с напълно плоска лента, спот цвят, който се рендерира като чисто черен, или калкулаторна функция, която се отклонява точно с единица при входовете, които тестовият набор не е пробвал

Тези три стоят до четвъртия, Type 0, който съхранява извадена таблица вместо формула и е разгледан отделно в съпътстващата статия за таблиците за търсене на цветове Type 0. Двете семейства решават един и същ проблем, като преобразуват вход в изход, но Type 0 е данни, изчислени веднъж и вградени във файла, докато Type 2, 3 и 4 са код, който четецът изчислява при всяко извикване. И четирите споделят една точка за разпределение в рендерера на HotPDF, определена от записа /FunctionType в речника на функцията, така че shading, tint transform или spot функция на полутонове никога не трябва предварително да знае кой от четирите е получила, преди да поиска цвят

Как работи експоненциална PDF функция Type 2

PDF функция Type 2 изчислява една формула — y = C0 + x^N × (C1 − C0), приложена по компоненти — където x е единственият вход на функцията, нормализиран спрямо нейния /Domain преди изпълнението на формулата (ISO 32000-1 §7.10.3). /C0 и /C1 са изходните вектори в двата края на диапазона, по едно число за всеки изходен компонент, а /N е експонентата, която оформя кривата между тях: N = 1 дава правата линейна рампа зад повечето градиентни преходи и duotone преобразувания, N над 1 приближава кривата към C0, а N между 0 и 1 я приближава към C1. RegisterExponentialFunction изгражда този речник от пет аргумента и връща функционален обект, готов за включване в shading, spot функция на полутонове или навсякъде другаде, където спецификацията приема ключ /Function

Връзката между броя компоненти на C0 и C1 е важна два пъти: веднъж при създаването на функция Type 2 и отново всеки път, когато HotPDF трябва да рендерира функция, която не е създал. При създаване RegisterExponentialFunction проверява C0 и C1 един спрямо друг и повдига грешка, ако не съвпадат, така че извикване, достигнало до BeginDoc, вече има съгласуван функционален обект. При рендериране обаче изчислителят трябва да се довери на каквито /C0 и /C1 масиви действително декларира изходният файл — например файл от печатница, отворен за преглед, или подписан документ, показан отново на потребител — а версиите преди 2.376.0 четяха тези масиви в буфер, оразмерен за четири компонента, тоест за случая CMYK. Експоненциален tint за DeviceGray или DeviceRGB, с едно- или триелементни /C0 и /C1, тихо се проваляше при това четене и оставяше двата масива нулеви, така че tint-ът се рисуваше като плоско черен вместо с желания цвят. Версия 2.376.0 оразмери четеца според действителния деклариран брой изходи на функцията, вместо според фиксиран буфер — точно такъв дефект се открива само от тестов случай, който не е CMYK, тъй като съществуващият набор изпълняваше CMYK навсякъде, където четири към четири винаги се побира

var
  EaseIn: THPDFDictionaryObject;
begin
  // Type 2: one input, N > 1 biases the ramp toward C0 (an ease-in curve)
  EaseIn := Pdf.RegisterExponentialFunction(
    [0,1],           // Domain: single input, clamped to [0,1]
    [0, 0, 0],       // C0: output at x = 0
    [0.8, 0, 0],     // C1: output at x = 1
    3,               // N: exponent, 1 = linear, > 1 eases toward C0
    []);             // Range omitted: defaults to a [0,1] clamp per output
end;

Свързване Type 3: обединяване на подфункции чрез масив Bounds

PDF функция Type 3 свързва k подфункции в едно поетапно преобразуване върху един входен /Domain, а двата масива, които правят това възможно, са /Bounds и /Encode (ISO 32000-1 §7.10.4). /Bounds съдържа k − 1 вътрешни разделителни точки, които разделят /Domain на k последователни интервала; изчислителят избира първия интервал, чиято горна граница е по-голяма от входа, или последния интервал, щом входът достигне крайната граница, и предава управлението на подфункцията на този интервал. След това /Encode пренасочва входа от позицията му в интервала към диапазона, който очаква самата избрана подфункция — обикновено [0, 1], ако подфункцията е още един експоненциален сегмент — преди изчислението да продължи едно извикване по-дълбоко, към собствения /Domain и /Range на тази подфункция

Изчислителят за свързване на HotPDF преди обработваше точно две подфункции, а четецът на /Bounds изискваше пълен масив от осем елемента, така че единствената разделителна точка, от която действително се нуждае двусегментен градиент — едно число в /Bounds — винаги не успяваше да се анализира и функцията не връщаше нищо. /Encode изобщо не се прилагаше. Версия 2.376.0 пренаписа избора като общо търсене на k подфункции според описанието в спецификацията и започна да чете /Bounds според реалната му декларирана дължина, така че градиент с три, четири или пет стопа, свързан от същия брой експоненциални сегменти, сега се разрешава по същия начин, по който винаги е трябвало да се разрешава градиентът с два сегмента. Примерът по-долу изгражда двусегментна рампа от черно през червено към бяло, формата, към която осево или радиално shading се обръща, когато една експоненциална крива не може да пренесе всички цветови стопове, изисквани от дизайна

var
  ToRed, ToWhite, Ramp: THPDFDictionaryObject;
begin
  // Two linear segments: black->red over [0, 0.5], red->white over [0.5, 1]
  ToRed   := Pdf.RegisterExponentialFunction([0,1], [0, 0, 0],   [0.8, 0, 0], 1, []);
  ToWhite := Pdf.RegisterExponentialFunction([0,1], [0.8, 0, 0], [1, 1, 1],   1, []);

  Ramp := Pdf.RegisterStitchingFunction(
    [0,1],                  // Domain: the stitched function's own input range
    [ToRed, ToWhite],       // Functions: k = 2 sub-functions
    [0.5],                  // Bounds: k - 1 = 1 split point
    [0,1, 0,1],             // Encode: 2 numbers per sub-function
    []);                    // Range omitted: inherited from each sub-function
end;

Какво може калкулаторната функция Type 4 на PostScript, което Type 2 и Type 3 не могат

PDF функция Type 4 изпълнява истинска, макар и умишлено ограничена програма: калкулатор на PostScript, който поставя входовете си в стек от операнди, изпълнява аритметични, сравнителни, стекови и булеви оператори, както и условни конструкции if/ifelse, и оставя изходите си в стека при завършване (ISO 32000-1 §7.10.5, Table 42). Няма циклична конструкция и няма съхранение на именувани променливи, а само стекът, което прави съвместимата програма лесна за разсъждение — но в рамките на този ограничен набор от оператори Type 4 може да изрази неща, които Type 2 и Type 3 не могат, като реална формула за смесване на няколко мастила за сепарация DeviceN или функция на полутонове с условен праг. Изчислителят на HotPDF, HPDFEvalPostScriptCalculator, разделя програмата на елементи веднъж — числа, оператори и процедурни блокове { } — след което обхожда стек от 100 операнда, какъвто изисква ISO 32000-1 §7.10.5, зад твърд предел от 50 000 изчислени оператора като защитна мярка срещу патологични или ръчно написани програми

Операторът roll: посоката лесно се обръща

roll е операторът, който най-често се получава наопаки при първи опит, защото и редът на аргументите, и посоката на завъртането му са обратни на начина, по който английският ги описва. n j roll изважда брой n и величина на завъртане j, след което циклично измества горните n елемента на стека с j позиции, като връща елементите, изпаднали от единия край, обратно през другия; каноничният пример направо от спецификацията е a b c 3 1 roll, който дава c a b — горният елемент се премества в долната част на групата, а не обратно, и всеки друг елемент се измества с едно нагоре, за да освободи място. Изчислителят на HotPDF изчислява новата позиция на елемента от стека i като (i + j) mod n, което съвпада точно с този пример, но това е цикъл от два реда, който също толкова лесно може да бъде написан с обърнато завъртане, а огледален roll все пак създава правдоподобно изглеждащ цвят — просто не е цветът, поискан от автора на файла

const
  Prog = '{ 3 1 roll }';   // (a b c) -> (c a b): the third input moves to the front
var
  Reorder: THPDFStreamObject;
begin
  // Type 4: 3 inputs, 3 outputs, no extra clamping beyond Domain/Range
  Reorder := Pdf.RegisterPostScriptFunction(
    [0,1, 0,1, 0,1],   // Domain: 2 numbers per input
    [0,1, 0,1, 0,1],   // Range: 2 numbers per output (required for Type 4)
    Prog);
end;

round не е Delphi Round: закръгляне при половинки срещу банково закръгляне

Операторът round на PostScript разрешава равенство при .5 винаги към по-голямото цяло число, а вградената функция Round на Delphi не прави това: тя закръглява към най-близкото четно число, тоест използва банковото закръгляне, при което посоката на равенството при .5 се редува, за да не се натрупва отклонение при повторно закръгляне. Двете съвпадат почти навсякъде и се разминават точно на границата, която е важна тук — Round(0.5) на Delphi връща 0, а Round(2.5) връща 2, докато round в PDF спецификацията иска съответно 1 и 3 — така разликата се скрива при повърхностно тестване и след това се проявява като постоянна грешка с единица навсякъде, където междинната аритметика на калкулаторната програма попадне точно на полуцяло число. ISO 32000-1 §7.10.5 Table 42 изрично посочва, че round поставя дробното .5 към по-голямото цяло число, затова HotPDF реализира оператора като Floor(x + 0.5), вместо да извиква Round на Delphi, а всеки код, който пресъздава или проверява аритметиката на програма Type 4 ръчно, трябва да използва същата замяна

function PostScriptRound(const X: Double): Double;
begin
  // ISO 32000-1 7.10.5 Table 42: round pushes .5 toward the greater
  // integer. Delphi's Round() is banker's rounding and disagrees here:
  // Round(0.5) = 0, Round(2.5) = 2 - both one short of the spec value.
  Result := Floor(X + 0.5);
end;

Проверката при регистрация открива неправилна калкулаторна програма рано

Неправилно оформена програма Type 4 се открива евтино при създаването и скъпо навсякъде другаде, затова RegisterPostScriptFunction не просто съхранява изходния текст: тя пробно оценява програмата веднъж, в средната точка на декларирания /Domain, преди функционалният обект изобщо да бъде записан в документа. Несдвоени блокове { }, непознат оператор, недостатъчен брой елементи в стека или брой изходи, който не съвпада с /Range, провалят пробното изпълнение и повдигат изключение незабавно, като стекът на извикванията сочи към извикването на RegisterPostScriptFunction, а не към артефакт при рендериране, открит по време на проверка на вече доставен файл. Пробата в средната точка не доказва, че програмата е правилна по целия си /Domain — условен клон, който се държи неправилно само близо до единия край на входния диапазон, все още може да премине през една точка на проба — но премахва целия клас програми, които са структурно повредени, а не просто грешни в един ъгъл

Къде градиентите и spot цветовете използват тези функции

Type 2, 3 и 4 рядко се появяват самостоятелно в реален PDF; те се появяват навсякъде, където спецификацията приема ключ /Function, а двата най-чести потребителя са shading и tint transform на spot цветове. Операторът sh за осев или радиален градиент (ISO 32000-1 §8.7.4.5) оценява неговия /Function веднъж за всяка позиция по оста на градиента, което е точно случаят с множество стопове, за който съществува свързването Type 3. Tint transform на цветово пространство Separation или DeviceN е другият чест дом за тези три типа и именно там Type 4 е най-полезен: едно spot мастило обикновено се свежда до крива Type 2 или Type 0, но смесването на няколко мастила DeviceN с реални правила за trapping и overprint често се нуждае от условната логика, която може да изрази само калкулатор на PostScript, както е разгледано в статията за рендериране на spot цветове Separation и DeviceN. RegisterSeparationFunc е съответното извикване от страната на създаването: то приема име на оцветителя, алтернативно цветово пространство и всеки обект, върнат от семейството Register*Function, и свързва този tint transform с ресурс на цветово пространство Separation, който останалата част от страницата може да избере чрез scn/SCN

Заедно, извадени таблици Type 0 и тези три формулно управлявани типа покриват всяка /Function, която PDF може да декларира, а изборът на правилния тип е главно въпрос на това, с какво вече разполагате: таблица за търсене, изчислена другаде, става Type 0, смесване между две крайни точки става Type 2, няколко смесвания, свързани през домейн, стават Type 3, а всичко с действителна условна логика става Type 4. RegisterExponentialFunction, RegisterStitchingFunction и RegisterPostScriptFunction са част от стандартния HotPDF Component за Delphi и C++Builder, наред с останалата му API повърхност за функции и shading според ISO 32000-1