Stream, маркиран /Predictor 12, не означава, че всеки ред използва PNG filter 2. HotPDF, нативният VCL PDF компонент за Delphi и C++Builder, третира стойностите на predictor от 10 до 15 като едно семейство: истинският filter tag, от 0 до 4, е първият байт на всеки кодиран ред, а HPDFDecodePredictor чете и валидира този tag ред по ред. Тази разлика е формата на почти всеки бъг в този ъгъл на PDF, защото нищо не сигнализира грешка, когато я объркате. Веригата от filter-и се изпълнява, растерът е с очаквания размер, а изображението излиза като диагонален шум или градиент, отдалечаващ се все повече с всеки ред за сканиране. Петте числа в /DecodeParms (ISO 32000-1 §7.4.4) най-вече променят значението на байтовете, а не тяхната дължина, така че грешно число произвежда правдоподобен боклук вместо грешка
Защо /Predictor 12 не означава PNG filter 2 на всеки ред?
Защото номерът на predictor казва само "използва се PNG предсказване", не кой filter. PNG енкодери избират filter за всеки ред за сканиране, а PDF filter-ът наследява това, така че стойностите на predictor 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) и 15 (Optimum) всички се декодират идентично: водещият tag байт на всеки ред е това, на което декодерът трябва да се подчини. Следствието за оформлението има значение колкото семантиката. Всеки кодиран ред е дълъг 1 + RowBytes байта, входът затова надвишава изхода точно с броя редове, а stream, чиято дължина не е точно кратно на RowBytes + 1, е осечен по дефиниция. HotPDF проверява тази граница преди да докосне байт, отхвърля всеки tag над 4 с Invalid PNG predictor row tag, и чете предишния ред директно от единствения изходен буфер, вместо да материализира двумерен масив от редове. Filter-и 1 и 3 се пресягат назад BytesPerPixel в рамките на текущия ред, filter 2 чете направо нагоре, filter 4 изпълнява избора на Paeth над ляво, горе и горе-ляво — и всичките четири работят върху вече реконструиран изход, поради което горният ред трябва да бъде декодираният ред, никога филтрираният вход
uses
HPDFPredictor;
var
Filtered, Raster: AnsiString;
ErrorText: string;
begin
// /DecodeParms << /Predictor 12 /Colors 3 /BitsPerComponent 8 /Columns 1024 >>
if HPDFTryDecodePredictor(Filtered, 12, 3, 8, 1024,
Int64(1024) * 3 * 8192, Raster, ErrorText) then
ConsumeRaster(Raster)
else
LogStreamDefect('predictor', ErrorText); // no exception, no partial raster
end;
Аргументът MaxOutputBytes не е украса. Стъпката на predictor е декомпресионна стъпка под прикритие, а враждебна или просто счупена стойност на /Columns превръща няколко килобайта вход в заявка за разпределяне на многогигабайтова памет. HotPDF изчислява бита на ред, байтове на ред и общия размер на растера в Int64 първо, отказва геометрия, която прелива, и спазва зададения от извикващия таван. Подайте реална граница, изведена от речника на изображението, и режимът на провал става логвано съобщение вместо диалог за недостиг на памет на машина на клиент
Защо TIFF Predictor 2 поврежда 4-битови изображения?
Защото Predictor 2 е хоризонтално диференциране по sample, не по байт, а при 1, 2 или 4 бита на компонент няколко sample-а споделят байт. Обичайната реализация добавя байт N-Colors към байт N, което се оказва правилно при 8 бита на компонент и тихо грешно навсякъде другаде. 8-битово RGB сканиране се декодира перфектно, после същият код унищожава 4-битово индексирано изображение първия път, когато такова се появи в production
Правилната аритметика работи вътре в битовото поле. HotPDF обхожда sample-ите от индекс Colors до Colors * Columns - 1, извлича sample-а и неговия ляв съсед от същия компонент с маска (1 shl BitsPerComponent) - 1 при подходящото изместване, ги събира по модул тази маска, и записва резултата обратно, без да смущава другите sample-и, пакетирани в същия байт. Опашката също има значение: ред се допълва до байтова граница, така че допълващите битове след последния sample трябва да оцелеят недокоснати, вместо да бъдат сгънати в аритметиката. При 16 бита на компонент всеки sample е big-endian двойка байтове, а събирането прелива при $FFFF през двойката, вместо да пренася между байтовете независимо; при 8 бита простата байтова рекурентност е правилна, стъпвайки по Colors, така че червеното се натрупва спрямо червено, а алфа спрямо алфа. Във всеки вариант първият пиксел на ред е буквален, никога разлика, а рекурентността се рестартира при всяка граница на ред — TIFF предсказването никога не чете реда отгоре, което е цялата разлика между него и семейството PNG
Какво всъщност контролира EarlyChange в LZWDecode?
Той контролира кога четецът разширява размера на кода си с един бит, а изоставането с един код поврежда всичко, което следва. HotPDF изразява правилото като единствен инвариант: след добавяне на речников запис, следващото четене се разширява, когато NextCode достигне (1 shl CodeSize) - Ord(EarlyChange). При /EarlyChange 1, стойността по подразбиране в ISO 32000-1 §7.4.4, превключването се случва един код по-рано; при /EarlyChange 0 се случва точно на границата. И двете се срещат в реални файлове, и нищо в bitstream-а не ви казва коя е използвал енкодерът. Останалата част от state machine-а трябва да се движи в синхрон: clear код нулира размера на кода, битовата маска, следващия свободен код и съхранението на фрази заедно, а end-of-information кодът се чете при каквато и да е текущата ширина в този момент, не при първоначалните 9 бита. HotPDF стартира на InitialCodeSize 9, ограничава размера на кода до 12, а речника до 4096 записа, и по подразбиране задава FillOrder на foTop, защото PDF пакетира кодовете със старшия бит първи — foBottom съществува за stream-ове в TIFF стил, които не го правят
uses
HPDFLZW;
var
Decoder: TPDFLZWDecompressor;
Parms: TPDFLZWParms;
Plain: AnsiString;
begin
Decoder := TPDFLZWDecompressor.Create;
try
Decoder.EarlyChange := True; // /EarlyChange 1 is the PDF default
Decoder.FillOrder := foTop; // high-order bit first
Decoder.MaxOutputBytes := 256 * 1024 * 1024;
Decoder.RequireInitialClear := False;
Decoder.RequireEndOfInformation := False;
Parms.Predictor := 12;
Parms.Colors := 3;
Parms.BitsPerComponent := 8;
Parms.Columns := 1024;
Parms.ExpandedTo8Bit := False;
Parms.ColorSpace := 'DeviceRGB';
if Decoder.TryDecompress(RawStreamBytes, Parms, Plain) then
LogDecodeStats(Decoder.PeakCodeSize, Decoder.DictionaryAdds,
Decoder.KwKwKExpansions, Decoder.OutputBytes)
else
LogStreamDefect('lzw', Decoder.LastError);
finally
Decoder.Free;
end;
end;
Статистиките съществуват за triage, не за суета. Когато файл се декодира до правилната дължина, но грешните пиксели, PeakCodeSize и DictionaryAdds веднага ви казват дали четецът изобщо се е разширил там, където енкодерът го е направил. Преобърнете EarlyChange, декодирайте отново, сравнете двете: ако числата се движат, имате отговора си в едно изпълнение, вместо да стъпвате през bit reader
Разклонението KwKwK и кога stream просто трябва да се провали
Единственият легален случай, който изглежда нелегален, е Code = NextCode, а HotPDF го обработва, като конструира записа, преди да го излъчи. Енкодер може да излъчи кода за фраза, която дефинира в същата стъпка, което се случва винаги когато входът съдържа шаблон от вида K w K w K; декодерът не може да намери този код, защото той още не съществува, така че трябва да построи Previous + First(Previous), да го добави като нов запис, и да излъчи записа, който току-що е създал. HotPDF брои тях в KwKwKExpansions и кръстосано проверява, че кодът, който е добавил, е кодът, за който е бил помолен. Всичко над NextCode е повреда, и там декодер трябва да спре, а не да импровизира: HotPDF вдига грешка при бъдещ код, при речников префикс, сочещ извън арената на фрази, при пълен речник, и при първи код, който не е буквален. Два превключвателя за строгост са умишлено изключени по подразбиране, RequireInitialClear и RequireEndOfInformation, защото доста production PDF-и пропускат водещия clear код или свършват данните без terminator. Включете ги при валидиране на собствения си изход, оставете ги изключени при консумиране на файлове от дивото
Къде /DecodeParms всъщност се чете на страната на заредения документ
HotPDF разрешава /DecodeParms или неговата абревиатура /DP върху речника на image stream, приема или речник, или масив, и взима последния елемент, когато е масив, после пренася Predictor, Colors, BitsPerComponent, Columns и EarlyChange в пътя за растера. Случаят с масив е този, който хората забравят: stream, филтриран от [/ASCII85Decode /FlateDecode], носи паралелен масив от параметри, а настройките на predictor принадлежат на последния filter, не на първия
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned.pdf', '') > 0 then
for I := 0 to Pdf.GetLoadedImageCount - 1 do
if Pdf.GetLoadedImageInfo(I, Info) then
begin
Bmp := Pdf.ExtractLoadedImage(I); // nil when the raster is unusable
if Bmp <> nil then
try
Bmp.SaveToFile(Format('image-%d.bmp', [I]));
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Един исторически дефект по този път си струва да се назове, защото класът бъгове се повтаря. Старата рутина за Flate с параметри създаваше decompression stream и после копираше от оригиналния компресиран вход, така че стъпката на predictor получаваше компресирани байтове и добросъвестно ги "де-предсказваше": винаги грешно, никога сигнализирано. Текущият код чете само от декодера, преди да подаде резултата на споделения predictor, и отхвърля растер, по-къс от изчисления размер, вместо да пада обратно на все още компресираните байтове — резервен вариант, който преди превръщаше провал при декодиране в повреден bitmap. Същата реализация на predictor сега обслужва и cross-reference streams, което е полезна съгласуваност, ако работите и с object streams и incremental updates, а заобикалящата машинерия за извличане е разгледана в съпътстващата статия за извличане на заредени изображения и техните decode filter-и. Изображения, пристигащи като DCTDecode или JPXDecode, никога не достигат predictor-а; те носят собствен компресиран пикселен модел
Пропускателна способност: непрекъсната арена от фрази срещу низове по запис
Замяната на речника с низове по запис с непрекъсната арена от фрази измери около 1.61 пъти по-бързо върху патологичен вход: 1558 MiB/s срещу 969 MiB/s върху benchmark, чиято единствена най-дълга фраза достига 7,370,880 байта. Формата на този вход обяснява разликата, защото класическите реализации избират един от два лоши компромиса. Речник от стойности AnsiString разпределя и копира свеж низ за всеки от до 4096 записа, всеки нов запис копирайки родителя си изцяло; стек с префикс/суфикс избягва тази памет изцяло, но реконструира всяка фраза, обхождайки веригата назад по един байт наведнъж и обръщайки я, което е добре за обикновен текст и болезнено, когато една фраза достигне мегабайти. HotPDF добавя всяка фраза непрекъснато в геометрично растяща арена, индексира записите по offset и дължина, и излъчва фраза с едно единствено Move в изходния буфер. Честната цена е памет: арена, държаща всяка фраза изцяло, е ограничена от сумата на всички дължини на фрази, а не от броя записи, което е точно причината MaxOutputBytes да съществува както на decompressor-а, така и на predictor-а. Изведете тази граница от това, което речникът на изображението твърди, че растерът трябва да бъде, и лъжлив stream се проваля бързо
LZW decompressor-ът, споделеният predictor и пътят за извличане на заредени изображения, показани тук, се доставят като част от стандартния HotPDF Component за Delphi и C++Builder, с пълния справочник за filter-и и DecodeParms на продуктовата страница