Artykuł techniczny

Adnotacje Redact w PDF: znacznik, ApplyRedactions i /RO

Znacznik redakcji narysowany bez koloru /IC może wyjść z kroku aplikowania jako przezroczysta dziura albo pusty obrys, podczas gdy komentarz recenzenta przy nim zostaje w pliku. PDFlibPas aplikuje adnotacje Redact przez ApplyRedactions: przykryta treść idzie najpierw, nakładka pochodzi z /RO albo z /IC z czernią jako fallback, a słownik znacznika jest opróżniany

Znakowanie jest odwracalne, aplikowanie już nie

ISO 32000-1 §12.5.6.23 dzieli redakcję na identyfikację treści i usuwanie treści. W czasie identyfikacji adnotacja Redact jest tylko propozycją, którą użytkownicy wciąż mogą widzieć, przenosić i przedefiniować, i to umożliwia rundę recenzji. Usuwanie zostawiono aplikacji, z twardymi wymogami: wszystko, co adnotacja identyfikuje, znika, adnotacja znika, a przykryta część każdego obrazu ma zniszczone dane, bo clipping path albo maska obrazu na pikselach się nie liczą. Klucze są wyliczone w tabeli 192 ISO 32000-1 (tabela 195 w ISO 32000-2), i większość z nich opisuje, jak strona ma wyglądać po usunięciu, a nie co usunąć

KluczRola w specyfikacjiCo ApplyRedactions z tym robi
QuadPointsObszar do usunięcia jako n czworokątów; zastępuje Rect jako obszar, gdy jest obecnyCzyści otoczkę każdego czworokąta osobno; tablica o długości niebędącej wielokrotnością 8 wraca do Rect
RectObszar do usunięcia, gdy QuadPoints nieobecneUżywany po znormalizowaniu odwróconych rogów
ICWypełnienie DeviceRGB po usunięciu; brak znaczy przezroczystość; ignorowany, gdy RO istniejeZachowany, gdy jest; dodawany jako czarny, gdy go nie ma
RONakładka Form XObject; ma pierwszeństwo przed IC, OverlayText, DA i QInstalowany jako wygląd normalny i spłaszczany na stronę
OverlayText, RepeatPodpis rysowany nad obszarem, opcjonalnie kafelkowanyRysowany do wygenerowanej nakładki, kafelkowany, gdy Repeat jest true
DA, QFont, rozmiar i kolor podpisu; justowanieRozmiar i kolor czytane z DA, wyrównanie z Q

Tworzenie znaczników przez AddRedactAnnotation

TPDFlib.AddRedactAnnotation(Left, Top, Width, Height, FillR, FillG, FillB, OverlayText, RepeatOverlay) dodaje znacznik do wybranej strony i zwraca nową liczbę adnotacji strony, która jest zarazem indeksem znacznika liczonym od 1, albo 0, gdy nic nie dodano. Współrzędne idą za bieżącym początkiem i jednostkami, więc przykład przełącza się na początek w lewym górnym rogu, gdzie Top to górna krawędź. Wywołanie zapisuje /Rect, /IC ze składników wypełnienia i sztywną /DA /F0 10 Tf 1 1 1 rg, biały tekst 10 punktów; niepusty OverlayText dokłada /OverlayText, /Repeat i /Q 1 dla wyśrodkowanego podpisu. Adnotacje Redact to funkcja PDF 1.7, więc starszy nagłówek jest podciągany do 1.7. Strumień wyglądu nie jest jeszcze zapisywany; oczekujący znacznik wygląda tak, jak rysuje go aplikacja recenzenta. Z podpisem sztywno białym paruj OverlayText z ciemnym wypełnieniem, bo na bladożółtym podpis znika. Znaczniki można tworzyć w każdej edycji 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);            // początek w lewym górnym rogu: Top to górna krawędź
    Pdf.SetMeasurementUnits(0);  // punkty
    if Pdf.SelectPage(1) <> 1 then
      raise Exception.Create('Page 1 not found');
    // Nazwa świadka: czarne wypełnienie, wyśrodkowany biały podpis
    Index := Pdf.AddRedactAnnotation(72, 180, 220, 14, 0, 0, 0, 'REDACTED');
    if Index = 0 then
      raise Exception.Create('Mark was not added');
    // Blok konta: wykafelkuj kod zwolnienia na wyższym polu
    Pdf.AddRedactAnnotation(72, 410, 300, 40, 0, 0, 0, '(b)(6)', 1);
    // Tylko znaczniki; każdy bajt pierwotnej treści wciąż jest obecny
    Pdf.SaveToFile(ReviewFile);
  finally
    Pdf.Free;
  end;
end;

QuadPoints rozstrzygają obszar usuwania; odpowiedzi zachowują tekst

Gdy znacznik niesie /QuadPoints, te czworokąty rozstrzygają, co ApplyRedactions usuwa, a Rect tylko otacza je na potrzeby wyświetlania. Znaczniki robione narzędziami zaznaczania tekstu mają zwykle jeden czworokąt na zaznaczoną linię, więc zmiana rozmiaru takiego znacznika przez SetAnnotRect przesuwa jego ramkę na ekranie i zostawia obszar usuwania tam, gdzie był; SetAnnotQuadPoints to wywołanie, które przesuwa region, a GetAnnotQuadCount zwraca 0 dla znacznika polegającego na samym Rect. Tekst przypięty do znacznika podlega innej regule. Narzędzia recenzji trzymają notatkę w /Contents, a autora w /T, i notatka przy znaczniku redakcji bywa opisem właśnie tego, co jest usuwane („PESEL klienta, potwierdź z prawnikiem”). ApplyRedactions opróżnia własny słownik znacznika, ale odpowiedzi nawązane pod nim przez /IRT to osobne adnotacje, usuwane tylko wtedy, gdy ich prostokąt nachodzi na region redakcji, więc odpowiedź zaparkowana na marginesie zachowuje tekst. Poniższe zestawienie drukuje każdy znacznik z liczbą czworokątów i odpowiedziami nawązanymi pod nim

Anatomia QuadPoints kontra Rect w przepływie redakcji PDFlibPas dla Delphi: każdy czworokąt otacza własny obszar usuwania, a odpowiedź na marginesie zachowuje tekst
quadpoints otaczają usuwanie linia po linii, więc przesunięcie znacznika znaczy SetAnnotQuadPoints, a odpowiedź na marginesie poza regionami zachowuje tekst
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, gdy /IRT nieobecne
        if (Parent > 0) and (Pdf.GetAnnotType(Parent) = 'Redact') then
          Writeln(Format('  reply #%d to #%d: %s',
            [I, Parent, Pdf.GetAnnotContentsEx(I)]));
      end;
  end;
end;

Jaka nakładka pojawia się po ApplyRedactions?

Wpis /RO wygrywa zawsze, gdy jest obecny, jak wymaga reguła pierwszeństwa z tabeli 192. ApplyRedactions instaluje ten Form XObject jako wygląd normalny adnotacji (/AP /N), zrzuca stan wyglądu i spłaszcza go, więc /IC, /OverlayText, /DA i /Q nie mają na ten znacznik wpływu. Rozmieszczenie to jedyny subtelny element. Specyfikacja mówi, że początek nakładki wyrównuje się z lewym dolnym rogiem Rect, podczas gdy spłaszczanie mapuje BBox formy przez jej Matrix na Rect, to samo rozmieszczenie BBox i Matrix, które spłaszczanie stosuje do widgetów pól formularza. Nakładka o BBox [0 0 w h], wymiarowanym do prostokąta, z identycznościowym Matrix wygląda tak samo pod obiema lekturami, więc żądaj tego kształtu, gdy własne narzędzia generują znaczniki /RO

Bez /RO ApplyRedactions normalizuje słownik i buduje wygląd sam. Usuwa /C i /BS i ustawia /Border na [0 0 0]: te wpisy opisują oczekujący znacznik, zwykle kolorowy obrys, i wypalenie go do końcowej strony sprawiłoby, że skończona redakcja wygląda na niezakończoną. Z obrysem poza grą wypełnienie pokrywa cały prostokąt. Podpis jest rysowany wewnątrz wygenerowanej formy, z rozmiarem i kolorem z /DA, justowaniem z /Q, kafelkowaniem z /Repeat, a nazwa fontu F0 związana w zasobach samej formy ze standardowym Helvetica. To związanie ma znaczenie, gdy pliki podróżują między narzędziami: ISO 32000-1 §12.7.3.3 definiuje nazwę fontu w DA jako wpis w domyślnych zasobach formularza interaktywnego (/DR), dokument bez pól formularza nie ma żadnych, i inna aplikacja aplikująca ten sam znacznik może podstawić font zapasowy. PDFlibPas pomija tę wędrówkę, a z Helvetica podpis pasuje tekstowi pisma łacińskiego, jak REDACTED albo kod zwolnienia

Przepływ decyzji nakładki po ApplyRedactions w PDFlibPas dla Delphi: obecne RO wygrywa i spłaszcza, brakujące IC wraca do czerni
RO form XObject jest instalowany jako wygląd normalny i spłaszczany, a znaczniki bez niego dostają przebudowany wygląd i czarne wypełnienie, gdy IC nieobecne

Brakujące /IC staje się czarnym prostokątem

Domyślną specyfikacji dla nieobecnego /IC jest przezroczyste wnętrze, i PDFlibPas świadomie od niej odbiega. Po usunięciu przezroczyste wnętrze pokazuje papier, białą dziurę tam, gdzie kiedyś była nazwa, i ta dziura wygląda tak samo jak układ, który nigdy tam nic nie miał. Czytnik nie odróżni zredagowanego od pustego, a recenzent nie potwierdzi, że redakcja nastąpiła, bez porównania wyciągniętego tekstu. Pusta rama — to, co krok aplikowania produkuje, gdy spłaszcza oczekujący wygląd znacznika będącego samym obrysem — zawodzi inaczej: czyta się jako propozycja, na którą nikt nie zareagował. Rozważyliśmy dosłowne pójście za domyślną i wybraliśmy czarny prostokąt, bo widoczny znak, że coś zostało wstrzymane, jest częścią tego, co zredagowany dokument mówi swojemu czytelnikowi, a wybór dotyczy tylko znaczników, których autor nigdy nie podał wypełnienia. /IC już obecny w słowniku jest używany niezależnie od wartości, więc producent, który zażądał bieli, dostaje biel

Co znika, zanim nakładka zostanie narysowana

ApplyRedactions działa strona po stronie i dokańcza całe usuwanie, zanim spłaszczy jakąkolwiek nakładkę, więc nakładka nigdy nie może zostać złapana przez własny region. Dla każdej strony zbiera po jednym prostokącie na czworokąt, albo Rect znaczników bez czworokątów, i szoruje każdą warstwę treści przeciw każdemu z nich:

  • Tekst jest wycinany glif po glifie, mierzony z fontów strony i pełnego stanu tekstu, a reszta częściowo przykrytego łańcucha zostaje na pierwotnej pozycji
  • Wektorowe ścieżki, obrazy inline i cieniowania malujące wewnątrz regionu są usuwane ze strumienia treści
  • Form XObjecty są odwiedzane rekurencyjnie; forma, którą rysują też inne strony albo rozmieszczenia, jest redagowana przez prywatną kopię, więc tamte strony zachowują oryginał
  • Image XObjecty tracą tylko przykryte piksele, nadpisywane czernią (pełny atrament K dla CMYK); obrazy JPEG, JPEG 2000, JBIG2 i CCITT są dekodowane i odkładane z powrotem jako bezstratne strumienie Flate
  • W tagged PDF /Alt i /ActualText przypięte do usuwanej oznaczonej treści są czyszczone w treści strony i w drzewie struktury
  • Inne adnotacje, których prostokąt styka się z regionem, są usuwane, a pola formularzy, których widgety tam siedzą, tracą wartości

Krok pikseli to punkt obrazowy specyfikacji w praktyce. Czarny prostokąt namalowany nad zeskanowanym paszportem zadowala oko i wciąż zawala §12.5.6.23, bo pierwotne próbki dekodują, skoro tylko ktoś wyciągnie obraz. Ten sam silnik siedzi za RedactRegion, który usuwa treść z prostokąta podanego wprost i chodzi w parze z SanitizeDocument, bez czytania jakichkolwiek adnotacji. Gdy treść już zniknie, każdy znacznik jest spłaszczany, a jego słownik czyszczony do /Type i /Subtype. Spłaszczanie usuwa adnotację z tablicy /Annots strony, ale słownik wciąż jest obiektem w dokumencie, i zapis, który go zapisze, wręczy /Contents, /RC, /T i resztę śladu recenzji odbiorcy

Zapis bez niezredagowanej rewizji

Zapis przyrostowy unieważnia wszystko powyżej. Na mocy §7.5.6 (ten sam numer w ISO 32000-1 i ISO 32000-2) aktualizacja przyrostowa dokleja zmienione obiekty na koniec pliku i zostawia pierwotne bajty nietknięte, więc pierwotna rewizja, z treścią stron, obrazami i oznaczonymi znacznikami, pozostaje czytelna dla każdego, kto utnie plik przy wcześniejszym %%EOF; ExtractDocumentRevision w edycji Small Business PDFlibPas robi to jednym wywołaniem. Po ApplyRedactions zapisz nowy plik przez SaveToFile, nigdy SaveChanges, który dokleja. Przed zapisem GarbageCollectObjects przesiewa tabelę obiektów i wyrzuca obiekty, których nic już nie referencjonuje, jak strumienie wyglądu, które znaczniki nosiły przed krokiem aplikowania; pełny zapis pomija ten przesiew, chyba że SetGarbageCollect go włączy, albo ustawione jest pole GarbageCollect rekordu TPDFlibSaveOptions przekazywanego do SaveToFileOptions. Metadane to pozostała dziura. SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET) zamazuje Author, Subject, Keywords, Creator i Producer i usuwa strumień XMP, zostawiając Title do twojego sprawdzenia. Unikaj tu SANITIZE_ALL: zawiera SANITIZE_ANNOTATIONS i usuwa każdą adnotację pozostałą w dokumencie, w tym notatki, które miałeś zachować

Os czasu zapisu po ApplyRedactions w PDFlibPas dla Delphi: przyrostowe doklejenie zostawia oryginał odzyskiwalny, więc release robi sanitize, garbage collect i pełne przepisanie
po aplikowaniu ścieżka release to sanitize metadanych, przesiew niereferencjonowanych obiektów i zapis nowego pliku, bo zapis przyrostowy trzyma niezredagowaną rewizję czytelną

ApplyRedactions, jak RedactRegion, jest dostępny w edycji Enterprise. Na gołym TPDFlib zwraca 0 i ustawia LastErrorCode na PDFLIB_ERROR_ENTERPRISE_REQUIRED (900), więc krok release używa TPDFlibEnterprise z jednostki 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;   // każdy znacznik Redact na każdej stronie
    if Applied = 0 then
      raise Exception.CreateFmt('No marks applied (error %d)',
        [Pdf.LastErrorCode]);
    // Info i XMP wciąż mogą nazywać to, czego strony już nie pokazują
    Pdf.SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET);
    Orphans := Pdf.GarbageCollectObjects;
    // Pełne przepisanie do nowego pliku; nigdy SaveChanges po redakcji
    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;

Oczekiwane: ApplyRedactions zwraca liczbę znaczników Redact spłaszczonych przez wszystkie strony, 2 dla kopii recenzenckiej zapisanej przez MarkForReview, a GarbageCollectObjects zwraca, ile niereferencjonowanych obiektów przesiew wyrzucił

Jakiekolwiek narzędzie narysowało znacznik, jego wpisy /RO i /IC rozstrzygają, co odbiorca widzi na stronie, podczas gdy krok aplikowania w PDFlibPas Enterprise for Delphi rozstrzyga, co zniknęło pod spodem