Articolo tecnico

Redact nel PDF: contrassegni, applicazione e overlay /RO

Un contrassegno di oscuramento disegnato senza un colore /IC può uscire dalla fase di applicazione come un varco trasparente o un contorno vuoto, mentre il commento del revisore su di esso resta nel file. PDFlibPas applica le annotazioni Redact con ApplyRedactions: il contenuto coperto va prima, l'overlay arriva da /RO o da /IC con il nero come ripiego, e il dizionario del contrassegno viene svuotato

Contrassegnare è reversibile, applicare no

ISO 32000-1 §12.5.6.23 divide l'oscuramento in identificazione del contenuto e rimozione del contenuto. Durante l'identificazione una annotazione Redact è solo una proposta che gli utenti possono ancora vedere, spostare e ridefinire, ed è ciò che rende possibile un giro di revisione. La rimozione è lasciata all'applicazione, con requisiti fermi: tutto ciò che l'annotazione identifica va via, l'annotazione va via, e la parte coperta di ogni immagine ha i suoi dati distrutti, perché un percorso di ritaglio o una image mask sopra i pixel non valgono. Le chiavi sono elencate nella Table 192 di ISO 32000-1 (Table 195 in ISO 32000-2), e la maggior parte descrive come dovrebbe apparire la pagina dopo la rimozione invece di che cosa rimuovere

ChiaveRuolo nella specificaChe cosa ci fa ApplyRedactions
QuadPointsRegione da rimuovere come n quadrilateri; sostituisce Rect come regione quando presenteElimina il bounding box di ogni quadrilatero separatamente; un array la cui lunghezza non è multiplo di 8 ripiega su Rect
RectRegione da rimuovere quando QuadPoints è assenteUsato con gli angoli invertiti normalizzati
ICRiempimento DeviceRGB dopo la rimozione; assente significa trasparente; ignorato quando esiste ROConservato quando presente, aggiunto come nero quando assente
ROOverlay di Form XObject; ha precedenza su IC, OverlayText, DA e QInstallato come appearance normale e appiattito sulla pagina
OverlayText, RepeatEtichetta disegnata sopra la regione, opzionalmente ripetutaDisegnata nella overlay generata, ripetuta quando Repeat è true
DA, QFont, dimensione e colore dell'etichetta; giustificazioneDimensione e colore letti da DA, allineamento da Q

Creare contrassegni con AddRedactAnnotation

TPDFlib.AddRedactAnnotation(Left, Top, Width, Height, FillR, FillG, FillB, OverlayText, RepeatOverlay) aggiunge un contrassegno alla pagina selezionata e restituisce il nuovo numero di annotazioni della pagina, che è anche l'indice 1-based del contrassegno, oppure 0 quando nulla è stato aggiunto. Le coordinate seguono l'origine e le unità correnti, così l'esempio passa all'origine in alto a sinistra, dove Top è il bordo superiore. La chiamata scrive /Rect, una /IC dalle componenti del riempimento e un /DA fisso di /F0 10 Tf 1 1 1 rg, testo bianco a 10 punti; un OverlayText non vuoto aggiunge /OverlayText, /Repeat e /Q 1 per un'etichetta centrata. Le annotazioni Redact sono una funzionalità di PDF 1.7, così un'intestazione più vecchia viene portata a 1.7. Nessun appearance stream viene ancora scritto; il contrassegno in sospeso appare come lo disegna l'applicazione del revisore. Con l'etichetta fissata al bianco, abbinate OverlayText a un riempimento scuro, dato che sul giallo pallido l'etichetta sparisce. I contrassegni possono essere creati in ogni edizione di 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);            // origine in alto a sinistra: Top è il bordo superiore
    Pdf.SetMeasurementUnits(0);  // punti
    if Pdf.SelectPage(1) <> 1 then
      raise Exception.Create('Page 1 not found');
    // Nome del teste: riempimento nero, etichetta bianca centrata
    Index := Pdf.AddRedactAnnotation(72, 180, 220, 14, 0, 0, 0, 'REDACTED');
    if Index = 0 then
      raise Exception.Create('Mark was not added');
    // Blocco conto: ripete il codice di esenzione su un box più alto
    Pdf.AddRedactAnnotation(72, 410, 300, 40, 0, 0, 0, '(b)(6)', 1);
    // Solo contrassegni; ogni byte del contenuto originale è ancora presente
    Pdf.SaveToFile(ReviewFile);
  finally
    Pdf.Free;
  end;
end;

I QuadPoints decidono la zona di rimozione; le risposte conservano il loro testo

Quando un contrassegno porta /QuadPoints, sono quei quadrilateri a decidere cosa ApplyRedactions rimuove, e Rect li delimita solo per la visualizzazione. I contrassegni fatti con strumenti di selezione testo hanno di solito un quadrilatero per riga selezionata, così ridimensionare un tale contrassegno con SetAnnotRect sposta la sua cornice sullo schermo e lascia la zona di rimozione dov'era; SetAnnotQuadPoints è la chiamata che sposta la regione, e GetAnnotQuadCount restituisce 0 per un contrassegno che si affida al solo Rect. Il testo allegato a un contrassegno segue una regola diversa. Gli strumenti di revisione tengono la nota in /Contents e l'autore in /T, e una nota su un contrassegno di oscuramento tende a descrivere proprio la cosa che viene rimossa ("SSN del cliente, conferma con l'ufficio legale"). ApplyRedactions svuota il dizionario del contrassegno stesso, ma le risposte in thread sotto di esso tramite /IRT sono annotazioni separate, cancellate solo quando il loro rettangolo sovrappone una regione di oscuramento, così una risposta parcheggiata nel margine conserva il suo testo. Il listato qui sotto stampa ogni contrassegno con il conteggio dei suoi quadrilateri e le risposte in thread sotto di esso

Anatomia di QuadPoints contro Rect nel flusso di oscuramento di PDFlibPas per Delphi: ogni quadrilatero delimita la propria zona di rimozione mentre una risposta nel margine conserva il suo testo
i quadpoints delimitano la rimozione una riga alla volta, così spostare il contrassegno significa SetAnnotQuadPoints e una risposta nel margine fuori dalle regioni conserva il suo testo
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 è assente
        if (Parent > 0) and (Pdf.GetAnnotType(Parent) = 'Redact') then
          Writeln(Format('  reply #%d to #%d: %s',
            [I, Parent, Pdf.GetAnnotContentsEx(I)]));
      end;
  end;
end;

Quale overlay appare dopo ApplyRedactions?

Una voce /RO vince ogni volta che è presente, come richiede la regola di precedenza della Table 192. ApplyRedactions installa quel Form XObject come appearance normale dell'annotazione (/AP /N), elimina qualsiasi stato di appearance e lo appiattisce, così /IC, /OverlayText, /DA e /Q non hanno effetto su quel contrassegno. Il piazzamento è la parte più sottile. La specifica dice che l'origine della overlay si allinea con l'angolo inferiore sinistro di Rect, mentre l'appiattimento mappa la BBox della Form attraverso la sua Matrix sul Rect, lo stesso piazzamento BBox e Matrix che l'appiattimento applica ai widget dei campi modulo. Una overlay la cui BBox è [0 0 w h], dimensionata sul rettangolo, con una Matrix identità appare uguale sotto entrambe le letture, così richiedete quella forma quando i vostri strumenti generano contrassegni /RO

Senza /RO, ApplyRedactions normalizza il dizionario e costruisce l'appearance da sé. Elimina /C e /BS e imposta /Border a [0 0 0]: quelle voci descrivono il contrassegno in sospeso, di solito un contorno colorato, e inciderlo nella pagina finale farebbe sembrare incompiuto un oscuramento finito. Con il bordo via, il riempimento copre l'intero rettangolo. L'etichetta viene disegnata dentro la Form generata, con dimensione e colore da /DA, giustificazione da /Q, ripetizione da /Repeat, e il nome di font F0 legato nelle risorse proprie di quella Form alla Helvetica standard. Quel legame conta quando i file viaggiano tra strumenti: ISO 32000-1 §12.7.3.3 definisce un nome di font DA come una voce nelle risorse predefinite del modulo interattivo (/DR), un documento senza campi modulo non ne ha, e un'altra applicazione che applichi lo stesso contrassegno può sostituire un font di ripiego. PDFlibPas salta quella ricerca, e con Helvetica l'etichetta si addice a testo in scrittura latina come REDACTED o un codice di esenzione

Flusso di decisione della overlay dopo ApplyRedactions in PDFlibPas per Delphi: una RO presente vince e si appiattisce, una IC assente ripiega sul nero
una RO form XObject viene installata come appearance normale e appiattita, mentre i contrassegni senza ne ricevono un appearance ricostruito e un riempimento nero quando la IC è assente

Una /IC assente diventa un riquadro nero

Il default della specifica per una /IC assente è un interno trasparente, e PDFlibPas se ne discosta di proposito. Dopo la rimozione, un interno trasparente mostra la carta, un varco bianco dove prima c'era un nome, e quel varco appare uguale a un impaginato che non ha mai avuto nulla lì. Un lettore non distingue oscurato da bianco, e un revisore non può confermare che l'oscuramento sia avvenuto senza confrontare il testo estratto. Una cornice vuota, che è ciò che produce una fase di applicazione quando appiattisce l'appearance in sospeso di un contrassegno solo contorno, fallisce diversamente: si legge come una proposta su cui nessuno ha agito. Abbiamo pesato di seguire il default alla lettera e abbiamo scelto il riquadro nero, perché il segno visibile che qualcosa è stato trattenuto fa parte di ciò che un documento oscurato dice al suo lettore, e la scelta tocca solo i contrassegni il cui autore non ha mai dichiarato un riempimento. Una /IC già nel dizionario viene usata qualunque sia il suo valore, così un produttore che ha chiesto il bianco ottiene il bianco

Che cosa non c'è più prima che la overlay venga disegnata

ApplyRedactions lavora pagina per pagina e completa tutta la rimozione prima di appiattire una qualsiasi overlay, così una overlay non può mai essere colta dalla propria regione. Per ogni pagina raccoglie un rettangolo per quadrilatero, o il Rect dei contrassegni senza quadrilateri, e ripulisce ogni layer di contenuto contro ciascuno di loro:

  • Il testo viene tagliato glifo per glifo, misurato dai font di pagina e dallo stato di testo completo, e il resto di una stringa parzialmente coperta resta alla sua posizione originale
  • Percorsi vettoriali, immagini inline e shading che dipingono dentro la regione vengono eliminati dal content stream
  • Si entra nei Form XObject in modo ricorsivo; una Form che altre pagine o piazzamenti disegnano pure viene oscurata attraverso una copia privata, così quelle pagine conservano l'originale
  • Gli image XObject perdono solo i pixel coperti, sovrascritti con nero (inchiostro K pieno per CMYK); le immagini JPEG, JPEG 2000, JBIG2 e CCITT vengono decodificate e rimemorizzate come stream Flate senza perdita
  • Nel PDF taggato, /Alt e /ActualText allegati al marked content rimosso vengono cancellati nel contenuto di pagina e nell'albero delle strutture
  • Le altre annotazioni il cui rettangolo tocca la regione vengono eliminate, e i campi modulo i cui widget stanno lì perdono i loro valori

Il passo dei pixel è la clausola immagini della specifica in pratica. Un rettangolo nero dipinto sopra un passaporto scansionato soddisfa l'occhio e boccia comunque §12.5.6.23, perché i campioni originali si decodificano appena qualcuno estrae l'immagine. Lo stesso motore sta dietro a RedactRegion, che rimuove contenuto da un rettangolo che passate direttamente e fa coppia con SanitizeDocument, senza leggere alcuna annotazione. Una volta che il contenuto è andato, ogni contrassegno viene appiattito e il suo dizionario pulito fino a /Type e /Subtype. L'appiattimento rimuove l'annotazione dall'array /Annots della pagina, ma il dizionario resta comunque un oggetto nel documento, e un salvataggio che lo scrivesse consegnerebbe /Contents, /RC, /T e il resto della traccia di revisione al destinatario

Salvare senza la revisione non oscurata

Un salvataggio incrementale disfa tutto quanto sopra. Ai sensi di §7.5.6 (lo stesso numero in ISO 32000-1 e ISO 32000-2), un aggiornamento incrementale accoda gli oggetti cambiati in coda al file e lascia i byte originali intatti, così la revisione originale, con il suo contenuto di pagina, immagini e contrassegni annotati, resta leggibile da chiunque tagli il file al %%EOF precedente; ExtractDocumentRevision nell'edizione Small Business di PDFlibPas lo fa in una chiamata. Dopo ApplyRedactions, scrivete un nuovo file con SaveToFile, mai SaveChanges, che accoda. Prima di salvare, GarbageCollectObjects passa in rassegna la tabella degli oggetti e butta gli oggetti che più nulla referenzia, come gli appearance stream che i contrassegni portavano prima della fase di applicazione; un salvataggio completo salta quella passata a meno che SetGarbageCollect la abiliti, oppure il campo GarbageCollect del record TPDFlibSaveOptions che passate a SaveToFileOptions sia impostato. I metadati sono il buco che resta. SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET) cancella Author, Subject, Keywords, Creator e Producer e rimuove lo stream XMP, lasciando il Title da controllare a voi. Evitate SANITIZE_ALL qui: include SANITIZE_ANNOTATIONS ed elimina ogni annotazione rimasta nel documento, tra cui le note che intendevate conservare

Timeline di salvataggio dopo ApplyRedactions in PDFlibPas per Delphi: un accodamento incrementale lascia l'originale recuperabile, così il rilascio esegue sanitize, garbage collect e una riscrittura completa
dopo la fase di applicazione il percorso di rilascio è sanitizzare i metadati, ripulire gli oggetti non referenziati e salvare un nuovo file, perché un salvataggio incrementale tiene la revisione non oscurata leggibile

ApplyRedactions, come RedactRegion, è disponibile nell'edizione Enterprise. Su un TPDFlib semplice restituisce 0 e imposta LastErrorCode a PDFLIB_ERROR_ENTERPRISE_REQUIRED (900), così il passo di rilascio usa TPDFlibEnterprise dall'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;   // ogni contrassegno Redact su ogni pagina
    if Applied = 0 then
      raise Exception.CreateFmt('No marks applied (error %d)',
        [Pdf.LastErrorCode]);
    // Info e XMP possono ancora nominare ciò che le pagine non mostrano più
    Pdf.SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET);
    Orphans := Pdf.GarbageCollectObjects;
    // Riscrittura completa su un nuovo file; mai SaveChanges dopo la rimozione
    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;

Atteso: ApplyRedactions restituisce il numero di contrassegni Redact appiattiti su tutte le pagine, 2 per la copia di revisione scritta da MarkForReview, e GarbageCollectObjects restituisce quanti oggetti non referenziati ha buttato la passata

Qualunque strumento abbia disegnato un contrassegno, le sue voci /RO e /IC stabiliscono cosa vede il destinatario sulla pagina, mentre la fase di applicazione in PDFlibPas Enterprise for Delphi stabilisce cosa è andato via sotto