Usuń adnotację Link z otagowanego PDF-a edytorem działającym wyłącznie na poziomie strony, a strona renderuje się dobrze, podczas gdy drzewo struktury dalej trzyma OBJR, wpis w drzewie nadrzędnym i element Link dla adnotacji, której już nie ma. RemovePdfPageAnnotations w PDFium Component wycofuje adnotację ze strony i na żądanie usuwa razem z nią te elementy struktury
Gdzie otagowana adnotacja wpina się w drzewo struktury
Otagowana adnotacja siedzi na styku czterech obiektów i czyste usunięcie musi odwiedzić wszystkie. ISO 32000-1 §14.7.4.3 mówi, że gdy zawartością elementu struktury jest cały obiekt PDF, na przykład adnotacja, tablica /K tego elementu nazywa go słownikiem odwołania do obiektu (tabela 325): /Type /OBJR, wymagane odwołanie pośrednie /Obj wskazujące adnotację i opcjonalne /Pg nadpisujące własną stronę elementu. To pokrywa kierunek w przód, od drzewa do strony
Droga wstecz to §14.7.4.4. Drzewo nadrzędne to drzewo liczb (§7.9.7) z wpisem dla każdego obiektu będącego elementem zawartości, kluczowanego liczbą całkowitą, którą ten obiekt przy sobie niesie. Adnotacja niesie /StructParent, a jej wartość w drzewie nadrzędnym to pojedyncze odwołanie pośrednie do elementu właściciela; strona, której strumień zawartości trzyma oznaczoną treść, niesie zamiast tego /StructParents, a jego wartość to tablica indeksowana MCID-ami. Tabela 326 dopuszcza najwyżej jeden z tych dwóch kluczy w dowolnym obiekcie, bo obiekt jest bądź w całości elementem zawartości, bądź pojemnikiem na oznaczoną treść. Dla hiperłączy §14.8.4.4.2 pozwala elementowi Link trzymać i widoczne słowa jako odwołania do oznaczonej treści (MCR), i OBJR wskazujący adnotację łącza. Usuń adnotację, a oto inwentaryzacja:
| Miejsce | Co trzyma | Co robi czyste usunięcie |
|---|---|---|
/Annots strony | Odwołanie pośrednie do adnotacji | Usuwa odwołanie; pozostałe zachowują swoją kolejność |
/K elementu Link | OBJR z /Obj i /Pg | Usuwa element zawartości OBJR |
| Słownik adnotacji | Klucz całkowity /StructParent | Usuwa klucz, bo obiekt nie jest już elementem zawartości |
Liść /Nums drzewa nadrzędnego | Klucz sparowany z odwołaniem do elementu Link | Usuwa parę klucz-wartość |
| Korzeń drzewa struktury | /ParentTreeNextKey | Zostawia go w spokoju |
Dlaczego łącze-widmo przeżywa swoją adnotację?
Edytor poziomu strony usuwa adnotację przez wyjęcie jej z /Annots, i stąd idzie jedną z dwóch dróg. Jeśli narzędzie dodatkowo zwalnia obiekt adnotacji, /Obj w OBJR wskazuje teraz obiekt niezdefiniowany, który §7.3.10 każe czytnikowi traktować jako null: element Link posiada element zawartości, który nie rozwiązuje się w nic. Jeśli narzędzie zostawi obiekt w pliku, OBJR nadal się rozwiązuje, ale jego /Pg nazywa stronę, której /Annots już go nie wymienia. W obu przypadkach element Link zachowuje swoje dzieci z oznaczonej treści, więc słowa „Pobierz kartę katalogową” dalej są otagowane jako łącze, a czytnik ekranowy przechodzący drzewo w kolejności czytania może odczytać łącze, którego nic na stronie nie aktywuje. Walidatory PDF/UA takie jak PAC i veraPDF, które sprawdzają zgodność z ISO 14289-1, porównują to, co wymienia strona, z tym, co głosi drzewo, więc rozjazd między nimi może tam wyjść na jaw, nawet gdy wyrenderowana strona wygląda idealnie
Usuwanie treści ma własną wersję tego problemu. Usuń obiekty tekstowe wewnątrz /P <</MCID 4>> BDC … EMC i zostaw operatory, a strona trzyma pustą sekwencję oznaczonej treści, której MCR dalej siedzi w elemencie <P>; czytelnik dostaje akapit bez słów. Usuń BDC bez jego EMC, a strumień zawartości jest po prostu popsuty. §14.6 wymaga, aby każda sekwencja oznaczonej treści była otoczona BDC albo BMC i pasującym EMC wewnątrz jednego strumienia zawartości, a osierocony EMC paruje każdą późniejszą sekwencję na stronie z niewłaściwym otwieraczem
Usuwanie adnotacji funkcją RemovePdfPageAnnotations
RemovePdfPageAnnotations w FPdfDocumentStructure przyjmuje strumień źródłowy, indeks strony liczony od zera w kolejności drzewa stron, numery obiektów adnotacji do usunięcia, rekord TPdfAnnotationRemovalOptions i dwa parametry wyjściowe dla nowego pliku oraz TPdfAnnotationRemovalReport. Z TPdfAnnotationRemovalOptions.Default dotyka tylko strony: jedna przyrostowa rewizja niesie słownik strony ze skróconą tablicą /Annots (albo bez klucza /Annots, gdy ostatnia adnotacja znika) plus niezmieniony katalog, więc numery obiektów, identyfikator dokumentu i każdy inny klucz strony przechodzą. Obiekt adnotacji zachowuje swoje ciało, a wywołujący, który dalej trzyma jego numer, może go odczytać. Drzewo struktury zostaje takie, jakie było, co utrzymuje otagowanie nietknięte na wypadek, gdy zaraz skierujesz ten element Link na zastępczą adnotację. Wejście jest sprawdzane, zanim cokolwiek zostanie zapisane:
- Każdy numer musi widnieć w
/Annotswybranej strony i nazywać obiekt/Type /Annot; numer wymieniony dwa razy w jednym wywołaniu jest odrzucany - Adnotacje widget są odrzucane, bo widget należy do swojego pola formularza, a zerwanie go ze strony zostawiłoby pole, którego widget żadna strona nie wyświetla
- Zaszyfrowane źródło jest odrzucane, bo edycja struktury potrzebuje odszyfrowanych bajtów; odszyfruj plik zawczasu
MaxOutputBytes,MaxWorkiCancellationTokenograniczają pracę, a każde niepowodzenie zwracaFalsebez wyjścia i zErrorMessagenazywającym fazę, na której stanęło
Dla dokumentu otagowanego drzewo struktury już mówi ci, które numery adnotacji mają znaczenie. Każdy element OBJR w migawce z ReadPdfDocumentStructure niesie przywołany numer obiektu, a jego numer strony pochodzi z własnego /Pg OBJR-a albo, gdy tego brak, z elementu. Poniższa procedura zbiera wszystkie adnotacje, na które wskazuje element Link na jednej stronie, i usuwa je z obiema opcjami sprzątania włączonymi. Trzymaj filtr strony: numer wzięty z innej strony odrzuca całą partię
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);
// Każda adnotacja, na którą wskazuje element Link na tej stronie
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; // elementy OBJR i wpisy ParentTree
Options.RemoveEmptiedStructureNodes := True; // elementy opróżnione do zera
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;
Co się dzieje z kluczem ParentTree
Z ustawionym RemoveOwnedStructurePeers każdy OBJR, którego /Obj jest usuniętą adnotacją na tej stronie, odchodzi ze swojego elementu przez edytor struktury, co trzyma odwrócony indeks w kroku. Para klucz-wartość zapisana pod /StructParent adnotacji jest wyciągana z jej liścia /Nums, a sam klucz /StructParent jest usuwany ze słownika adnotacji, bo tabela 326 żąda go tylko na obiektach będących strukturalnymi elementami zawartości. /ParentTreeNextKey nie jest obniżane. §14.7.4.4 wymaga jedynie, aby był większy od każdego klucza w użyciu, czego usunięcie nie potrafi złamać, a klucze drzewa liczb nie muszą być ciągłe, więc writer trzymający się /ParentTreeNextKey nigdy ponownie nie wydaje uwolnionego klucza. Każda ocalała adnotacja zachowuje swój numer obiektu, pozycję w /Annots i klucz w drzewie nadrzędnym; funkcja odczytuje z powrotem zapisaną przez siebie stronę i odrzuca wynik, gdy któraś ocalała zniknęła albo zmieniła miejsce. Oczekiwane: na stronie z jednym otagowanym łączem Report.RemovedAnnotations i Report.RemovedObjectReferences mają obie wartość 1, a Report.SourceSha256 i Report.OutputSha256 odnotowują, które bajty weszły, a które wyszły
Kiedy opróżniony element też powinien odejść?
RemoveEmptiedStructureNodes usuwa elementy, które usunięcie OBJR zostawiło bez elementów zawartości i bez elementów potomnych, najgłębsze najpierw. Działa w turach: każda tura odczytuje strukturę ponownie, usuwa elementy, które są już puste, i dodaje ich rodziców do kandydatów następnej tury, więc łańcuch owijek zapada się od dołu w górę, podczas gdy korzeń drzewa struktury zawsze zostaje. Element Annot tagujący żółtą karteczę zwykle nie trzyma niczego poza OBJR-em (§14.8.4.4.3 na to pozwala), więc opróżnia się i odchodzi. Element Link jest inny. Nadal posiada odwołania do oznaczonej treści dla swoich widocznych słów, więc nie jest pusty i przechodzi — jako Link bez adnotacji. RemovePdfPageAnnotations nie przemianowuje go, bo to, czy te słowa powinny zostać łączem, jest pytaniem redakcyjnym, a nie strukturalnym. Przy czystym usunięciu włącz obie opcje, a potem zdegraduj każdy element Link zostawiony bez OBJR do Span: technologie wspomagające czytają wtedy te słowa jako zwykły tekst, zamiast serwować łącze prowadzące w nikąd
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;
// Skrót migawki strzeże przed zmianą bajtów w międzyczasie
if not EditPdfDocumentStructure(Source, Snapshot.SourceSha256, Ops,
Options, Fixed, Report) then
raise Exception.Create(Report.ErrorMessage);
Result := Length(Ops);
finally
Source.Free;
end;
end;
Przy połówce zamiany trzymaj domyślne. Opróżniony element zostawiony na miejscu dalej niesie swoją rolę, tytuł, tekst alternatywny i identyfikator elementu, a jeśli późniejszy krok przypnie do niego nową adnotację operacją psekAddObjectReference, która daje adnotacji świeży klucz /StructParent i przesuwa /ParentTreeNextKey według tych samych reguł, które rejestrują nowe elementy MCR i OBJR w drzewie nadrzędnym, element zachowuje swoje miejsce w kolejności czytania. Usunięcie i odtworzenie tego elementu wyrzuciłoby te wartości i dało mu nowy numer obiektu
Usuwanie otagowanej treści: własność biegnie od BDC do swojego EMC
Adnotacje nie są obiektami strony, więc tekst, ścieżki i obrazy idą przez inne API: wsadowy edytor obiektów, PlanStyledPdfObjectEdits i ExecuteStyledPdfObjectEdits w FPdfStyledEdit, którego TPdfObjectEditOptions niesie RemoveOwnedStructurePeers i RemoveEmptiedStructureNodes w tym samym znaczeniu. Otagowana sekwencja oznaczonej treści znika razem ze swoim wpisem struktury tylko wtedy, gdy każdy obiekt między jej BDC a pasującym EMC jest usuwany. Wtedy oba operatory odchodzą ze strumienia zawartości, MCR odchodzi ze swojego elementu, a slot tego MCID-a w tablicy ParentTree strony jest ustawiany na null, zamiast być zasuniętym, bo każdy inny MCID na stronie indeksuje w tę samą tablicę. Sekwencja, która owija też przetrwałą treść, zostaje razem ze swoim MCR-em. Strona, która ponownie używa usuniętego MCID-a dla przetrwałej treści, jest odrzucana wprost, bo MCID-y są ograniczone do strumienia zawartości strony, a wyrzucenie wpisu struktury osierociłoby przetrwałego. Przy opcji zostawionej jako False partia zachowuje zamiast tego pary BDC i EMC na granicy usuwania, więc drzewo struktury dalej się rozwiązuje i nic w nim się nie zmienia
Własność trzeba mierzyć od BDC do końca jego pasującego EMC. Nasza pierwsza wersja mierzyła ją po zakresie bajtów samego operatora BDC, bo tam sparsowany znacznik odnotowuje swoją pozycję, i nie znajdowała żadnych objętych obiektów. Pokrewny błąd to oznaczenie BDC do usunięcia i zgubienie jego partnera, co zapisuje dokładnie niezbalansowany strumień opisany wyżej, więc edytor zawsze usuwa obie połówki pary razem. Poniższa procedura usuwa każdy obiekt najwyższego poziomu niosący jeden MCID. ExecuteStyledPdfObjectEdits przeładowuje Pdf wynikiem, chyba że ustawiono EditOptions.PreviewOnly, a Report.RemovedStructurePeers i Report.RemovedStructureNodes liczą, co odeszło z drzewa; funkcja zwraca ten raport
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 liczy od 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;
Usunięta adnotacja po każdym z tych wywołań dalej zajmuje swój obiekt w pliku, bo wycofane zostaje tylko odwołanie ze strony. Gdy OBJR-a już nie ma, nic w grafie katalogu do niego nie sięga, więc TPdf.SaveAsReachable może wymieść ten obiekt-sierotę z przepisanej kopii; przy opcjach domyślnych OBJR trzyma adnotację w zasięgu i zamiatanie zostawia ją też. Przeprowadzaj usuwanie z obiema opcjami struktury włączonymi zawsze, gdy usunięcie jest ostateczne, a strona, drzewo struktury i drzewo nadrzędne w pliku, który zapisuje z powrotem PDFium Component dla Delphi, opisują wszystkie ten sam zestaw adnotacji