O PDFlibPas versão 3.539.23 decodifica arquivos JBIG2 standalone que usam a organização random-access do Anexo D.2 da ITU-T T.88, em que todo segment header vem primeiro e os dados dos segmentos seguem na mesma ordem. O decoder Pascal nativo em PDFlibJBIG2.pas indexa os offsets dos headers até o header end-of-file obrigatório, confere que os números de segmento crescem e que os tamanhos de dados declarados somam exatamente os bytes que restam, e então decodifica cada corpo na ordem dos headers sem copiar nem reordenar os dados comprimidos. Antes desta release o mesmo arquivo levantava um erro seco de "random-access organisation is not supported" no momento em que os flags do header eram lidos
Arquivos JBIG2 random-access são raros, e é exatamente por isso que eles machucam quando aparecem. Eles saem de pipelines de arquivamento e sistemas de imageamento de documentos que querem que o leitor veja todo segment header, e portanto toda dependência de página e de dicionário, antes de tocar num único byte comprimido. Uma aplicação Delphi que converte arquivos escaneados em lote para PDF costuma encontrar um desses no meio de um job, depois que centenas de arquivos sequenciais passaram limpamente, e um decoder que para morto num arquivo bem-formado é só marginalmente melhor que um que renderiza lixo. A mesma linha de release tinha acabado de ensinar ao decoder tabelas Huffman customizadas e códigos de prefixo canônicos do JBIG2, então random access era a última lacuna de organização restante nos limites de capacidade documentados do decoder
O que é a organização random-access do JBIG2?
A organização random-access é uma das três formas como o Anexo D do T.88 permite dispor os mesmos segmentos: sequential (D.1) intercala cada header com os dados dele, random-access (D.2) põe todos os headers primeiro e todos os dados depois, e embedded (D.3) é a forma sem header usada dentro de outros containers como o PDF. Um arquivo .jb2 standalone começa com o identificador de oito bytes 97 4A 42 32 0D 0A 1A 0A, seguido de um byte de flags e, quando a contagem de páginas é conhecida, uma contagem de páginas de quatro bytes. O bit 0 do byte de flags seleciona a organização, com 1 significando sequential e 0 significando random-access; o bit 1 setado significa que o número de páginas é desconhecido e a contagem de quatro bytes está ausente. O PDFlibPas lê isso em checkHeader e setFileHeaderFlags, e os bits reservados de 2 a 7 são tolerados em vez de rejeitados
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = contagem de páginas omitida
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// stream PDF: sem header de arquivo, organização embedded, uma página
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
O PDF em si nunca carrega esse layout. Um stream de imagem JBIG2Decode, descrito na ISO 32000-1 §7.4.7, guarda apenas os segmentos de página em organização embedded, com os symbol dictionaries compartilhados movidos para um stream JBIG2Globals separado e sem header de arquivo, nem segmentos end-of-page ou end-of-file. Quando o decodeJBIG2 não encontra o identificador de oito bytes, ele assume exatamente isso e força decodificação sequential de página única. O export de imagem JBIG2 nativo vai no outro sentido e embrulha os segmentos do PDF num arquivo standalone cujo byte de flags é $03, sequential com contagem de páginas desconhecida, seguido de um header end-of-file acrescentado no final. Então o trabalho de random-access toca um único caminho: arquivos standalone entregues direto ao TPLJBIG2Decoder, tipicamente antes de serem convertidos ou recomprimidos para PDF, o trabalho que os backends de encoder JBIG2 do PDFlibPas fazem do lado do output
Por que um arquivo random-access não pode ser lido na ordem do arquivo?
Um arquivo random-access não pode ser lido na ordem do arquivo porque nada no byte stream marca onde o bloco de headers para e o bloco de dados começa, exceto o próprio header do segmento end-of-file. Headers de segmento JBIG2 têm tamanho variável: a contagem de segmentos referidos pode ser uma forma curta de três bits ou uma forma longa com um bitmap de retenção, os números de segmentos referidos ocupam um, dois ou quatro bytes dependendo do número do próprio segmento, e o campo de associação de página é de um ou quatro bytes. Um leitor sequential ingênuo parseia o primeiro header, lê o tamanho de dados dele e então trata os primeiros bytes do segundo header como os dados daquele segmento. O decoder só percebe que errou muito depois, e é por isso que o código antigo recusava a organização de cara em vez de tentar
Como o PDFlibPas indexa os segment headers random-access?
O PDFlibPas indexa os headers random-access numa única pré-varredura, o IndexRandomHeaders, que parseia todo header, registra só o byte offset dele e para no primeiro header end-of-file (segmento tipo 51). Cada header é parseado por completo e descartado, então o índice é um array de inteiros em vez de uma lista de objetos, e a pré-varredura acumula os tamanhos de dados declarados conforme avança. Quando a varredura termina, o leitor está posicionado no primeiro byte dos dados do primeiro segmento, e essa posição vira o NextBodyOffset
// IndexRandomHeaders, local a TJBIG2StreamDecoder.readSegments
while not reader.isFinished do
begin
Offset := reader.bytePointer;
Header := TSegmentHeader.Create;
try
readSegmentHeader(Header);
if reader.BufferOverrun then
raise EJBIG2DecodeError.CreateFmt(
'JBIG2 truncated random-access header at byte %d', [Offset]);
if (HeaderCount > 0) and
(Cardinal(Header.getSegmentNumber) <= Cardinal(PreviousNumber)) then
raise EJBIG2DecodeError.Create('JBIG2 random-access segment numbers must increase');
PreviousNumber := Header.getSegmentNumber;
Count := Header.getSegmentDataLength;
if Count < 0 then
raise EJBIG2DecodeError.Create('JBIG2 unknown or oversized segment length is not supported');
Inc(TotalLength, Count); // acumulador Int64
HeaderOffsets[HeaderCount] := Offset; // cresce em chunks
Inc(HeaderCount);
if Header.getSegmentType = JBIG2_END_OF_FILE then
begin
if Count <> 0 then
raise EJBIG2DecodeError.Create('JBIG2 invalid end segment length');
FoundEnd := True;
Break;
end;
finally
Header.Free;
end;
end;
if not FoundEnd then
raise EJBIG2DecodeError.Create('JBIG2 random-access file is missing its end-of-file header');
if TotalLength > Length(reader.Data) - reader.bytePointer then
raise EJBIG2DecodeError.Create('JBIG2 truncated random-access segment data');
if TotalLength < Length(reader.Data) - reader.bytePointer then
raise EJBIG2DecodeError.Create('JBIG2 trailing random-access data');
NextBodyOffset := reader.bytePointer;
Toda checagem naquele loop existe porque um arquivo random-access tem menos redundância que um sequential. Os números de segmento precisam crescer estritamente, comparados como valores unsigned, porque dois headers reivindicando o mesmo número tornam ambíguo qual corpo a lista de referidos de uma região posterior significa. O campo de tamanho de dados é lido pelo handleSegmentDataLength, que mapeia qualquer valor com o bit mais alto setado, incluindo o marcador de "unknown length" 0xFFFFFFFF, para -1; no layout random-access não há outro jeito de descobrir onde o próximo corpo começa, então o PDFlibPas rejeita esse tamanho imediatamente em vez de varrer procurando um marcador de fim. O total precisa casar com os bytes restantes exatamente nas duas direções, e um único byte extra depois do último corpo falha com "trailing random-access data". O rigor é deliberado: nesse layout uma discrepância de tamanho significa que todo corpo depois do ponto do erro está deslocado, e um decoder que dá de ombros para um byte solto não tem como saber se ele é padding inofensivo ou o primeiro sintoma de dados desalinhados
Por que o último segmento end-of-page sumia?
O último segmento end-of-page sumia porque a primeira versão do loop de decodificação mantinha o teste de término sequential, o while not reader.isFinished, e no layout random-access o stream de dados acaba antes do índice de headers. Segmentos end-of-page (tipo 49) e end-of-file carregam zero bytes de dados, e normalmente são os últimos headers do arquivo. Depois que o corpo da última região é consumido, o leitor fica exatamente no fim do buffer, então o loop sai e esses segmentos de tamanho zero nunca são despachados, deixando a página inacabada. A correção faz o loop random-access contar headers em vez de bytes. Cada iteração salta o leitor para o próximo header indexado, reseta o bitPointer para 7 porque o corpo anterior pode ter terminado no meio de um byte, re-parseia esse header, então move o bytePointer para o NextBodyOffset e o avança para depois do corpo. Os handlers de segmento existentes, as checagens de segmentos referidos e os diagnósticos de Context rodam inalterados, e uma mensagem de erro ainda reporta o byte offset original do header, não a posição do corpo
// TJBIG2StreamDecoder.readSegments, loop principal
if randomAccessOrganisation then
IndexRandomHeaders;
while (randomAccessOrganisation and (HeaderIndex < HeaderCount)) or
((not randomAccessOrganisation) and (not reader.isFinished)) do
begin
if randomAccessOrganisation then
begin
reader.bytePointer := HeaderOffsets[HeaderIndex];
reader.bitPointer := 7; // realinha depois de um byte parcial
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // usado no contexto de erro
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // salta para os dados deste segmento
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... despacha para o handler de segmento existente, então busca DataEnd
end;
O que a validação random-access realmente prova?
A validação prova que os bytes reorganizados decodificam para os mesmos pixels que os originais sequential, e prova que input random-access malformado falha limpatamente; ela não prova cobertura de arquivos random-access vindos de encoders arbitrários. A regressão Pascal compartilhada usa um arquivo sintético de 235 bytes construído sobre um fixture de tabela customizada que precisa decodificar para uma fileira de 7 por 1 pixels pretos, tanto com a contagem de páginas conhecida quanto com o campo de contagem removido, e depois alimenta o decoder com todo prefixo truncado desse arquivo, um número de segmento duplicado, um byte final extra e um tamanho de dados desconhecido, asserindo a cada vez que o LoadFromByteArray retorna False e deixa Width e Height em zero. O caso de imagem real é uma imagem de refinement de tabela customizada de 500 por 473 cujos segmentos foram reorganizados em layout random-access com todo header original e byte comprimido preservados; o SHA-256 dela casa exatamente com o baseline sequential revisado. Esse arquivo é um derivado produzido por uma transformação de organização, não um documento random-access natural achado na natureza, e nenhuma amostra natural dessas estava disponível. As suítes passaram com 1.598 testes no Delphi Win32, 42 na suíte de imagens do Delphi Win64, 48 no FPC Win32 e 46 no FPC Win64, junto com os três casos de pixel sequential existentes
Carregando um arquivo .jb2 random-access e seus limites
O código da aplicação não muda: o TPLJBIG2Decoder.LoadFromByteArray detecta o header de arquivo e a organização por conta própria, retorna False em qualquer input rejeitado com o motivo em LastError, e expõe a página decodificada por meio de Width, Height e GetScanline, que retorna um byte por pixel
uses
SysUtils, Classes, PDFlibJBIG2;
function ReadJb2(const FileName: string): TJBIG2ByteArray;
var
FS: TFileStream;
begin
FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
SetLength(Result, FS.Size);
if Length(Result) > 0 then
FS.ReadBuffer(Result[0], Length(Result));
finally
FS.Free;
end;
end;
function CountBlackPixels(const FileName: string): Integer;
var
Decoder: TPLJBIG2Decoder;
Row: TJBIG2ByteArray;
X, Y: Integer;
begin
Result := 0;
Decoder := TPLJBIG2Decoder.Create;
try
// Arquivos standalone sequential e random-access usam a mesma chamada
if not Decoder.LoadFromByteArray(ReadJb2(FileName)) then
raise Exception.Create('JBIG2 rejected: ' + Decoder.LastError);
for Y := 0 to Decoder.Height - 1 do
if Decoder.GetScanline(Y, Row) then
for X := 0 to Decoder.Width - 1 do
if Row[X] = 1 then
Inc(Result);
finally
Decoder.Free;
end;
end;
Vale explicitar os limites sem rodeio. Suporte a random-access é um recurso de organização de arquivo, não uma API de páginas aleatórias: o TPLJBIG2Decoder ainda retorna o bitmap da primeira página, e não existe chamada para pegar a página 7 de um arquivo de 40 páginas nem para decodificar páginas lazy. Segmentos com tamanho de dados desconhecido são rejeitados em arquivos random-access, e os limites existentes de comprimento de prefixo Huffman customizado e de contagem de entradas de tabela permanecem inalterados. Esses limites são estreitos o bastante para que uma aplicação Delphi encaminhe os casos rejeitados para outro lugar via LastError, e o resto do pipeline de imagens, da extração de imagens de PDF à codificação JBIG2, está coberto na página de produto da biblioteca de PDF em Delphi PDFlibPas