O HotPDF achata a rotação de página de PDF com THotPDF.FlattenLoadedPageRotation: o método embrulha o conteúdo de cada página rotacionada numa transformação cm horária, reescreve cada page box que a página de fato tem, gira a geometria de anotações, matrizes de appearance, destinos explícitos e geometria de estrutura taggada pelo mesmo ângulo, e então define /Rotate como 0. A página fica idêntica num viewer, mas o sistema de coordenadas dela agora é ereto. Isso importa no momento em que uma ferramenta downstream, um print RIP ou o seu próprio código de stamping ignora o /Rotate e coloca coisas no user space cru
O gatilho típico é um scanner ou um app de captura mobile que grava páginas landscape como media portrait com /Rotate 90. Todo viewer as mostra corretamente, então ninguém nota até alguém estampar um número de página no "canto inferior direito" e ele cair de lado ao longo da borda esquerda, ou um passo de imposition que só lê /MediaBox montar um slot portrait para uma página landscape. Achatar soa como um trabalho de matriz de uma linha só. Na prática toca cinco page boxes, três tipos de geometria de anotação, os alvos de link do documento e a structure tree, e cada um deles tem a própria regra na ISO 32000-1
Para que lado o /Rotate gira uma página de PDF?
O /Rotate gira a página no sentido horário para exibição e impressão, em múltiplos de 90 graus (ISO 32000-1 §7.7.3.3, Tabela 30). A 90 graus a borda esquerda do media vira o topo e a borda de cima vira o lado direito, então num device space com y para baixo o mapeamento é X = (y - Bottom) * Scale e Y = (x - Left) * Scale. A 270 graus a borda direita vira o topo. O /Rotate também é um dos apenas quatro atributos de página herdáveis, junto com /Resources, /MediaBox e /CropBox (§7.7.3.4), então um dicionário de página sem /Rotate próprio ainda pode ser girado por um ancestral /Pages. O THotPDF.GetLoadedPageRotation caminha pela cadeia de /Parent e normaliza o resultado para 0–359, que é o valor que você quer, não a chave crua na página
O sentido é fácil de errar de um jeito que sobrevive aos testes, e builds anteriores do HotPDF fizeram exatamente isso. A antiga matriz de página para device trocava os componentes y para 90 e 270, o que produz uma reflexão pela diagonal em vez de uma rotação: a orientação da matriz vira em relação ao caso sem rotação. Os dois ângulos ainda "pareciam girados", o bitmap tem largura e altura trocadas, e um round trip de página para view e de volta retorna o ponto de partida, então checagens de dimensão e testes de round trip todos passam. A única checagem confiável é onde um marcador de canto acaba, comparado pixel por pixel contra um renderer de referência. Como o modelo de viewer, o backend de render SIMD e o mapeamento de highlight tinham copiado a mesma matriz, todos foram corrigidos juntos, e o código de achatamento agora usa a mesma convenção horária do renderer
Como o FlattenLoadedPageRotation reescreve uma página
O FlattenLoadedPageRotation(PageRange, Info) processa toda página em PageRange cuja rotação efetiva é 90, 180 ou 270, e retorna o número de páginas que achatou. Um PageRange vazio significa todas as páginas; caso contrário a string usa a sintaxe usual 1-based '1-3,7', e um número de página fora do intervalo levanta exceção em vez de ser pulado. Os content streams originais nunca são recodificados. O método precede um stream novo contendo q 0 -1 1 0 -Bottom Width+Left cm (para 90 graus) ao /Contents da página, anexa um stream contendo Q, e por fim grava um /Rotate 0 explícito no dicionário de página para que um valor herdado num nó /Pages não possa girar a página uma segunda vez
var
Pdf: THotPDF;
Info: THPDFRotationFlattenInfo;
Flattened: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
begin
// '' = todas as páginas; páginas a 0 graus são varridas mas ficam como estão
Flattened := Pdf.FlattenLoadedPageRotation('', Info);
Writeln(Format('Scanned %d, flattened %d pages', [Info.ScannedPageCount, Info.FlattenedPageCount]));
Writeln(Format('Turned %d annotations, %d destinations, %d tagged geometry entries',
[Info.TransformedAnnotationCount, Info.TransformedDestinationCount,
Info.TransformedStructureGeometryCount]));
if Flattened > 0 then
Pdf.SaveLoadedDocument('scanned-batch-upright.pdf');
end;
finally
Pdf.Free;
end;
end;
O record THPDFRotationFlattenInfo merece ir para o log em vez de ser descartado. O ScannedPageCount é o tamanho da faixa, o FlattenedPageCount iguala o valor de retorno, e os três contadores Transformed... dizem se o documento tinha links, bookmarks ou geometria taggada apontando para as páginas giradas. Um lote em que todo arquivo reporta zero destinos está ok; um arquivo PDF/UA taggado que reporta zero geometria de estrutura quando você esperava bounding boxes de figure é um sinal para inspecioná-lo à mão
Quais page boxes o achatamento reescreve, e em que ordem?
O achatamento reescreve só os boxes que a página já tem, e lê todo box antes de gravar qualquer um. A ordem importa por causa da cadeia de defaults: o GetLoadedPageBox(PageIndex, pbCropBox, ...) retorna a /MediaBox quando a página não tem /CropBox, e /BleedBox, /TrimBox e /ArtBox caem no CropBox por default (§14.11.2). Uma versão anterior lia, transformava e gravava um box por vez. Ela reescrevia a MediaBox primeiro, depois lia a "CropBox", recebia a MediaBox já girada de volta, girava uma segunda vez e gravava uma CropBox que a página nunca teve, o que cortava uma página landscape até virar um quadrado. As regras de herança se dividem do mesmo jeito: MediaBox e CropBox são buscadas ao longo da cadeia de /Parent, enquanto Bleed, Trim e ArtBox contam só se sentam no dicionário de página em si, então um /TrimBox perdido num nó /Pages é tratado como ausente e nunca é copiado para a página
procedure DumpPageGeometry(Pdf: THotPDF; PageIndex: Integer);
var
L, B, R, T: Single;
begin
Writeln('Effective /Rotate: ', Pdf.GetLoadedPageRotation(PageIndex));
if Pdf.GetLoadedPageBox(PageIndex, pbMediaBox, L, B, R, T) then
Writeln(Format('MediaBox [%g %g %g %g]', [L, B, R, T]));
// True mesmo sem chave /TrimBox: o valor cai para CropBox, depois MediaBox
if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
Writeln(Format('TrimBox [%g %g %g %g]', [L, B, R, T]));
// Letter pré-ajustado; GetLoadedPageVisibleBox deixa as saídas intocadas em falha
L := 0; B := 0; R := 612; T := 792;
Pdf.GetLoadedPageVisibleBox(PageIndex, L, B, R, T);
Writeln(Format('Visible [%g %g %g %g]', [L, B, R, T]));
end;
Rode esse helper antes e depois do achatamento e os números se explicam sozinhos. Para uma página de 90 graus com MediaBox [0 0 612 792], a MediaBox achatada vira [0 0 792 612]; todo box reescrito é mapeado pela mesma volta horária, relativo à origem da MediaBox original, então a MediaBox nova sempre começa na origem e os outros boxes mantêm a posição deles dentro dela. O GetLoadedPageVisibleBox retorna o que viewers exibem e impressoras imprimem, a CropBox cortada pela MediaBox e normalizada para Left ser menor que Right, e o renderer, o export SVG, o viewer e o caminho de impressão do HotPDF todos usam esse mesmo box. Quando você precisa do tamanho de página que um humano vê, chame o GetLoadedPageVisibleBox em vez de ler a /MediaBox
Por que anotações quebram quando você só gira o /Rect?
Anotações quebram porque um appearance stream não é desenhado direto no /Rect. Sob a §12.5.5 o viewer primeiro transforma a /BBox do form pela /Matrix dele, depois escala e translada o bounding box desse resultado para dentro do /Rect. Gire só o /Rect e um carimbo de 200 × 40 é espremido num slot de 40 × 200, ilegível e deitado. O FlattenLoadedPageRotation portanto multiplica à direita a volta horária da página na /Matrix de cada appearance (para 90 graus, [0 -1 1 0 0 0] na convenção de row-vector), pelas appearances /N, /R e /D e por todo estado dentro delas. Um appearance stream pode ser compartilhado por várias anotações ou estados, então cada stream é girado exatamente uma vez por chamada. O único caso sem resposta limpa é um stream compartilhado entre páginas com rotações diferentes; ele segue a primeira página que o alcança
Duas regras a mais mantêm form fields e sticky notes no lugar. A entrada /MK /R de um widget (§12.5.6.19) é um ângulo anti-horário, então o ângulo horário da página é subtraído dele, módulo 360; pule isso e a próxima regeneração de appearance desenha o texto do campo na direção errada. Anotações com a flag NoRotate (posição de bit 5, valor 16, §12.5.3) ficam eretas numa página girada e pivotam em volta do canto superior esquerdo do /Rect delas, então o achatamento mantém a largura, a altura e a aparência ereta delas e só move esse canto para onde a volta o põe. Além de anotações, o método também gira /QuadPoints, /Vertices, /L e /InkList, reescreve destinos explícitos que nomeiam a página (pontos /XYZ, retângulos /FitR, e /FitH / /FitV trocados a 90 e 270 graus, §12.3.2.2), e transforma geometria taggada como entradas de /BBox de atributos para elementos de estrutura cujo /Pg é a página
O que o achatamento não cobre?
O achatamento é uma reescrita geométrica dos objetos próprios de uma página, e várias situações ficam fora dele em silêncio em vez de com barulho
- Páginas cuja rotação efetiva já é 0, ou cuja MediaBox falta ou tem largura ou altura zero, são puladas sem erro; compare o valor de retorno com o número de páginas que você esperava mudar
- Form XObjects referenciados dos recursos da página mantêm a própria
/BBoxno espaço de form, porque ocmexterno já os gira; a varredura da structure tree segue só/Ke/Aentão nunca entra nos recursos da página ou nas anotações uma segunda vez - Destinos são encontrados varrendo todo objeto indireto uma vez por página achatada, então um documento grande com centenas de páginas giradas paga essa caminhada em cada uma
- O page renderer do HotPDF não desenha anotações, então uma checagem visual de carimbos girados precisa do
FlattenLoadedAnnotationsprimeiro
// Asse as appearances no conteúdo para o renderer poder mostrá-las,
// depois renderize a página 1 antes e depois de remover o /Rotate dela
Pdf.FlattenLoadedAnnotations('1');
Before := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Pdf.FlattenLoadedPageRotation('1', Info);
After := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Assert((Before.Width = After.Width) and (Before.Height = After.Height));
// Compare aqui os pixels do marcador de canto, não só as dimensões
finally
After.Free;
end;
finally
Before.Free;
end;
Para contexto mais fundo, o lado de anotações desta história continua em sintetizar appearances de anotações antes de achatá-las, o renderer por trás da comparação de antes e depois é coberto em renderizar uma página de PDF carregada para um bitmap, e redaction e costura N-up em PDFs carregados mostra a mesma técnica de anexar content stream de que o prefixo e o sufixo de rotação dependem. O HotPDF, incluindo o FlattenLoadedPageRotation e os leitores de page box, está disponível para Delphi e C++Builder na página do componente HotPDF Delphi PDF