Technisch artikel

PDF Redact-annotaties: markeren, toepassen en de /RO-overlay

Een redactiemark getekend zonder een /IC-kleur kan uit de apply-stap komen als een transparant gat of een holle omlijning, terwijl de opmerking van de reviewer erop in het bestand blijft. PDFlibPas past Redact-annotaties toe met ApplyRedactions: de gedekte content gaat eerst weg, de overlay komt uit /RO of uit /IC met zwart als fallback, en het dictionary van de mark wordt geleegd

Markeren is omkeerbaar, toepassen niet

ISO 32000-1 §12.5.6.23 splitst redactie in contentidentificatie en contentverwijdering. Tijdens de identificatie is een Redact-annotatie slechts een voorstel dat gebruikers nog kunnen zien, verplaatsen en herdefiniëren, en dat is wat een reviewronde mogelijk maakt. De verwijdering is aan de applicatie, met harde eisen: alles wat de annotatie identificeert gaat weg, de annotatie gaat weg, en het gedekte deel van elke afbeelding krijgt zijn data vernietigd, want een clipping path of image mask over de pixels volstaat niet. De sleutels staan in Table 192 van ISO 32000-1 (Table 195 in ISO 32000-2), en de meeste beschrijven hoe de pagina er na de verwijdering uit hoort te zien in plaats van wat er weg moet

SleutelRol in de specificatieWat ApplyRedactions ermee doet
QuadPointsTe verwijderen gebied als n vierhoeken; vervangt Rect als gebied wanneer aanwezigScrubt de bounding box van elke vierhoek apart; een array waarvan de lengte geen veelvoud van 8 is valt terug op Rect
RectTe verwijderen gebied wanneer QuadPoints ontbreektGebruikt met omgekeerde hoeken genormaliseerd
ICDeviceRGB-vulling na verwijdering; afwezig betekent transparant; genegeerd wanneer RO bestaatBehouden wanneer aanwezig, als zwart toegevoegd wanneer afwezig
ROForm XObject-overlay; gaat boven IC, OverlayText, DA en QGeïnstalleerd als de normal appearance en op de pagina afgevlakt
OverlayText, RepeatLabel boven het gebied getekend, optioneel getegeldIn de gegenereerde overlay getekend, getegeld wanneer Repeat true is
DA, QFont, grootte en kleur van het label; uitlijningGrootte en kleur uit DA gelezen, uitlijning uit Q

Marks aanmaken met AddRedactAnnotation

TPDFlib.AddRedactAnnotation(Left, Top, Width, Height, FillR, FillG, FillB, OverlayText, RepeatOverlay) voegt een mark toe aan de geselecteerde pagina en geeft het nieuwe annotatieaantal van de pagina terug, wat ook de 1-based index van de mark is, of 0 wanneer er niets is toegevoegd. Coördinaten volgen de huidige oorsprong en eenheden, dus het voorbeeld schakelt naar de oorsprong linksboven, waar Top de bovenrand is. De aanroep schrijft /Rect, een /IC uit de vulcomponenten en een vaste /DA van /F0 10 Tf 1 1 1 rg, witte tekst op 10 points; een niet-lege OverlayText voegt /OverlayText, /Repeat en /Q 1 toe voor een gecentreerd label. Redact-annotaties zijn een PDF 1.7-feature, dus een oudere header wordt verhoogd naar 1.7. Er wordt nog geen appearance stream geschreven; de mark in behandeling ziet eruit zoals de applicatie van de reviewer hem tekent. Met het label vast op wit paart u OverlayText aan een donkere vulling, want op bleek geel verdwijnt het label. Marks kunnen in elke PDFlibPas-editie worden aangemaakt

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);            // oorsprong linksboven: Top is de bovenrand
    Pdf.SetMeasurementUnits(0);  // points
    if Pdf.SelectPage(1) <> 1 then
      raise Exception.Create('Page 1 not found');
    // Getuigenaam: zwarte vulling, gecentreerd wit label
    Index := Pdf.AddRedactAnnotation(72, 180, 220, 14, 0, 0, 0, 'REDACTED');
    if Index = 0 then
      raise Exception.Create('Mark was not added');
    // Accountblok: tegel de vrijstellingscode over een hoger kader
    Pdf.AddRedactAnnotation(72, 410, 300, 40, 0, 0, 0, '(b)(6)', 1);
    // Alleen marks; elke byte van de originele content is nog aanwezig
    Pdf.SaveToFile(ReviewFile);
  finally
    Pdf.Free;
  end;
end;

QuadPoints bepalen het verwijdergebied; replies houden hun tekst

Draagt een mark eenmaal /QuadPoints, dan bepalen die vierhoeken wat ApplyRedactions verwijdert, en Rect omringt ze alleen voor de weergave. Marks gemaakt met tekstselectietools hebben meestal één vierhoek per geselecteerde regel, dus het verschuiven van het kader van zo een mark met SetAnnotRect verplaatst alleen zijn kader op het scherm en laat het verwijdergebied waar het was; SetAnnotQuadPoints is de aanroep die het gebied verplaatst, en GetAnnotQuadCount geeft 0 terug voor een mark die uitsluitend op Rect leunt. De tekst die aan een mark hangt volgt een andere regel. Reviewtools bewaren de notitie in /Contents en de auteur in /T, en een notitie op een redactiemark beschrijft vaak precies wat er wordt verwijderd ("SSN van de klant, even afstemmen met legal"). ApplyRedactions leegt het eigen dictionary van de mark, maar replies die via /IRT eronder hangen zijn aparte annotaties, alleen verwijderd wanneer hun rechthoek een redactiegebied overlapt, dus een reply geparkeerd in de marge houdt zijn tekst. De lijst hieronder drukt elke mark af met zijn aantal vierhoeken en de eronder hangende replies

QuadPoints versus Rect-anatomie in de redactiestroom van PDFlibPas voor Delphi: elke vierhoek omringt zijn eigen verwijdergebied terwijl een reply in de marge zijn tekst houdt
quadpoints omringen de verwijdering regel voor regel, dus de mark verplaatsen betekent SetAnnotQuadPoints en een reply in de marge buiten de gebieden houdt zijn 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 wanneer /IRT ontbreekt
        if (Parent > 0) and (Pdf.GetAnnotType(Parent) = 'Redact') then
          Writeln(Format('  reply #%d to #%d: %s',
            [I, Parent, Pdf.GetAnnotContentsEx(I)]));
      end;
  end;
end;

Welke overlay verschijnt na ApplyRedactions?

Een /RO-entry wint zodra hij aanwezig is, zoals de voorrangsregel in Table 192 vereist. ApplyRedactions installeert die Form XObject als de normal appearance van de annotatie (/AP /N), gooit elke appearance state weg en vlakt hem af, dus /IC, /OverlayText, /DA en /Q hebben op die mark geen effect. Plaatsing is het ene subtiele punt. De specificatie zegt dat de oorsprong van de overlay aansluit op de linkerbenedenhoek van Rect, terwijl het afvlakken de BBox van de Form via zijn Matrix op Rect mapt, dezelfde BBox- en Matrix-plaatsing die afvlakken toepast op widgets van formuliervelden. Een overlay met een BBox van [0 0 w h], op de rechthoek gemaakt, en een identiteits-Matrix ziet er onder beide lezingen hetzelfde uit, dus vraag om die vorm wanneer uw eigen tools /RO-marks genereren

Zonder /RO normaliseert ApplyRedactions het dictionary en bouwt de appearance zelf. Hij verwijdert /C en /BS en zet /Border op [0 0 0]: die entries beschrijven de mark in behandeling, meestal een gekleurde omlijning, en die in de definitieve pagina branden zou een afgeronde redactie onafgewerkt laten lijken. Zonder rand dekt de vulling de hele rechthoek. Het label wordt in de gegenereerde Form getekend, met grootte en kleur uit /DA, uitlijning uit /Q, tegeling uit /Repeat, en de fontnaam F0 gebind in de eigen resources van die Form aan standaard Helvetica. Die binding doet ertoe wanneer bestanden tussen tools reizen: ISO 32000-1 §12.7.3.3 definieert een DA-fontnaam als een entry in de default resources (/DR) van het interactieve formulier, een document zonder formuliervelden heeft die niet, en een andere applicatie die dezelfde mark toepast kan een fallback-font substitueren. PDFlibPas slaat die opslagactie over, en met Helvetica past het label bij Latijnse tekst zoals REDACTED of een vrijstellingscode

Overlay-beslisstroom na ApplyRedactions in PDFlibPas voor Delphi: een aanwezige RO wint en vlakt af, een ontbrekende IC valt terug op zwart
een RO-form XObject wordt geïnstalleerd als de normal appearance en afgevlakt, terwijl marks zonder er een een herbouwde appearance en een zwarte vulling krijgen wanneer IC ontbreekt

Een ontbrekende /IC wordt een zwart vlak

De default van de specificatie voor een afwezige /IC is een transparant binnenvlak, en PDFlibPas wijkt daar opzettelijk van af. Na de verwijdering laat een transparant binnenvlak het papier zien, een wit gat waar vroeger een naam stond, en dat gat ziet er hetzelfde uit als een layout waar nooit iets stond. Een reader kan geredigeerd niet van leeg onderscheiden, en een reviewer kan niet bevestigen dat de redactie heeft plaatsgevonden zonder geëxtraheerde tekst te vergelijken. Een hol kader, wat een apply-stap oplevert wanneer het de pending appearance van een mark met alleen een omlijning afvlakt, faalt anders: het leest als een voorstel waar niemand op handelde. We hebben de default letterlijk volgen tegen het zwarte vlak afgewogen en kozen voor het zwarte vlak, want het zichtbare teken dat iets is achtergehouden hoort bij wat een geredigeerd document zijn lezer vertelt, en de keuze raakt alleen marks waarvan de auteur nooit een vulling opgaf. Een /IC die al in het dictionary zit wordt gebruikt, welke waarde ook, dus een producent die om wit vroeg krijgt wit

Wat is al weg voordat de overlay wordt getekend

ApplyRedactions werkt pagina voor pagina en voltooit alle verwijdering voordat het enige overlay afvlakt, dus een overlay kan nooit door zijn eigen gebied worden gepakt. Per pagina verzamelt het één rechthoek per vierhoek, of de Rect van marks zonder vierhoeken, en schrobt elke contentlaag tegen elk ervan:

  • Tekst wordt glyph voor glyph weggeknipt, gemeten vanuit de pagefonts en de volledige text state, en de rest van een deels gedekte string blijft op zijn oorspronkelijke positie
  • Vectorpaden, inline afbeeldingen en shadings die binnen het gebied tekenen worden uit de contentstream verwijderd
  • Form XObjects worden recursief betreden; een Form die ook andere pagina's of plaatsingen tekenen wordt via een privé-kopie geredigeerd, zodat die pagina's het origineel houden
  • Image XObjects verliezen alleen de gedekte pixels, overschreven met zwart (volle K-inkt voor CMYK); JPEG-, JPEG 2000-, JBIG2- en CCITT-afbeeldingen worden gedecodeerd en als verliesvrije Flate-streams terug opgeslagen
  • In getagde PDF worden /Alt en /ActualText die aan verwijderde marked content hangen gewist in de paginacontent en in de structuurboom
  • Andere annotaties waarvan de rechthoek het gebied raakt worden verwijderd, en formuliervelden waarvan de widgets er zitten verliezen hun waarden

De pixelstap is de image-clausule van de specificatie in de praktijk. Een zwart kader over een gescand paspoort voldoet voor het oog en zakt nog steeds voor §12.5.6.23, want de originele samples worden gedecodeerd zodra iemand de afbeelding extraheert. Dezelfde engine zit achter RedactRegion, dat content uit een rechthoek verwijdert die u direct doorgeeft en samenwerkt met SanitizeDocument, zonder annotaties te lezen. Als de content eenmaal weg is, wordt elke mark afgevlakt en zijn dictionary gewist tot op /Type en /Subtype. Afvlakken verwijdert de annotatie uit de /Annots-array van de pagina, maar het dictionary blijft een object in het document, en een save die het wegschrijft zou /Contents, /RC, /T en de rest van het reviewspoor aan de ontvanger overhandigen

Opslaan zonder de niet-geredigeerde revisie

Een incrementele save maakt alles hierboven ongedaan. Onder §7.5.6 (hetzelfde nummer in ISO 32000-1 en ISO 32000-2) voegt een incrementele update gewijzigde objecten achteraan in het bestand toe en laat de originele bytes intact, dus de originele revisie, met zijn paginacontent, afbeeldingen en geannoteerde marks, blijft leesbaar voor iedereen die het bestand knipt op de eerdere %%EOF; ExtractDocumentRevision in de PDFlibPas Small Business-editie doet dat in één aanroep. Na ApplyRedactions schrijft u een nieuw bestand met SaveToFile, nooit SaveChanges, dat toevoegt. Vóór het opslaan veegt GarbageCollectObjects de objecttabel schoon en laat objecten vallen waarnaar niets meer verwijst, zoals de appearance streams die de marks vóór de apply-stap droegen; een volledige save slaat die veegbeurt over tenzij SetGarbageCollect hem aanzet, of het veld GarbageCollect van het TPDFlibSaveOptions-record dat u aan SaveToFileOptions doorgeeft is gezet. Metadata blijft het resterende gat. SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET) maakt Author, Subject, Keywords, Creator en Producer leeg en verwijdert de XMP-stream, en laat de Title over om zelf na te kijken. Vermijd SANITIZE_ALL hier: het omvat SANITIZE_ANNOTATIONS en verwijdert elke annotatie die nog in het document zit, notities die u had willen houden inbegrepen

Opslagtijdlijn na ApplyRedactions in PDFlibPas voor Delphi: een incrementele toevoeging laat het origineel herstelbaar achter, dus de release draait sanitize, garbage collect en een volledige herschrijving
na apply loopt het releasepad via metadata saneren, niet-gerefereerde objecten vegen en een nieuw bestand opslaan, want een incrementele save houdt de niet-geredigeerde revisie leesbaar

ApplyRedactions is, net als RedactRegion, beschikbaar in de Enterprise-editie. Op een kale TPDFlib geeft het 0 terug en zet het LastErrorCode op PDFLIB_ERROR_ENTERPRISE_REQUIRED (900), dus de releasestap gebruikt TPDFlibEnterprise uit de 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;   // elke Redact-mark op elke pagina
    if Applied = 0 then
      raise Exception.CreateFmt('No marks applied (error %d)',
        [Pdf.LastErrorCode]);
    // Info en XMP kunnen nog benoemen wat de pagina's niet meer tonen
    Pdf.SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET);
    Orphans := Pdf.GarbageCollectObjects;
    // Volledige herschrijving naar een nieuw bestand; nooit SaveChanges na redactie
    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;

Verwacht: ApplyRedactions geeft het aantal Redact-marks terug dat het over alle pagina's heeft afgevlakt, 2 voor de reviewkopie geschreven door MarkForReview, en GarbageCollectObjects geeft terug hoeveel niet-gerefereerde objecten de veegbeurt heeft laten vallen

Welke tool ook een mark tekende, zijn /RO- en /IC-entries bepalen wat de ontvanger op de pagina ziet, terwijl de apply-stap in PDFlibPas Enterprise for Delphi bepaalt wat eronder is verdwenen