Eliminate un'annotazione Link da un PDF con tag con un editor che opera solo sulla pagina, e la pagina viene renderizzata bene mentre l'albero della struttura conserva ancora un OBJR, una voce ParentTree e un elemento Link per un'annotazione sparita. RemovePdfPageAnnotations di PDFium Component ritira l'annotazione dalla pagina e, su richiesta, rimuove con lei anche quei pezzi di struttura
Dove un'annotazione con tag aggancia l'albero della struttura
Un'annotazione con tag siede all'incrocio di quattro oggetti, e una cancellazione pulita deve toccarli tutti. ISO 32000-1 §14.7.4.3 dice che quando il contenuto di un elemento di struttura è un intero oggetto PDF, come un'annotazione, l'array /K dell'elemento lo nomina con un dizionario di riferimento a oggetto (Tabella 325): /Type /OBJR, un /Obj indiretto obbligatorio che punta all'annotazione, e una /Pg facoltativa che sostituisce la pagina dell'elemento stesso. Questo copre la direzione in avanti, dall'albero verso la pagina
Il percorso inverso è il §14.7.4.4. Il parent tree è un number tree (§7.9.7) con una voce per ogni oggetto che è un content item, indicizzata dall'intero che quell'oggetto porta. Un'annotazione porta /StructParent, e il suo valore nel parent tree è un unico riferimento indiretto all'elemento proprietario; una pagina il cui content stream contiene marked content porta invece /StructParents, e il suo valore è un array indicizzato per MCID. La Tabella 326 ammette al massimo una delle due chiavi su un oggetto, perché un oggetto è o un content item nella sua interezza o un contenitore di marked content. Per gli hyperlink, il §14.8.4.4.2 consente a un elemento Link di tenere sia le parole visibili, come riferimenti a marked content (MCR), sia un OBJR verso l'annotazione del link. Eliminate l'annotazione e questo è l'inventario:
| Posizione | Cosa contiene | Cosa fa una rimozione pulita |
|---|---|---|
Pagina /Annots | Riferimento indiretto all'annotazione | Toglie il riferimento; le superstiti conservano il loro ordine |
Elemento Link /K | OBJR con /Obj e /Pg | Rimuove il content item OBJR |
| Dizionario dell'annotazione | Chiave intera /StructParent | Rimuove la chiave, visto che l'oggetto non è più un content item |
Foglia /Nums del ParentTree | Chiave abbinata a un riferimento all'elemento Link | Rimuove la coppia chiave e valore |
| Radice dell'albero di struttura | /ParentTreeNextKey | La lascia stare |
Come fa un link fantasma a sopravvivere alla sua annotazione?
Un editor a livello di pagina cancella un'annotazione togliendola da /Annots, e da lì la storia segue una di due strade. Se lo strumento libera anche l'oggetto annotazione, il /Obj dell'OBJR ora punta a un oggetto non definito, che il §7.3.10 dice al lettore di trattare come null: l'elemento Link possiede un content item che non risolve a nulla. Se lo strumento lascia l'oggetto nel file, l'OBJR risolve ancora, ma la sua /Pg nomina una pagina il cui /Annots non lo elenca più. In entrambi i casi l'elemento Link conserva i suoi figli marked content, così le parole "Download the datasheet" restano taggate come link, e uno screen reader che percorre l'albero in ordine di lettura può annunciare un link che nulla sulla pagina attiverà mai. I verificatori PDF/UA come PAC e veraPDF, che validano contro ISO 14289-1, confrontano ciò che la pagina elenca con ciò che l'albero dichiara, così un disaccordo tra i due può saltare fuori lì anche quando la pagina renderizzata sembra perfetta
Anche la cancellazione di contenuto ha la sua versione del problema. Togliete gli oggetti di testo dentro /P <</MCID 4>> BDC … EMC e lasciate gli operatori, e la pagina resta con una sequenza di marked content vuota il cui MCR siede ancora in un elemento <P>; il lettore riceve un paragrafo senza parole. Togliete il BDC ma non il suo EMC e il content stream si rompe sul posto. Il §14.6 esige che ogni sequenza di marked content sia racchiusa da un BDC o un BMC e da un EMC corrispondente dentro un unico content stream, e un EMC orfano fa abbinare a ogni sequenza successiva della pagina l'apertura sbagliata
Rimuovere annotazioni con RemovePdfPageAnnotations
RemovePdfPageAnnotations in FPdfDocumentStructure prende uno stream sorgente, un indice di pagina a base zero in ordine di page tree, i numeri di oggetto delle annotazioni da rimuovere, un record TPdfAnnotationRemovalOptions e due parametri out per il nuovo file e un TPdfAnnotationRemovalReport. Con TPdfAnnotationRemovalOptions.Default tocca solo la pagina: una revisione incrementale porta il dizionario di pagina con l'array /Annots accorciato (o nessuna chiave /Annots una volta andata via l'ultima annotazione) più il catalogo invariato, così i numeri di oggetto, il document ID e ogni altra chiave di pagina sopravvivono. L'oggetto annotazione conserva il suo corpo, e chi chiama e conserva il suo numero può ancora leggerlo. L'albero della struttura resta com'era, il che tiene intatto il tagging per il caso in cui state per puntare quell'elemento Link verso un'annotazione di sostituzione. L'input viene controllato prima che qualcosa venga scritto:
- Ogni numero deve comparire negli
/Annotsdella pagina selezionata e nominare un oggetto/Type /Annot; un numero elencato due volte nella stessa chiamata viene rifiutato - Le annotazioni widget vengono rifiutate, perché un widget appartiene al suo form field e staccarlo dalla pagina lascerebbe un campo il cui widget nessuna pagina mostra
- Una sorgente cifrata viene respinta, visto che la modifica alla struttura serve byte decifrati; decifrate il file in anticipo
MaxOutputBytes,MaxWorkeCancellationTokendelimitano il lavoro, e ogni fallimento restituisceFalsesenza output e con unErrorMessageche nomina la fase dove si è fermato
Per un documento con tag l'albero della struttura vi dice già quali numeri di annotazione contano. Ogni item OBJR in uno snapshot di ReadPdfDocumentStructure porta il numero di oggetto referenziato, e il suo numero di pagina arriva dalla /Pg dell'OBJR stesso o, in mancanza, da quella dell'elemento. La routine sotto raccoglie ogni annotazione a cui un elemento Link di una pagina si riferisce e le rimuove con entrambe le opzioni di pulizia accese. Tenete il filtro di pagina: un numero preso da un'altra pagina fa rifiutare l'intero batch
uses
System.SysUtils, System.Classes, System.IOUtils,
FPdfCompress, FPdfDocumentStructure;
function RemoveTaggedLinks(const InName, OutName: string;
PageIndex: Integer): TPdfAnnotationRemovalReport;
var
Source: TMemoryStream;
Pages: TPdfSparsePageDictionaries;
ReadOptions: TPdfStructureEditOptions;
Snapshot: TPdfDocumentStructureSnapshot;
ReadReport: TPdfStructureEditReport;
Item: TPdfDocumentStructureContentItem;
Targets: array of Integer;
Options: TPdfAnnotationRemovalOptions;
Report: TPdfAnnotationRemovalReport;
Output: TBytes;
N, I, J: Integer;
Known: Boolean;
begin
Report := Default(TPdfAnnotationRemovalReport);
Source := TMemoryStream.Create;
try
Source.LoadFromFile(InName);
if not ReadPdfSparsePageDictionaries(Source, Pages) or
(PageIndex < 0) or (PageIndex > High(Pages)) then
raise Exception.Create('Page index is outside the page tree');
ReadOptions := TPdfStructureEditOptions.Default;
if not ReadPdfDocumentStructure(Source, ReadOptions, Snapshot, ReadReport) then
raise Exception.Create(ReadReport.ErrorMessage);
// Ogni annotazione a cui punta un elemento Link di questa pagina
for N := 0 to High(Snapshot.Nodes) do
if Snapshot.Nodes[N].RoleName = 'Link' then
for I := 0 to High(Snapshot.Nodes[N].ContentItems) do
begin
Item := Snapshot.Nodes[N].ContentItems[I];
if (Item.Kind <> psckObjectReference) or
(Item.PageObjectNumber <> Pages[PageIndex].ObjectNumber) then
Continue;
Known := False;
for J := 0 to High(Targets) do
if Targets[J] = Item.ReferencedObjectNumber then Known := True;
if not Known then
begin
SetLength(Targets, Length(Targets) + 1);
Targets[High(Targets)] := Item.ReferencedObjectNumber;
end;
end;
if Length(Targets) = 0 then
Exit(Report);
Options := TPdfAnnotationRemovalOptions.Default;
Options.RemoveOwnedStructurePeers := True; // item OBJR e voci ParentTree
Options.RemoveEmptiedStructureNodes := True; // elementi rimasti senza nulla dentro
if not RemovePdfPageAnnotations(Source, PageIndex, Targets, Options,
Output, Report) then
raise Exception.Create(Report.ErrorMessage);
TFile.WriteAllBytes(OutName, Output);
Result := Report;
finally
Source.Free;
end;
end;
Cosa succede alla chiave ParentTree
Con RemoveOwnedStructurePeers impostata, ogni OBJR il cui /Obj è un'annotazione rimossa su quella pagina lascia il suo elemento attraverso l'editor di struttura, e così l'indice inverso resta allineato. La coppia chiave e valore conservata sotto il /StructParent dell'annotazione esce dalla sua foglia /Nums, e la chiave /StructParent stessa viene tolta dal dizionario dell'annotazione, visto che la Tabella 326 la chiede solo sugli oggetti che sono content item strutturali. /ParentTreeNextKey non viene abbassato. Il §14.7.4.4 richiede solo che sia maggiore di ogni chiave in uso, cosa che una rimozione non può rompere, e le chiavi di un number tree non devono essere contigue, così un writer che segue /ParentTreeNextKey non ridistribuisce mai la chiave liberata. Ogni annotazione superstite conserva il suo numero di oggetto, la sua posizione negli /Annots e la sua chiave ParentTree; la funzione rilegge la pagina che ha scritto e rifiuta il risultato se una superstite è sparita o ha cambiato posto. Atteso: su una pagina con un solo link con tag, Report.RemovedAnnotations e Report.RemovedObjectReferences valgono entrambi 1, e Report.SourceSha256 e Report.OutputSha256 registrano quali byte sono entrati e quali sono usciti
Quando va buttato via anche l'elemento svuotato?
RemoveEmptiedStructureNodes rimuove gli elementi che la rimozione degli OBJR ha lasciato senza content item né elementi figli, dal più profondo in giù. Lavora a tornate: ogni tornata rilegge la struttura, rimuove gli elementi ormai vuoti e aggiunge i loro padri ai candidati della tornata successiva, così una catena di wrapper collassa dal basso verso l'alto mentre la radice dell'albero di struttura resta sempre. Un elemento Annot che tagga una sticky note di solito non tiene niente oltre l'OBJR (il §14.8.4.4.3 lo consente), quindi si svuota e va via. Un elemento Link è diverso. Possiede ancora i riferimenti a marked content delle sue parole visibili, quindi non è vuoto e sopravvive, come Link senza annotazione. RemovePdfPageAnnotations non lo rinomina, perché se quelle parole debbano restare un link è una domanda editoriale più che strutturale. Per una cancellazione netta, accendete entrambe le opzioni e poi declassate a Span ogni elemento Link rimasto senza OBJR: la tecnologia assistiva legge quelle parole come testo semplice invece di offrire un link che non porta a nulla
function DemoteUnlinkedLinks(const Data: TBytes; out Fixed: TBytes): Integer;
var
Source: TBytesStream;
Options: TPdfStructureEditOptions;
Snapshot: TPdfDocumentStructureSnapshot;
Ops: TPdfStructureEditOperations;
Report: TPdfStructureEditReport;
N: Integer;
begin
Result := 0;
Fixed := Data;
Source := TBytesStream.Create(Data);
try
Options := TPdfStructureEditOptions.Default;
if not ReadPdfDocumentStructure(Source, Options, Snapshot, Report) then
raise Exception.Create(Report.ErrorMessage);
for N := 0 to High(Snapshot.Nodes) do
if (Snapshot.Nodes[N].RoleName = 'Link') and
(Snapshot.Nodes[N].ObjectReferenceCount = 0) and
(Length(Snapshot.Nodes[N].ContentItems) > 0) then
begin
SetLength(Ops, Length(Ops) + 1);
Ops[High(Ops)] := Default(TPdfStructureEditOperation);
Ops[High(Ops)].Kind := psekSetRole;
Ops[High(Ops)].NodeID := Snapshot.Nodes[N].NodeID;
Ops[High(Ops)].Value := 'Span';
end;
if Length(Ops) = 0 then Exit;
// Il digest dello snapshot presidia i byte cambiati nel frattempo
if not EditPdfDocumentStructure(Source, Snapshot.SourceSha256, Ops,
Options, Fixed, Report) then
raise Exception.Create(Report.ErrorMessage);
Result := Length(Ops);
finally
Source.Free;
end;
end;
Tenete i valori predefiniti quando la cancellazione è metà di una sostituzione. Un elemento svuotato lasciato al suo posto conserva ancora ruolo, titolo, testo alternativo e element ID, e se un passo successivo gli aggancia una nuova annotazione con un'operazione psekAddObjectReference, che dà all'annotazione una chiave /StructParent fresca e avanza /ParentTreeNextKey secondo le stesse regole che registrano nuovi item MCR e OBJR nel parent tree, l'elemento conserva il suo posto nell'ordine di lettura. Togliere e ricreare quell'elemento butterebbe via quei valori e gli darebbe un nuovo numero di oggetto
Cancellare contenuto con tag: la proprietà va dal BDC al suo EMC
Le annotazioni non sono oggetti di pagina, quindi testo, percorsi e immagini passano da un'altra API: l'editor batch di oggetti, PlanStyledPdfObjectEdits e ExecuteStyledPdfObjectEdits in FPdfStyledEdit, il cui TPdfObjectEditOptions porta RemoveOwnedStructurePeers e RemoveEmptiedStructureNodes con lo stesso significato. Una sequenza di marked content con tag va via insieme alla sua voce di struttura solo quando ogni oggetto tra il suo BDC e l'EMC corrispondente sta per essere cancellato. In quel caso entrambi gli operatori lasciano il content stream, l'MCR lascia il suo elemento, e lo slot di quell'MCID nell'array ParentTree della pagina viene messo a null invece che chiuso, perché ogni altro MCID della pagina indicizza nello stesso array. Una sequenza che avvolge anche contenuto superstite resta, insieme al suo MCR. Una pagina che riusa l'MCID rimosso per contenuto superstite viene rifiutata in blocco, visto che gli MCID hanno come ambito il content stream della pagina e buttare via la voce di struttura lascerebbe orfano il superstite. Con l'opzione lasciata False, il batch conserva invece le coppie BDC e EMC al confine della rimozione, così l'albero di struttura continua a risolvere e nulla dentro di lui cambia
La proprietà va misurata dal BDC fino alla fine del suo EMC corrispondente. La nostra prima versione la misurava sul solo intervallo di byte dell'operatore BDC, che è dove un mark analizzato registra la sua posizione, e non trovava alcun oggetto racchiuso. Un errore affine è marcare il BDC per la rimozione e perdere di vista il suo compagno, che scrive esattamente lo stream sbilanciato descritto sopra, così l'editor rimuove sempre insieme entrambe le metà di una coppia. La routine sotto cancella ogni oggetto di primo livello che porta un MCID. ExecuteStyledPdfObjectEdits ricarica Pdf con il risultato a meno che EditOptions.PreviewOnly sia impostata, e Report.RemovedStructurePeers e Report.RemovedStructureNodes contano cosa ha lasciato l'albero; la funzione restituisce quel report
uses
System.SysUtils, PDFium, FPdfStyledEdit;
function DeleteTaggedRun(Pdf: TPdf; PageIndex, Mcid: Integer): TPdfObjectEditReport;
var
Ops: TPdfObjectEditOperations;
NoSemanticEdits: TPdfObjectSemanticEdits;
Options: TPdfObjectEditOptions;
Plan: TPdfObjectEditPlan;
Preview: TBytes;
Report: TPdfObjectEditReport;
I: Integer;
begin
Pdf.PageNumber := PageIndex + 1; // PageNumber è a base uno
for I := 0 to Pdf.ObjectCount - 1 do
if Pdf.PageObjectMarkedContentID(I) = Mcid then
begin
SetLength(Ops, Length(Ops) + 1);
Ops[High(Ops)] := Default(TPdfObjectEditOperation);
Ops[High(Ops)].Kind := poekRemove;
Ops[High(Ops)].ObjectIndex := I;
end;
if Length(Ops) = 0 then
raise Exception.CreateFmt('No object on page %d carries MCID %d',
[PageIndex + 1, Mcid]);
NoSemanticEdits := nil;
Options := TPdfObjectEditOptions.Default;
Options.RemoveOwnedStructurePeers := True;
Options.RemoveEmptiedStructureNodes := True;
Plan := PlanStyledPdfObjectEdits(Pdf, PageIndex, Ops, NoSemanticEdits, Options);
if not ExecuteStyledPdfObjectEdits(Pdf, Plan, Options, Preview, Report) then
raise Exception.Create(Report.ErrorMessage);
Result := Report;
end;
Un'annotazione rimossa occupa ancora il suo oggetto nel file dopo una qualsiasi delle due chiamate, perché viene ritirato solo il riferimento dalla pagina. Una volta che l'OBJR è andato, nulla nel grafo del catalogo lo raggiunge, così TPdf.SaveAsReachable può spazzare via quell'oggetto orfano da una copia riscritta; con le opzioni predefinite l'OBJR tiene l'annotazione raggiungibile e la pulizia la mantiene anche lei. Eseguite la rimozione con entrambe le opzioni di struttura accese ogni volta che la cancellazione è definitiva, e la pagina, l'albero di struttura e il parent tree nel file che PDFium Component per Delphi riscrive descriveranno tutti lo stesso insieme di annotazioni