Teknisk artikel

Omskriv PDF content streams sikkert i Delphi

HotPDF gennemløber hver operator i en sides content stream og overdrager hver enkelt til din kode via VisitLoadedPageContentOperators. For hver operator vælger du en handling: behold den, slet den, erstat den med dine egne bytes, stop gennemløbet, eller afvis hele redigeringen. Når alle operatorer beholdes, er den omskrevne stream byte for byte identisk med originalen, hvilket er den egenskab, der gør mekanismen sikker at køre over dokumenter, du ikke selv har oprettet

Redigering af content streams er der, hvor PDF-værktøjer som regel går galt. En stream er en sekvens af operatorer over en stakmaskine, ikke et dokumenttræ, og den naive tilgang med at køre et regulært udtryk over de afkodede bytes går i stykker, i det øjeblik et string-literal indeholder noget, der ligner en operator, eller et inline-billede indeholder vilkårlig binærdata. En visitor, der allerede har tokeniseret streamen, har ikke det problem

PDF content stream-operatorer som BT, Tf og TJ, der strømmer ind i visitorens event-data med fase, operatornavn, operandantal, byte-span og nesting-tilstand, og derefter returnerer én af fem handlinger: behold, erstat, slet, stop eller afvis
Hver besøgt operator giver præcis én beslutning, og et gennemløb, der beholder alt, skriver urørte intervaller tilbage for byte-identisk output

Hvad kan du egentlig gøre med adgang på operatorniveau?

Tre familier af opgaver. Auditering: tæl, hvor mange gange en side sætter en farve uden for et kendt sæt, eller find tekst tegnet med en skrifttype, der ikke er indlejret. Fjernelse: slet et vandmærke, der blev tegnet som marked content, eller fjern en bestemt billedpåkaldelse. Og normalisering: erstat én operatorform med en tilsvarende, som en efterfølgende forbruger håndterer bedre

Hver af disse deler ét krav: du skal kunne lade de øvrige 99 procent af streamen være urørte. HotPDF's syntakstræ gemmer én kopi af kilden plus kompakte spans, og redigeringer er sorterede, ikke-overlappende overlejringer. Skrivning af resultatet kopierer urørte intervaller direkte og udsender kun erstatningsbytes, hvor du har ændret noget, så en no-op-tur frem og tilbage ikke kan indføre drift

Visitor-kontrakten

Implementér interfacet eller tildel eventet, og kald derefter visit-metoden med ApplyChanges sat til false for en auditering eller true for en omskrivning. Event-dataene bærer fasen, operatorens navn, dens operandantal, dens byte-span, den aktuelle nesting-tilstand og en lånt view til at læse operander:

uses
  HPDFDoc, HPDFContentStream;

procedure TAuditor.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
var
  Bytes: TBytes;
begin
  if EventData.Phase <> covpOperator then
    Exit;

  // Tæl tekstvisende operatorer tegnet inde i marked content
  if (EventData.OperatorName = 'Tj') or
     (EventData.OperatorName = 'TJ') then
  begin
    Inc(FTextOperators);
    if EventData.State.MarkedContentDepth > 0 then
      Inc(FTextInsideMarkedContent);
  end;

  // Læs kun en operand, når du har brug for den
  if EventData.OperatorName = 'Do' then
    if EventData.View.GetOperandBytes(0, Bytes) then
      NoteXObjectUse(TEncoding.ANSI.GetString(Bytes));

  EventData.Action := coaKeep;
end;

procedure TAuditor.Run(const FileName: string);
var
  Pdf: THotPDF;
  Stats: THPDFContentOperatorVisitStatistics;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(FileName) <= 0 then
      Exit;
    Pdf.OnContentOperator := HandleOperator;
    for I := 0 to Pdf.LoadedPageCount - 1 do
      Pdf.VisitLoadedPageContentOperators(I, False, Stats);   // kun auditering
    Writeln(Format('%d operators visited, max q/Q depth %d',
      [Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
  finally
    Pdf.Free;
  end;
end;

Hvorfor er viewet lånt frem for muterbart?

Callbacket modtager aldrig et ændringsbart syntakstræ. Det modtager et view, der kan hente den aktuelle operations bytes eller en specifik operand efter behov, og det er alt. Enhver ændring går i stedet gennem action- og replacement-felterne

Denne indirekthed er ikke ceremoni. Direkte trædmutation ville lade en visitor omgå erstatningsbudgettet og tjekket for overlappende redigeringer, og overlappende redigeringer i en byte-span-overlejringsmodel giver output, der er stiltiende forkert frem for tydeligt ødelagt. At lede ændringer gennem action-feltet betyder, at hver redigering valideres på samme måde, uanset hvad visitoren havde til hensigt

At læse efter behov holder også det almindelige tilfælde billigt. En auditering, der kun tæller operatornavne, materialiserer aldrig operandbytes overhovedet, hvilket betyder noget på en content stream med hundredtusindvis af operationer

State-feltet beskriver altid inputtet

Hvert event bærer grafiktilstands-dybden, tekstobjekt-dybden, marked content-dybden og kompatibilitetssektion-dybden, som de stod i det oprindelige input umiddelbart før den operator blev udført. Det gælder, selv hvis din visitor slettede eller erstattede tidligere operatorer

At forankre tilstanden til det oprindelige input frem for til det redigerede output er et bevidst valg med en praktisk gevinst: at køre den samme auditering to gange over det samme dokument giver identiske resultater, og en auditering, der kører sideløbende med redigeringer, forskyder ikke sin egen referenceramme. Hvis du har brug for at kende dybden i outputtet, ved du, hvad du slettede, så du kan beregne det; det omvendte, at rekonstruere stabile inputpositioner ud fra et output i bevægelse, er ikke muligt

Sletning og erstatning, med de regler der beskytter dig

En erstatning er rå content-stream-bytes, og du er ansvarlig for deres syntaks. Beskyttelsesforanstaltningerne omkring dem er budgettet for de samlede erstatningsbytes, kravet om ikke-overlap og den valgfrie output-validering, der genparser resultatet, før det accepteres:

Beskyttelsesforanstaltninger ved omskrivning i Delphi: en visitor, der erklærer Action- og Replacement-hensigt, ledt gennem erstatningsbudgettet, ikke-overlappende byte-span og output-valideringstjek ind i den validerede gemte side
Hver ændring ledes gennem action-feltet ind i budget-, overlap- og genparse-tjekkene, så mislykket validering afbryder i stedet for at skrive ødelagte operatorer
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
begin
  EventData.Action := coaKeep;
  if EventData.Phase <> covpOperator then
    Exit;

  // Drop de operatorer, der tegner vandmærke-formularen
  if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
    EventData.Action := coaDelete
  // Normalisér en vildfaren farve til husets grå
  else if EventData.OperatorName = 'rg' then
  begin
    EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
    EventData.Action := coaReplace;
  end
  // Afvis hele redigeringen, hvis vi støder på noget, vi ikke forstår
  else if EventData.OperatorName = 'BI' then
    EventData.Action := coaReject;
end;

var
  Options: THPDFContentOperatorVisitOptions;
begin
  Options := THPDFContentOperatorVisitOptions.Default;
  Options.ValidateOutput := True;        // genpars før accept
  Options.MaxReplacementBytes := 64 * 1024;

  Pdf.OnContentOperator := HandleOperator;
  if Pdf.VisitLoadedPageContentOperators(PageIndex, True, Options, nil, Stats) then
    if Stats.Modified then
      Pdf.SaveLoadedDocument('cleaned.pdf');
end;

Handlingssættet indeholder to udgange af en grund. coaStop afslutter gennemløbet og beholder, hvad der er besluttet indtil da, hvilket er rigtigt, når du har fundet det, du ledte efter. coaReject opgiver hele besøget og lader siden være urørt, hvilket er rigtigt, når du støder på en konstruktion, dine omskrivningsregler ikke dækker. Inline-billeder er det klassiske eksempel: deres binære payload er ikke noget, en farve-normaliserende visitor bør redigere omkring

Aflæsning af statistikken, og hvor gennemløbet stopper

Status-feltet adskiller fuldførelse fra stop, afvisning, ugyldigt input, en overskredet grænse, ugyldigt output og en visitor-fejl. Det er forskellige problemer: ugyldigt output betyder, at dine erstatningsbytes ikke lod sig genparse, mens ugyldigt input betyder, at kildestreamen allerede var fejlformet, og ingen redigering blev forsøgt

Tællerne gør en auditering selvkontrollerende. VisitedOperatorCount sammenholdt med KeptOperatorCount, DeletedOperatorCount og ReplacedOperatorCount skal gå op, og maksimum-dybdetallene fortæller dig, om siden nester dybt nok til at være et nærmere kig værd. SourceBytes sammenholdt med OutputBytes giver størrelseseffekten af din redigering i ét tal

Afstemning af auditstatistik, hvor beholdte, slettede og erstattede operatorantal summer til det besøgte antal, ved siden af syv status-udgange fra fuldført til visitor-fejl, over en note om, at state-dybder beskriver det oprindelige input, så auditeringer gentages identisk
Tællere, der skal gå op, plus en status, der navngiver hver mulig udgang, gør en auditering selvkontrollerende på dokumenter, du ikke selv har oprettet

For de almindelige opgaver findes der ruter på et højere niveau, som slet ikke kræver, at du skriver en visitor. Permanent fjernelse af indhold, med den ressourceoprydning ægte redigering af følsomme oplysninger kræver, er dækket i redigering af følsomme oplysninger og sidesammensyning på indlæste dokumenter. Erstatning af synlig tekst er dækket i søg og erstat i en eksisterende PDF. Grib til operator-visitoren, når din regel er strukturel frem for tekstuel, eller når du har brug for en auditering, som ingen eksisterende rapport besvarer

Content stream-besøg, ekstraktion og omskrivning kører alle mod den samme indlæste dokumentmodel i Delphi og C++Builder; den komplette funktionsliste findes på HotPDF Delphi PDF component-siden