Haalt een editor die alleen naar de pagina kijkt een Link-annotatie uit een getagde PDF, dan rendert de pagina prima terwijl de structuurboom nog een OBJR, een ParentTree-entry en een Link-element bewaart voor een annotatie die allang weg is. RemovePdfPageAnnotations in PDFium Component trekt de annotatie van de pagina terug en ruimt op verzoek die structuuronderdelen meteen mee op
Waar een getagde annotatie in de structuurboom vastzit
Een getagde annotatie zit op de kruising van vier objecten, en een nette verwijdering moet alle vier bezoeken. ISO 32000-1 §14.7.4.3 zegt dat wanneer de content van een structuurelement een heel PDF-object is, zoals een annotatie, de /K-array van het element hem benoemt met een object reference dictionary (Tabel 325): /Type /OBJR, een verplichte indirecte /Obj die naar de annotatie wijst, en een optionele /Pg die de eigen pagina van het element overstijgt. Dat dekt de vooruitse richting, van de boom naar de pagina
De omgekeerde route is §14.7.4.4. De parent tree is een number tree (§7.9.7) met een entry voor elk object dat een content-item is, als sleutel genomen van de integer die dat object draagt. Een annotatie draagt /StructParent, en zijn parent tree-waarde is één enkele indirecte verwijzing naar het bezittende element; een pagina waarvan de contentstream marked content bevat draagt in plaats daarvan /StructParents, en zijn waarde is een array geïndexeerd op MCID. Tabel 326 staat in een object maximaal één van de twee sleutels toe, want een object is ofwel in zijn geheel een content-item ofwel een container voor marked content. Voor hyperlinks laat §14.8.4.4.2 een Link-element zowel de zichtbare woorden bevatten, als marked-content-verwijzingen (MCR's), als een OBJR naar de linkannotatie. Haalt u de annotatie weg, dan is dit de inventaris:
| Locatie | Wat erin staat | Wat een nette verwijdering doet |
|---|---|---|
Pagina /Annots | Indirecte verwijzing naar de annotatie | Zet de verwijzing weg; overlevenden houden hun volgorde |
Link-element /K | OBJR met /Obj en /Pg | Verwijdert het OBJR-content-item |
| Annotatiedictionary | De integer-sleutel /StructParent | Verwijdert de sleutel, want het object is geen content-item meer |
ParentTree-/Nums-leaf | Sleutel gekoppeld aan een verwijzing naar het Link-element | Verwijdert het sleutel-waarde-paar |
| Structuurboom-root | /ParentTreeNextKey | Laat hem met rust |
Hoe overleeft een spooklink zijn eigen annotatie?
Een editor op paginaniveau verwijdert een annotatie door hem uit /Annots te halen, en daarna gaat het een van twee kanten op. Ruimt de tool ook het annotatieobject op, dan wijst de /Obj van de OBJR nu naar een ongedefinieerd object, wat §7.3.10 een lezer opdraagt als null te behandelen: het Link-element bezit een content-item dat op niets uitdraait. Laat de tool het object in het bestand staan, dan resolveert de OBJR nog steeds, maar zijn /Pg benoemt een pagina waarvan /Annots hem niet langer noemt. In beide gevallen houdt het Link-element zijn marked-content-kinderen, dus de woorden "Download de datasheet" zijn nog steeds als link getagd, en een screenreader die de boom in leesvolgorde doorloopt kan een link aankondigen die niets op de pagina zal activeren. PDF/UA-checkers zoals PAC en veraPDF, die tegen ISO 14289-1 valideren, vergelijken wat de pagina noemt met wat de boom claimt, dus een verschil tussen de twee kan daar aan het licht komen, ook als de gerenderde pagina er perfect uitziet
Content verwijderen kent een eigen versie van dit probleem. Neem de tekstobjecten binnen /P <</MCID 4>> BDC … EMC weg en behoud de operators, dan bevat de pagina een lege marked-content-sequentie waarvan de MCR nog in een <P>-element zit; de lezer krijgt een alinea zonder woorden. Haal de BDC weg maar niet zijn EMC, en de contentstream is ronduit stuk. §14.6 eist dat elke marked-content-sequentie binnen één contentstream wordt omsloten door een BDC of BMC en een bijbehorende EMC, en een wees-EMC koppelt elke latere sequentie op de pagina aan de verkeerde opener
Annotaties verwijderen met RemovePdfPageAnnotations
RemovePdfPageAnnotations in FPdfDocumentStructure krijgt een bronstream, een op nul gebaseerde pagina-index in paginaboomvolgorde, de objectnummers van de te verwijderen annotaties, een TPdfAnnotationRemovalOptions-record en twee out-parameters voor het nieuwe bestand en een TPdfAnnotationRemovalReport mee. Met TPdfAnnotationRemovalOptions.Default raakt hij alleen de pagina aan: één incrementele revisie bevat de pagedictionary met de ingekorte /Annots-array (of zonder /Annots-sleutel zodra de laatste annotatie weg is) plus de ongewijzigde catalog, dus objectnummers, het document-ID en alle andere paginasleutels overleven het. Het annotatieobject houdt zijn body, en een aanroeper die zijn nummer nog heeft kan hem nog lezen. De structuurboom blijft zoals hij was, wat de tagging intact houdt voor het geval u dat Link-element op het punt staat naar een vervangende annotatie te laten wijzen. De invoer wordt gecontroleerd voordat er iets wordt weggeschreven:
- Elk nummer moet in de
/Annotsvan de gekozen pagina voorkomen en naar een/Type /Annot-object wijzen; een nummer dat in één aanroep twee keer voorkomt wordt geweigerd - Widget-annotaties worden geweigerd, want een widget hoort bij zijn formulierveld en hem van de pagina halen laat een veld achter waarvan geen enkele pagina de widget toont
- Een versleutelde bron wordt geweigerd, want de structuurbewerking heeft ontsleutelde bytes nodig; ontsleutel het bestand vooraf
MaxOutputBytes,MaxWorkenCancellationTokenbegrenzen het werk, en elke mislukking geeftFalseterug zonder uitvoer en met eenErrorMessagedie de fase benoemt waar het stopte
Bij een getagd document vertelt de structuurboom u al welke annotatienummers ertoe doen. Elk OBJR-item in een ReadPdfDocumentStructure-snapshot draagt het gerefereerde objectnummer, en zijn paginanummer komt uit de eigen /Pg van de OBJR of, als die er niet is, uit die van het element. De routine hieronder verzamelt elke annotatie waarnaar een Link-element op één pagina verwijst en haalt ze weg met beide opruimopties aan. Houd de paginafilter aan: een nummer van een andere pagina wordt voor de hele batch geweigerd
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);
// Elke annotatie waarnaar een Link-element op deze pagina wijst
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; // OBJR-items en ParentTree-entries
Options.RemoveEmptiedStructureNodes := True; // elementen die leeg achterblijven
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;
Wat gebeurt er met de ParentTree-sleutel
Met RemoveOwnedStructurePeers aan verlaat elke OBJR waarvan /Obj een verwijderde annotatie op die pagina is via de structuureditor zijn element, wat de omgekeerde index bijhoudt. Het sleutel-waarde-paar onder de /StructParent van de annotatie komt uit zijn /Nums-leaf, en de /StructParent-sleutel zelf wordt uit de annotatiedictionary gehaald, want Tabel 326 vraagt hem alleen op objecten die structurele content-items zijn. /ParentTreeNextKey wordt niet verlaagd. §14.7.4.4 eist alleen dat hij groter is dan elke sleutel in gebruik, wat een verwijdering niet kan breken, en number tree-sleutels hoeven niet aaneengesloten te zijn, dus een writer die /ParentTreeNextKey volgt geeft de vrijgekomen sleutel nooit opnieuw uit. Elke overlevende annotatie houdt zijn objectnummer, zijn positie in /Annots en zijn ParentTree-sleutel; de functie leest de pagina die hij schreef opnieuw en weigert het resultaat als een overlevende verdwenen is of van plek veranderde. Verwacht: op een pagina met één getagde link zijn Report.RemovedAnnotations en Report.RemovedObjectReferences allebei 1, en Report.SourceSha256 en Report.OutputSha256 leggen vast welke bytes erin gingen en welke eruit kwamen
Wanneer gaat het leeggemaakte element ook weg?
RemoveEmptiedStructureNodes haalt de elementen weg die de OBJR-verwijdering zonder content-items en zonder childelementen achterliet, diepste eerst. Hij draait in rondes: elke ronde leest de structuur opnieuw, haalt de elementen weg die nu leeg zijn en zet hun ouders op de kandidatenlijst voor de volgende ronde, dus een keten van wrappers klapt van onder naar boven in elkaar terwijl de structuurboom-root altijd blijft. Een Annot-element dat een sticky note tagt bevat meestal niets dan de OBJR (§14.8.4.4.3 staat dat toe), dus hij wordt leeg en verdwijnt. Een Link-element is anders. Hij bezit nog de marked-content-verwijzingen voor zijn zichtbare woorden, dus hij is niet leeg en hij overleeft het, als een Link zonder annotatie. RemovePdfPageAnnotations hernoemt hem niet, want of die woorden een link moeten blijven is een redactionele vraag in plaats van een structurele. Voor een zuivere verwijdering zet u beide opties aan en degradeert u elk Link-element dat zonder OBJR achterblijft naar Span: screenreaders lezen die woorden dan als gewone tekst in plaats van een link aan te bieden die nergens heen gaat
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;
// De snapshot-digest bewaakt dat de bytes intussen niet veranderden
if not EditPdfDocumentStructure(Source, Snapshot.SourceSha256, Ops,
Options, Fixed, Report) then
raise Exception.Create(Report.ErrorMessage);
Result := Length(Ops);
finally
Source.Free;
end;
end;
Houd de standaardinstellingen aan wanneer de verwijdering de helft van een vervanging is. Een geleegd element dat blijft staan draagt nog zijn rol, zijn titel, zijn alternatieve tekst en zijn element-ID, en als een latere stap er met een psekAddObjectReference-operatie een nieuwe annotatie aan hangt, die de annotatie een frisse /StructParent-sleutel geeft en /ParentTreeNextKey met dezelfde regels laat opschuiven die nieuwe MCR- en OBJR-items in de parent tree registreren, dan houdt het element zijn plek in de leesvolgorde. Dat element verwijderen en opnieuw aanmaken zou die waarden weggooien en hem een nieuw objectnummer geven
Getagde content verwijderen: het eigendomsbereik loopt van BDC tot de bijbehorende EMC
Annotaties zijn geen paginaobjecten, dus tekst, paden en afbeeldingen lopen via een andere API: de batchobjecteditor, PlanStyledPdfObjectEdits en ExecuteStyledPdfObjectEdits in FPdfStyledEdit, waarvan TPdfObjectEditOptions RemoveOwnedStructurePeers en RemoveEmptiedStructureNodes met dezelfde betekenis meedraagt. Een getagde marked-content-sequentie verdwijnt pas samen met zijn structuurentry als elk object tussen zijn BDC en de bijbehorende EMC wordt verwijderd. In dat geval verlaten beide operators de contentstream, verlaat de MCR zijn element, en wordt het slot van die MCID in de ParentTree-array van de pagina op null gezet in plaats van dichtgeschoven, omdat elke andere MCID op de pagina in dezelfde array indexeert. Een sequentie die ook overlevende content omsluit blijft staan, samen met zijn MCR. Een pagina die de verwijderde MCID hergebruikt voor overlevende content wordt ronduit geweigerd, want MCID's zijn gebonden aan de contentstream van de pagina en de structuurentry laten vallen zou de overlevende wees maken. Met de optie op False houdt de batch in plaats daarvan de BDC- en EMC-paren op de verwijderingsgrens in stand, zodat de structuurboom resolveerbaar blijft en er niets in verandert
Het eigendomsbereik moet worden gemeten van de BDC tot het einde van zijn bijbehorende EMC. Onze eerste versie mat het over alleen het bytebereik van de BDC-operator, dat is waar een geparsde mark zijn positie vastlegt, en vond er helemaal geen omsloten objecten. Een verwante fout is de BDC voor verwijdering markeren en zijn partner uit het oog verliezen, wat precies de ongebalanceerde stream oplevert die hierboven werd beschreven, dus de editor haalt altijd beide helften van een paar samen weg. De routine hieronder verwijdert elk top-level-object dat één MCID draagt. ExecuteStyledPdfObjectEdits herlaadt Pdf met het resultaat tenzij EditOptions.PreviewOnly aanstaat, en Report.RemovedStructurePeers en Report.RemovedStructureNodes tellen wat de boom verliet; de functie geeft dat rapport terug
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 telt vanaf 1
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;
Een verwijderde annotatie bezet na elk van de twee aanroepen nog zijn object in het bestand, want alleen de paginaverwijzing wordt ingetrokken. Zodra de OBJR weg is bereikt niets in de grafiek van de catalog hem meer, dus kan TPdf.SaveAsReachable dat weesobject uit een herschreven kopie opruimen; met de standaardopties houdt de OBJR de annotatie bereikbaar en de opruiming houdt hem ook. Draai de verwijdering met beide structuuropties aan zodra ze definitief is, en de pagina, de structuurboom en de parent tree in het bestand dat PDFium Component for Delphi terugschrijft beschrijven allemaal dezelfde verzameling annotaties