Article technique

Annotations Redact : ApplyRedactions, /RO et /IC en Delphi

Une marque de caviardage dessinée sans couleur /IC peut sortir de l'étape d'application comme un trou transparent ou un contour creux, pendant que le commentaire du relecteur posé sur la marque reste dans le fichier. PDFlibPas applique les annotations Redact avec ApplyRedactions : le contenu couvert part d'abord, le recouvrement vient de /RO ou de /IC avec le noir en repli, et le dictionnaire de la marque est vidé

Marquer est réversible, appliquer ne l'est pas

ISO 32000-1 §12.5.6.23 partage le caviardage entre identification de contenu et suppression de contenu. Pendant l'identification, une annotation Redact n'est qu'une proposition que les utilisateurs peuvent encore voir, déplacer et redéfinir, et c'est ce qui rend possible un tour de relecture. La suppression est laissée à l'application, avec des exigences fermes : tout ce que l'annotation identifie part, l'annotation part, et la partie couverte de toute image voit ses données détruites, car un chemin de découpage ou un masque d'image par-dessus les pixels ne qualifie pas. Les clés sont listées dans la Table 192 de ISO 32000-1 (Table 195 en ISO 32000-2), et la plupart décrivent ce à quoi la page doit ressembler après suppression plutôt que quoi supprimer

CléRôle dans la spécificationCe que ApplyRedactions en fait
QuadPointsRégion à supprimer comme n quadrilatères ; remplace Rect comme région quand présentDécape la boîte englobante de chaque quadrilatère séparément ; un tableau dont la longueur n'est pas un multiple de 8 retombe sur Rect
RectRégion à supprimer quand QuadPoints est absentUtilisé avec les coins inversés normalisés
ICRemplissage DeviceRGB après suppression ; absent signifie transparent ; ignoré quand RO existeConservé quand présent, ajouté en noir quand absent
RORecouvrement Form XObject ; prime sur IC, OverlayText, DA et QInstallé comme apparence normale et aplati sur la page
OverlayText, RepeatÉtiquette dessinée par-dessus la région, éventuellement en pavageDessinée dans le recouvrement généré, en pavage quand Repeat est true
DA, QPolice, taille et couleur de l'étiquette ; justificationTaille et couleur lues dans DA, alignement depuis Q

Créer des marques avec AddRedactAnnotation

TPDFlib.AddRedactAnnotation(Left, Top, Width, Height, FillR, FillG, FillB, OverlayText, RepeatOverlay) ajoute une marque à la page sélectionnée et renvoie le nouveau nombre d'annotations de la page, qui est aussi l'index base 1 de la marque, ou 0 quand rien n'a été ajouté. Les coordonnées suivent l'origine et les unités courantes, si bien que l'exemple bascule vers l'origine en haut à gauche, où Top est le bord supérieur. L'appel écrit un /Rect, un /IC depuis les composantes de remplissage et un /DA fixe de /F0 10 Tf 1 1 1 rg, du texte blanc 10 points ; un OverlayText non vide ajoute /OverlayText, /Repeat et /Q 1 pour une étiquette centrée. Les annotations Redact sont une fonctionnalité PDF 1.7, si bien qu'un en-tête plus ancien est monté à 1.7. Aucun flux d'apparence n'est encore écrit ; la marque en attente a l'aspect que l'application du relecteur lui dessine. Avec l'étiquette fixée au blanc, associez OverlayText à un remplissage sombre, car sur jaune pâle l'étiquette disparaît. Les marques peuvent être créées dans chaque édition de 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 en haut à gauche : Top est le bord supérieur
    Pdf.SetMeasurementUnits(0);  // points
    if Pdf.SelectPage(1) <> 1 then
      raise Exception.Create('Page 1 not found');
    // Nom du témoin : remplissage noir, étiquette blanche centrée
    Index := Pdf.AddRedactAnnotation(72, 180, 220, 14, 0, 0, 0, 'REDACTED');
    if Index = 0 then
      raise Exception.Create('Mark was not added');
    // Bloc compte : paver le code d'exemption sur une boîte plus haute
    Pdf.AddRedactAnnotation(72, 410, 300, 40, 0, 0, 0, '(b)(6)', 1);
    // Marques seulement ; chaque octet du contenu original est toujours présent
    Pdf.SaveToFile(ReviewFile);
  finally
    Pdf.Free;
  end;
end;

Les QuadPoints décident de la zone de suppression ; les réponses gardent leur texte

Dès qu'une marque porte /QuadPoints, ces quadrilatères décident de ce que ApplyRedactions supprime, et le Rect ne fait que les borner pour l'affichage. Les marques faites avec des outils de sélection de texte ont d'ordinaire un quadrilatère par ligne sélectionnée, si bien que redimensionner une telle marque avec SetAnnotRect déplace son cadre à l'écran et laisse la zone de suppression où elle était ; SetAnnotQuadPoints est l'appel qui déplace la région, et GetAnnotQuadCount renvoie 0 pour une marque qui ne compte que sur son Rect. Le texte attaché à une marque suit une autre règle. Les outils de relecture gardent la note dans /Contents et l'auteur dans /T, et une note sur une marque de caviardage tend à décrire précisément la chose en train d'être supprimée (« numéro de sécurité sociale du client, à confirmer avec le juriste »). ApplyRedactions vide le dictionnaire propre de la marque, mais les réponses enfilées sous elle au travers de /IRT sont des annotations séparées, supprimées seulement quand leur rectangle chevauche une région de caviardage, si bien qu'une réponse garée dans la marge garde son texte. Le listing ci-dessous imprime chaque marque avec son nombre de quadrilatères et les réponses enfilées sous elle

Anatomie QuadPoints contre Rect dans le flux de caviardage PDFlibPas pour Delphi : chaque quadrilatère borne sa propre zone de suppression tandis qu'une réponse en marge garde son texte
les quadpoints bornent la suppression une ligne à la fois, si bien que déplacer la marque veut dire SetAnnotQuadPoints, et une réponse en marge hors des régions garde son texte
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 quand /IRT est absent
        if (Parent > 0) and (Pdf.GetAnnotType(Parent) = 'Redact') then
          Writeln(Format('  reply #%d to #%d: %s',
            [I, Parent, Pdf.GetAnnotContentsEx(I)]));
      end;
  end;
end;

Quel recouvrement apparaît après ApplyRedactions ?

Une entrée /RO gagne dès qu'elle est présente, comme l'exige la règle de précédence de la Table 192. ApplyRedactions installe ce Form XObject comme apparence normale de l'annotation (/AP /N), abandonne tout état d'apparence et l'aplatit, si bien que /IC, /OverlayText, /DA et /Q n'ont aucun effet sur cette marque. Le placement est la seule partie subtile. La spécification dit que l'origine du recouvrement s'aligne sur le coin inférieur gauche du Rect, tandis que l'aplatissement mappe le BBox du Form au travers de son Matrix sur le Rect, le même placement BBox et Matrix que l'aplatissement applique aux widgets de champs de formulaire. Un recouvrement dont le BBox vaut [0 0 w h], dimensionné sur le rectangle, avec un Matrix identité, se ressemble sous les deux lectures, donc demandez cette forme quand vos propres outils génèrent des marques /RO

Sans /RO, ApplyRedactions normalise le dictionnaire et construit l'apparence lui-même. Il supprime /C et /BS et pose /Border à [0 0 0] : ces entrées décrivent la marque en attente, d'ordinaire un contour coloré, et le brûler dans la page finale ferait passer un caviardage achevé pour inachevé. La bordure partie, le remplissage couvre tout le rectangle. L'étiquette est dessinée à l'intérieur du Form généré, avec la taille et la couleur de /DA, la justification de /Q, le pavage de /Repeat, et le nom de police F0 lié dans les ressources propres de ce Form à la Helvetica standard. Cette liaison compte quand les fichiers voyagent entre outils : ISO 32000-1 §12.7.3.3 définit un nom de police DA comme une entrée dans les ressources par défaut du formulaire interactif (/DR), un document sans champs de formulaire n'en a aucune, et une autre application appliquant la même marque peut substituer une police de repli. PDFlibPas saute cette recherche, et avec Helvetica l'étiquette convient au texte en écriture latine tel que REDACTED ou un code d'exemption

Flux de décision du recouvrement après ApplyRedactions dans PDFlibPas pour Delphi : un RO présent gagne et s'aplatit, un IC absent retombe sur le noir
un Form XObject RO est installé comme apparence normale et aplati, tandis que les marques sans en reçoivent une apparence reconstruite et un remplissage noir quand IC est absent

Un /IC manquant devient une boîte noire

Le défaut de la spécification pour un /IC absent est un intérieur transparent, et PDFlibPas s'en écarte à dessein. Après suppression, un intérieur transparent montre le papier, un blanc à l'endroit où un nom se trouvait, et ce blanc se confond avec une mise en page qui n'y a jamais rien eu. Un lecteur ne peut pas distinguer caviardé de vide, et un relecteur ne peut pas confirmer que le caviardage a eu lieu sans comparer le texte extrait. Un cadre creux, ce que produit une étape d'application quand elle aplatit l'apparence en attente d'une marque à contour seul, échoue autrement : il se lit comme une proposition sur laquelle personne n'a agi. Nous avons pesé le suivi littéral du défaut et choisi la boîte noire, car le signe visible que quelque chose a été retiré fait partie de ce qu'un document caviardé dit à son lecteur, et le choix ne touche que les marques dont l'auteur n'a jamais énoncé de remplissage. Un /IC déjà dans le dictionnaire est utilisé quelle que soit sa valeur, si bien qu'un producteur qui a demandé du blanc obtient du blanc

Ce qui a disparu avant que le recouvrement soit dessiné

ApplyRedactions travaille page par page et achève toute la suppression avant d'aplatir un recouvrement quelconque, si bien qu'un recouvrement ne peut jamais être attrapé par sa propre région. Pour chaque page il ramasse un rectangle par quadrilatère, ou le Rect des marques sans quadrilatères, et décape chaque couche de contenu contre chacun d'eux :

  • Le texte est coupé glyphe par glyphe, mesuré depuis les polices de la page et l'état de texte complet, et le reste d'une chaîne partiellement couverte reste à sa position d'origine
  • Les tracés vectoriels, les images en ligne et les shadings qui peignent à l'intérieur de la région sont supprimés du flux de contenu
  • Les Form XObjects sont entrés récursivement ; un Form que d'autres pages ou placements dessinent aussi est caviardé au travers d'une copie privée, si bien que ces pages gardent l'original
  • Les image XObjects ne perdent que les pixels couverts, écrasés en noir (encre K pleine pour le CMYK) ; les images JPEG, JPEG 2000, JBIG2 et CCITT sont décodées et re-stockées comme flux Flate sans perte
  • Dans le PDF balisé, /Alt et /ActualText attachés au contenu marqué supprimé sont effacés dans le contenu de page et dans l'arbre de structure
  • Les autres annotations dont le rectangle rencontre la région sont supprimées, et les champs de formulaire dont les widgets se tiennent là perdent leurs valeurs

L'étape des pixels est la clause image de la spécification en pratique. Un rectangle noir peint par-dessus un passeport scanné satisfait l'œil et échoue quand même à la §12.5.6.23, car les échantillons originaux se décodent dès que quelqu'un extrait l'image. Le même moteur se cache derrière RedactRegion, qui supprime le contenu d'un rectangle que vous passez directement et fait couple avec SanitizeDocument, sans lire aucune annotation. Une fois le contenu parti, chaque marque est aplatie et son dictionnaire vidé jusqu'à /Type et /Subtype. L'aplatissement retire l'annotation du tableau /Annots de la page, mais le dictionnaire reste un objet dans le document, et une sauvegarde qui l'écrirait remettrait /Contents, /RC, /T et le reste de la piste de relecture au destinataire

Sauvegarder sans la révision non caviardée

Une sauvegarde incrémentale défait tout ce qui précède. Sous §7.5.6 (le même numéro en ISO 32000-1 et ISO 32000-2), une mise à jour incrémentale ajoute les objets modifiés à la fin du fichier et laisse les octets originaux intacts, si bien que la révision originale, avec son contenu de page, ses images et ses marques annotées, reste lisible par quiconque coupe le fichier au %%EOF antérieur ; ExtractDocumentRevision de l'édition Small Business de PDFlibPas fait cela en un appel. Après ApplyRedactions, écrivez un nouveau fichier avec SaveToFile, jamais SaveChanges, qui ajoute. Avant la sauvegarde, GarbageCollectObjects balaie la table d'objets et abandonne les objets que plus rien ne référence, tels les flux d'apparence que les marques portaient avant l'étape d'application ; une sauvegarde complète saute ce balayage à moins que SetGarbageCollect ne l'active, ou que le champ GarbageCollect de l'enregistrement TPDFlibSaveOptions que vous passez à SaveToFileOptions ne soit posé. Les métadonnées sont le trou restant. SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET) blanchit Author, Subject, Keywords, Creator et Producer et retire le flux XMP, en laissant le Title à vérifier pour vous. Évitez SANITIZE_ALL ici : il inclut SANITIZE_ANNOTATIONS et supprime chaque annotation restée dans le document, les notes que vous vouliez garder comprises

Chronologie de sauvegarde après ApplyRedactions dans PDFlibPas pour Delphi : un ajout incrémental laisse l'original récupérable, si bien que la publication enchaîne sanitize, garbage collect et une réécriture complète
après l'application le chemin de publication est sanitize des métadonnées, balayage des objets non référencés et sauvegarde d'un nouveau fichier, car une sauvegarde incrémentale garde la révision non caviardée lisible

ApplyRedactions, comme RedactRegion, est disponible dans l'édition Enterprise. Sur un TPDFlib simple il renvoie 0 et pose LastErrorCode à PDFLIB_ERROR_ENTERPRISE_REQUIRED (900), si bien que l'étape de publication utilise TPDFlibEnterprise de l'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;   // chaque marque Redact sur chaque page
    if Applied = 0 then
      raise Exception.CreateFmt('No marks applied (error %d)',
        [Pdf.LastErrorCode]);
    // Info et XMP peuvent encore nommer ce que les pages ne montrent plus
    Pdf.SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET);
    Orphans := Pdf.GarbageCollectObjects;
    // Réécriture complète vers un nouveau fichier ; jamais SaveChanges après caviardage
    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;

Attendu : ApplyRedactions renvoie le nombre de marques Redact qu'il a aplaties sur toutes les pages, 2 pour la copie de relecture écrite par MarkForReview, et GarbageCollectObjects renvoie combien d'objets non référencés le balayage a abandonnés

Quel que soit l'outil qui a dessiné une marque, ses entrées /RO et /IC règlent ce que le destinataire voit sur la page, tandis que l'étape d'application dans PDFlibPas Enterprise for Delphi règle ce qui a disparu dessous