Uma marca de redação desenhada sem uma cor /IC pode sair do passo de aplicação como uma lacuna transparente ou um contorno vazio, enquanto o comentário do revisor sobre ela continua no arquivo. O PDFlibPas aplica anotações Redact com o ApplyRedactions: o conteúdo cobrido vai primeiro, o overlay vem do /RO ou do /IC com preto como fallback, e o dicionário da marca é esvaziado
Marcar é reversível, aplicar não é
A ISO 32000-1 §12.5.6.23 divide a redação em identificação de conteúdo e remoção de conteúdo. Durante a identificação, uma anotação Redact é só uma proposta que os usuários ainda podem ver, mover e redefinir, e é isso que torna uma rodada de revisão possível. A remoção fica por conta da aplicação, com exigências firmes: tudo o que a anotação identifica vai embora, a anotação vai embora, e a parte coberta de qualquer imagem tem os dados destruídos, porque um clipping path ou image mask sobre os pixels não se qualifica. As chaves estão listadas na Table 192 da ISO 32000-1 (Table 195 na ISO 32000-2), e a maioria delas descreve como a página deve ficar depois da remoção em vez do que remover
| Chave | Papel na spec | O que o ApplyRedactions faz com ela |
|---|---|---|
QuadPoints | Região a remover como n quadriláteros; substitui o Rect como região quando presente | Limpa a caixa delimitadora de cada quadrilátero separadamente; um array cujo comprimento não é múltiplo de 8 cai de volta para o Rect |
Rect | Região a remover quando QuadPoints está ausente | Usado com os cantos invertidos normalizados |
IC | Preenchimento DeviceRGB depois da remoção; ausente significa transparente; ignorado quando RO existe | Mantido quando presente, adicionado como preto quando ausente |
RO | Overlay de Form XObject; tem precedência sobre IC, OverlayText, DA e Q | Instalado como a aparência normal e achatado na página |
OverlayText, Repeat | Rótulo desenhado sobre a região, opcionalmente em mosaico | Desenhado no overlay gerado, em mosaico quando Repeat é true |
DA, Q | Fonte, tamanho e cor do rótulo; justificação | Tamanho e cor lidos do DA, alinhamento do Q |
Criando marcas com AddRedactAnnotation
O TPDFlib.AddRedactAnnotation(Left, Top, Width, Height, FillR, FillG, FillB, OverlayText, RepeatOverlay) adiciona uma marca à página selecionada e devolve a nova contagem de anotações da página, que também é o índice base 1 da marca, ou 0 quando nada foi adicionado. As coordenadas seguem a origem e as unidades atuais, então o exemplo troca para a origem superior esquerda, onde o Top é a borda de cima. A chamada grava o /Rect, um /IC a partir dos componentes de preenchimento e um /DA fixo de /F0 10 Tf 1 1 1 rg, texto branco de 10 pontos; um OverlayText não vazio adiciona /OverlayText, /Repeat e /Q 1 para um rótulo centralizado. Anotações Redact são um recurso do PDF 1.7, então um header mais antigo é elevado a 1.7. Nenhum appearance stream é gravado ainda; a marca pendente fica com a cara que a aplicação do revisor a desenhar. Com o rótulo fixo em branco, pareie o OverlayText com um preenchimento escuro, porque em amarelo pálido o rótulo desaparece. Marcas podem ser criadas em toda edição do PDFlibPas
uses
System.SysUtils, PDFlibrary;
procedure MarkForReview(const SourceFile, ReviewFile: string);
var
Pdf: TPDFlib;
Index: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile(SourceFile, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s', [SourceFile]);
Pdf.SetOrigin(1); // origem superior esquerda: Top é a borda de cima
Pdf.SetMeasurementUnits(0); // pontos
if Pdf.SelectPage(1) <> 1 then
raise Exception.Create('Page 1 not found');
// Nome da testemunha: preenchimento preto, rótulo branco centralizado
Index := Pdf.AddRedactAnnotation(72, 180, 220, 14, 0, 0, 0, 'REDACTED');
if Index = 0 then
raise Exception.Create('Mark was not added');
// Bloco de conta: repetir o código de isenção em mosaico numa caixa mais alta
Pdf.AddRedactAnnotation(72, 410, 300, 40, 0, 0, 0, '(b)(6)', 1);
// Só marcas; cada byte do conteúdo original continua presente
Pdf.SaveToFile(ReviewFile);
finally
Pdf.Free;
end;
end;
QuadPoints decidem a área de remoção; respostas mantêm o texto delas
Quando uma marca carrega /QuadPoints, aqueles quadriláteros decidem o que o ApplyRedactions remove, e o Rect só os delimita para exibição. Marcas feitas com ferramentas de seleção de texto costumam ter um quadrilátero por linha selecionada, então redimensionar tal marca com o SetAnnotRect move a moldura dela na tela e deixa a área de remoção onde estava; o SetAnnotQuadPoints é a chamada que move a região, e o GetAnnotQuadCount devolve 0 para uma marca que depende só do Rect. O texto anexado a uma marca segue outra regra. Ferramentas de revisão guardam a nota no /Contents e o autor no /T, e uma nota numa marca de redação tende a descrever justamente o que está sendo removido (“CPF do cliente, confirmar com o jurídico”). O ApplyRedactions esvazia o dicionário da própria marca, mas respostas encadeadas sob ela via /IRT são anotações separadas, apagadas só quando o retângulo delas se sobrepõe a uma região de redação, então uma resposta estacionada na margem mantém o texto dela. A listagem abaixo imprime cada marca com a contagem de quadriláteros dela e as respostas encadeadas sob ela
procedure ListPendingMarks(Pdf: TPDFlib);
var
P, I, Parent: Integer;
begin
for P := 1 to Pdf.PageCount do
begin
if Pdf.SelectPage(P) <> 1 then
Continue;
for I := 1 to Pdf.AnnotationCount do
if Pdf.GetAnnotType(I) = 'Redact' then
Writeln(Format('p%d #%d by %s, quads=%d: %s',
[P, I, Pdf.GetAnnotTitle(I), Pdf.GetAnnotQuadCount(I),
Pdf.GetAnnotContentsEx(I)]))
else
begin
Parent := Pdf.GetAnnotReplyTo(I); // 0 quando /IRT está ausente
if (Parent > 0) and (Pdf.GetAnnotType(Parent) = 'Redact') then
Writeln(Format(' reply #%d to #%d: %s',
[I, Parent, Pdf.GetAnnotContentsEx(I)]));
end;
end;
end;
Qual overlay aparece depois do ApplyRedactions?
Uma entrada /RO vence sempre que está presente, como a regra de precedência da Table 192 exige. O ApplyRedactions instala aquele Form XObject como a aparência normal da anotação (/AP /N), descarta qualquer estado de aparência e o achata, então /IC, /OverlayText, /DA e /Q não têm efeito algum sobre essa marca. O posicionamento é a única parte sutil. A spec diz que a origem do overlay se alinha com o canto inferior esquerdo do Rect, enquanto o achatamento mapeia o BBox do Form através do Matrix dele sobre o Rect, o mesmo posicionamento de BBox e Matrix que o achatamento aplica a widgets de campos de formulário. Um overlay cujo BBox é [0 0 w h], dimensionado para o retângulo, com um Matrix identidade fica igual sob as duas leituras, então peça essa forma quando as suas próprias ferramentas gerarem marcas /RO
Sem /RO, o ApplyRedactions normaliza o dicionário e constrói a aparência ele mesmo. Ele apaga /C e /BS e define o /Border como [0 0 0]: essas entradas descrevem a marca pendente, geralmente um contorno colorido, e queimá-lo na página final faria uma redação concluída parecer inacabada. Sem a borda, o preenchimento cobre o retângulo inteiro. O rótulo é desenhado dentro do Form gerado, com tamanho e cor do /DA, justificação do /Q, mosaico do /Repeat, e o nome de fonte F0 vinculado nos recursos do próprio Form à Helvetica padrão. Esse vínculo importa quando arquivos viajam entre ferramentas: a ISO 32000-1 §12.7.3.3 define um nome de fonte DA como uma entrada nos recursos default do formulário interativo (/DR), um documento sem campos de formulário não os tem, e outra aplicação que aplique a mesma marca pode substituir por uma fonte de fallback. O PDFlibPas pula essa procura, e com Helvetica o rótulo serve para texto em escrita latina como REDACTED ou um código de isenção
Um /IC ausente vira uma caixa preta
O default da spec para um /IC ausente é um interior transparente, e o PDFlibPas dele se afasta de propósito. Depois da remoção, um interior transparente mostra o papel, uma lacuna branca onde havia um nome, e essa lacuna fica igual a um layout que nunca teve nada ali. Um leitor não diferencia uma área com redação de uma em branco, e um revisor não confirma que a redação aconteceu sem comparar o texto extraído. Uma moldura vazia, que é o que um passo de aplicação produz quando achata a aparência pendente de uma marca só de contorno, falha de outro jeito: lê-se como uma proposta em que ninguém agiu. Nós pesamos seguir o default ao pé da letra e escolhemos a caixa preta, porque o sinal visível de que algo foi retido é parte do que um documento redigido diz ao leitor dele, e a escolha só afeta marcas cujo autor jamais declarou um preenchimento. Um /IC já presente no dicionário é usado seja qual for o valor dele, então um produtor que pediu branco recebe branco
O que já se foi antes do overlay ser desenhado
O ApplyRedactions trabalha página por página e completa toda a remoção antes de achatar overlay algum, então um overlay jamais pode ser pego pela própria região dele. Para cada página ele coleta um retângulo por quadrilátero, ou o Rect das marcas sem quadriláteros, e limpa cada camada de conteúdo contra cada um deles:
- Texto é cortado glifo por glifo, medido a partir das fontes da página e do estado de texto completo, e o resto de uma string parcialmente coberta fica na posição original dela
- Caminhos vetoriais, imagens inline e shadings que pintam dentro da região são apagados do content stream
- Form XObjects são adentrados recursivamente; um Form que outras páginas ou posicionamentos também desenham sofre redação através de uma cópia privada, então aquelas páginas mantêm o original
- Image XObjects perdem só os pixels cobertos, sobrescritos com preto (tinta K total para CMYK); imagens JPEG, JPEG 2000, JBIG2 e CCITT são decodificadas e armazenadas de volta como streams Flate sem perdas
- No PDF etiquetado,
/Alte/ActualTextanexados a marked content removido são limpos no conteúdo da página e na árvore de estrutura - Outras anotações cujo retângulo toca a região são apagadas, e campos de formulário cujos widgets ficam ali perdem os valores deles
O passo de pixels é a cláusula de imagem da spec na prática. Um retângulo preto pintado sobre um passaporte escaneado satisfaz o olho e ainda falha na §12.5.6.23, porque as amostras originais decodificam assim que alguém extrai a imagem. O mesmo motor fica por trás do RedactRegion, que remove conteúdo de um retângulo que você passa diretamente e faz par com o SanitizeDocument, sem ler anotação alguma. Depois que o conteúdo se foi, cada marca é achatada e o dicionário dela limpo até sobrar /Type e /Subtype. O achatamento remove a anotação do array /Annots da página, mas o dicionário ainda é um objeto no documento, e um salvamento que o gravaria entregaria /Contents, /RC, /T e o resto do rastro de revisão ao destinatário
Salvar sem deixar a revisão sem redação legível
Um salvamento incremental desfaz tudo acima. Sob a §7.5.6 (o mesmo número na ISO 32000-1 e na ISO 32000-2), uma atualização incremental anexa os objetos alterados ao fim do arquivo e deixa os bytes originais intactos, então a revisão original, com o conteúdo de página, as imagens e as marcas anotadas dela, continua legível para qualquer um que corte o arquivo no %%EOF anterior; o ExtractDocumentRevision da edição Small Business do PDFlibPas faz isso numa única chamada. Depois do ApplyRedactions, escreva um arquivo novo com SaveToFile, nunca SaveChanges, que anexa. Antes de salvar, o GarbageCollectObjects varre a tabela de objetos e descarta objetos que nada mais referencia, como os appearance streams que as marcas carregavam antes do passo de aplicação; um salvamento completo pula essa varredura a menos que o SetGarbageCollect a habilite, ou o campo GarbageCollect do record TPDFlibSaveOptions que você passa ao SaveToFileOptions esteja definido. Metadados são a lacuna restante. O SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET) apaga Author, Subject, Keywords, Creator e Producer e remove o stream XMP, deixando o Title para você conferir. Evite o SANITIZE_ALL aqui: ele inclui o SANITIZE_ANNOTATIONS e apaga toda anotação que sobrar no documento, notas que você pretendia manter entre elas
O ApplyRedactions, como o RedactRegion, está disponível na edição Enterprise. Num TPDFlib simples ele devolve 0 e define o LastErrorCode como PDFLIB_ERROR_ENTERPRISE_REQUIRED (900), então o passo de liberação usa o TPDFlibEnterprise da unit PDFlibEnterprise
uses
System.SysUtils, PDFlibrary, PDFlibEnterprise;
procedure ApplyAndRelease(const ReviewFile, ReleaseFile: string);
var
Pdf: TPDFlibEnterprise;
Applied, Orphans: Integer;
begin
Pdf := TPDFlibEnterprise.Create;
try
if Pdf.LoadFromFile(ReviewFile, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s', [ReviewFile]);
Applied := Pdf.ApplyRedactions; // toda marca Redact em cada página
if Applied = 0 then
raise Exception.CreateFmt('No marks applied (error %d)',
[Pdf.LastErrorCode]);
// Info e XMP ainda podem nomear o que as páginas não mostram mais
Pdf.SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET);
Orphans := Pdf.GarbageCollectObjects;
// Reescrita completa para um arquivo novo; nunca SaveChanges depois da redação
if Pdf.SaveToFile(ReleaseFile) <> 1 then
raise Exception.Create('Save failed');
Writeln(Format('%d marks applied, %d unreferenced objects dropped',
[Applied, Orphans]));
finally
Pdf.Free;
end;
end;
Esperado: o ApplyRedactions devolve o número de marcas Redact que ele achatou em todas as páginas, 2 para a cópia de revisão escrita pelo MarkForReview, e o GarbageCollectObjects devolve quantos objetos sem referência a varredura descartou
Seja qual ferramenta desenhou uma marca, as entradas /RO e /IC dela decidem o que o destinatário vê na página, enquanto o passo de aplicação no PDFlibPas Enterprise para Delphi decide o que se foi por baixo dela