HotPDF сравнява два PDF документа от Delphi чрез THPDFDocComparison, който обхожда графа от обекти на двата файла, започвайки от каталога навън, и при поискване също рендира всяка двойка страници и измерва различаващите се пиксели. Резултатът е JSON отчет, назоваващ всяка открита разлика, изразходвания бюджет и дали сравнението е завършило докрай. И двата прохода имат значение, защото структурната разлика и визуалната разлика отговарят на различни въпроси
Въпросът зад тази функция обикновено е въпрос за издаване на версия. Двигателят за отчети получава промяна, изходът се регенерира, и някой трябва да реши дали нещо се е променило. Отварянето на двата файла едно до друго се справя до около три страници, преди вниманието да отпадне. Сравняването на суровите байтове се проваля незабавно, тъй като две изпълнения на един и същ генератор произвеждат различни байтове по причини, които нямат нищо общо с това какво вижда четецът
Защо PDF файловете могат да са различни по байтове, но визуално идентични?
Два независимо генерирани PDF файла, които се отпечатват идентично, редовно се различават по байтовете си, а причините са структурни, а не козметични. Номерата на обектите се присвояват в реда, в който обектите се случва да бъдат записани. Подмножествата на шрифтове разпределят CID-ове в реда, в който глифите се срещат за първи път, така че подмножество, изградено при леко различно обхождане, произвежда различни байтове на потока от съдържание за един и същ видим текст. Отместванията в таблицата с кръстосани препратки се изместват при всяка промяна на дължината нагоре по веригата
Затова номерата на обектите не могат да се използват като идентичност между документи. Вместо това HotPDF изгражда всяка снимка чрез обхождане от каталога, разширявайки речниците по байтовия ред на техните ключове, а масивите — по индекс, така че всеки обект се назовава по пътя, който води до него. Обекти, до които обхождането не може да достигне от корена, попадат в синтетичен път $Unreachable[...], носещ номера на обекта и поколението, което пази осиротялото съдържание видимо в отчета, вместо тихомълком отсъстващо
Потоците не се сравняват чрез копиране. Всеки поток допринася с инкрементален SHA-256 подпис, изчислен, докато оригиналната позиция в потока се възстановява впоследствие, така че сравняването на два файла от по сто мегабайта не означава двойно материализиране на двеста мегабайта
Подравняване на страници, когато един документ има вмъкване
Сравняването на страница 1 със страница 1, страница 2 със страница 2 и така нататък е правилно само когато нищо не е било вмъкнато. Вмъкнете корична страница и наивно сравнение отчита всяка страница като променена, което е технически вярно и оперативно безполезно
HotPDF подравнява страниците, преди да ги сравни. Той изгражда подпис за всяка страница от извлечим текст, преминава към структурен подпис за страници без текст, и след това изчислява най-дългата нарастваща подпоследователност над съпоставените индекси в целта. Страниците вътре в тази подпоследователност са тези, които просто са се изместили; страниците извън нея са истински премествания. Това разграничение е това, което прави разликата на ръководство от 400 страници четима, защото отчетът казва, че е вмъкната една страница, вместо че са се променили четиристотин страници
Изпълнение на структурно сравнение
Най-простото извикване приема два заредени документа и режим. cmStructural извършва обхождането на графа от обекти, cmRenderedImage извършва сравнението на пиксели, cmFull прави и двете, а по-леките режими cmPageCount, cmPageText и cmObjectCount съществуват за евтини проверки за дим:
uses
HPDFDoc, HPDFDocCompare;
var
DocA, DocB: THotPDF;
Report: AnsiString;
begin
DocA := THotPDF.Create(nil);
DocB := THotPDF.Create(nil);
try
if (DocA.LoadFromFile('baseline.pdf') <= 0) or
(DocB.LoadFromFile('candidate.pdf') <= 0) then
Exit;
Report := THPDFDocComparison.Compare(DocA, DocB, cmStructural);
with TFileStream.Create('diff.json', fmCreate) do
try
WriteBuffer(Report[1], Length(Report));
finally
Free;
end;
finally
DocB.Free;
DocA.Free;
end;
end;
Отчетът разграничава три състояния, които булева стойност не може. identical казва дали има някаква разлика, comparisonComplete казва дали обхождането е приключило, а comparisonBudget назовава лимита, който го е спрял, ако такъв е сработил. Сравнение, което изчерпва бюджет, отчита едновременно comparisonComplete=false и identical=false, защото прекъснато обхождане няма основание да твърди равенство. Всяка автоматизация, която чете само identical, рано или късно ще третира спиране заради бюджет като реална разлика, затова четете и трите
Какви лимити държат обхождането ограничено?
Стойностите по подразбиране в THPDFStructuralCompareLimits.Default са оразмерени за реални документи, а не за враждебни такива, и всеки семантично значим бюджет има свой собствен таван: 250 000 обекта, 2 000 000 ребра, дълбочина 128, 10 000 отчетени разлики, 64 MB на поток и общо 512 MB байтове потоци, 1 MB на стойност и 4096 байта на път. Повишавайте ги съзнателно, когато познавате корпуса си, и ги намалявайте при сравняване на файлове, пристигнали отвън:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // бърз провал в CI
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // подразбирането е 72
Options.ColorTolerance := 2; // игнорирай шум от закръгляне на 1-2 нива
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // запиши наслагващи изображения за преглед
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
Рендиращият проход оценява броя пиксели от размерите на страницата и заявения DPI, преди изобщо да бъде разпределена растерна карта, и проверява отново действителната растерна карта впоследствие, така че деформирана геометрия на страницата не може да се промъкне покрай бюджета, лъжейки за размера си. Повишаването на DPI повишава точността и цената квадратично: 150 DPI е четири пъти повече пиксели от 72, а таваните за страница и общо съществуват именно защото пакетна задача при 300 DPI иначе ще се разпредели право в неприятности
Колко сходно е достатъчно сходно?
Две страници се броят за сходни само когато и двете условия важат: съотношението на променените пиксели е на или под MaxChangedPixelRatio, а сходството е на или над MinimumSimilarity. Два прага вместо един, защото шепа катастрофално грешни пиксели и широк порой от дребни цветови отмествания са различни провали, и всеки от тях сам по себе си може да бъде приемлив в един работен процес и дисквалифициращ в друг. Тестовете за прагове използват незакръглени стойности; шестте знака след десетичната запетая в JSON съществуват, за да поддържат отчетите стабилни и сравними, не за да дефинират сравнението
Променените пиксели се групират в региони, използвайки плочки с фиксиран размер като възли с четиристранна съседност, вместо заливане пиксел по пиксел. Това държи паметта ограничена, а списъка с региони — стабилен между изпълненията. Съкращаването на запазените детайли за региони засяга само списъка, не и отчетения брой региони, така че страница с повече променени региони от MaxChangedRegions все пак отчита колко на брой са били
Едно поведение си струва да се посочи ясно, защото обръща обичайния инстинкт. Провалите на рендиращото устройство, провалите при разпределяне на памет и провалите при наслагване никога не се поглъщат мълчаливо. Всичко от този вид се записва като renderError или renderBudget и налага renderComparisonComplete=false, защото страница, която не е успяла да се рендира, е страница, която никой не е сравнил, а отчитането ѝ като идентична е по-лошо, отколкото да не се отчете нищо
Къде принадлежи всеки режим в конвейер
Структурното сравнение отговаря какво се е променило и е правилният избор по подразбиране за регресионни комплекти: то назовава пътя, индекса на страницата и засегнатите номера на обекти, така че провал сочи към кода, който го е причинил. Рендираното сравнение отговаря дали някой ще забележи — това е въпросът за одобрения и за проверка дали проход на оптимизация наистина е бил без загуби
Двете се съчетават добре. Изпълнявайте cmStructural при всяка компилация и го оставете да се провали шумно при неочаквани промени на ниво обект; изпълнявайте cmFull с топлинни карти преди издаване, когато има наличен човек, който да разгледа наслагванията. За конвейери, които вече извеждат маркиране на страници по други причини, текстовият изход, описан в експортиране на PDF страници в SVG, дава трети, четим за хора изглед, а автоматизираните проверки в автоматизация на preflight отчети покриват въпроси за съответствие, на които нито един от двата режима на сравнение не е предназначен да отговаря
Сравнението, preflight и рендирането споделят един и същ обектен модел на зареден документ, така че едно преминаване по файл може да захрани и трите. Пълният списък с функции за Delphi и C++Builder е на страницата на HotPDF Delphi PDF компонента