Teknisk artikkel

Operatornivå-PDF-redigering i Delphi med PDFiumPas

Noen maler en svart boks over et navn, flater ut ingenting, sender filen, og korrekturleseren velger rektangelet og limer navnet inn i en e-post. PDFiumPas svarer på det med redigering på operatornivå: SaveAsRedacted sletter bare Unicode-skalarene hvis tegnbokser berører et redigeringsrektangel, gjenoppbygger overlevende fra original font, størrelse, matrise, rendermodus og farge, og beskjærer aksejusterte baner og bilder i stedet for å slippe dem hele

Hvorfor et malt rektangel ikke er en redigering

En tegneoperasjon lagt til oppå en innholdsstrøm skjuler ingenting, fordi tekstvisingsoperatorene under den fortsatt er i strømmen og fortsatt mappes til kodepunkt. ISO 32000-1 §9.4 definerer et tekstobjekt som en sekvens av posisjonerings- og visingsoperatorer inne i BT og ET; et fylt rektangel tegnet etterpå er rett og slett en annen operator i samme strøm. Ekstraksjon går gjennom operatorene, ikke pikslene, så den dekkede strengen kommer tilbake intakt. Ekte redigering må fjerne operanden, ikke skjule utdataene

Den åpenbare trygge implementeringen er brutal: finn hvert sideobjekt hvis bounding box skjærer et redigeringsrektangel og slett hele objektet. Det er hva tidligere PDFiumPas-utgivelser gjorde, og det er korrekt men dyrt. En enkelt Tj kan bære en hel tabellrad, så å sverte ett kontonummer tok med seg datoen, beskrivelsen og beløpet. En rektangulær fylling som tilfeldigvis var et tabelbånd i full bredde, forsvant over hele siden. En fakturalogo forsvant fordi redigeringen klippet av ett hjørne av den. Versjon 3.101.0 flytter beslutningen ett nivå ned, fra sideobjektet til operanden

Hva sletter redigering på operatornivå egentlig?

PDFiumPas sletter Unicode-skalarer, ikke tekstobjekter. Under SaveAsRedacted bygger komponenten en tegn-til-sideobjekt-mapping fra den innlastede tekstsiden, deretter for hvert tegn eid av objektet under test leser den tegnboksen og skjærer den boksen mot hvert redigeringsrektangel. Tegn som berører et rektangel markeres for fjerning; resten markeres som overlevende. Hvis ingenting skjærer, blir objektet fullstendig i fred. Hvis hvert tegn skjærer, fjernes objektet helt, nøyaktig som før. Bare det blandede tilfellet utløser en splitting

PDFiumPas redigering på operatornivå sammenlignet med helobjekt-sletting i Delphi: den gamle stien slipper et helt tekstobjekt når ett kontonummer er dekket, mens splittingsstien sletter bare de skjøtende tegnene og re-emitterer hver overlevende som sitt eget tekstobjekt
Bare det blandede tilfellet utløser en splitting: ingenting som skjærer lar objektet være i fred, alt som skjærer fjerner det helt

Hver overlevende re-emitteres deretter som sitt eget tekstobjekt bygget fra original fonthåndtak, den opprinnelige fontstørrelsen, per-tegn tekstmatrisen, den opprinnelige tekstrendermodusen og fyll- og stryketilstanden til foreldreobjektet inkludert strekbredde, linjeskjøt, linjeende og stiplet array. Å gjenbruke fonthåndtaket i stedet for å løse et nytt, er det som holder glyfene metrisk identiske, og å gjenbruke per-tegn-matrisen er det som holder kerning og ordmellomrom på plass uten å kjøre oppsett på nytt. Kostnaden er objektantall: ett beholdt tegn blir ett tekstobjekt, og det er derfor TPdfRedactionOptions.MaxSplitObjects finnes som et hardt tak på genererte fragmenter

procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
  Pdf: TPdf;
  Options: TPdfRedactionOptions;
  Report: TPdfRedactionReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := SourcePdf;   // filen bærer allerede /Redact-annoteringer
    Pdf.Active := True;

    Options := TPdfRedactionOptions.Default;
    Options.PreservePartialObjects := True;    // operatornivå-splitting (standarden)
    Options.RemoveIntersectingAnnotations := True;
    Options.MaxSplitObjects := 20000;          // tak på genererte fragmenter

    if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
      raise Exception.Create(Report.ErrorMessage);   // feil lukket, ikke send
  finally
    Pdf.Free;
  end;
end;

Rektangler beskjæres, rotert geometri gjør ikke

Baner splittes bare når PDFiumPas kan bevise at banen er et aksejustert rektangel. Beviset er bevisst smalt: objektmatrisen må ha begge shear-uttrykkene under 0.0001, banen må bestå av fire til seks segmenter som begynner med en MOVETO og fortsetter med bare LINETO, og de transformerte punktene må lande på alle fire hjørnene av objektgrensene innen en toleranse på 0.01. En bane som klarer den sjekken reduseres ved suksessiv rektangelsubtraksjon, hvert redigeringsrektangel utskjærer det overlevende settet i venstre, høyre, under og over striper, og hver resulterende stripe gjenskapes med original fyllmodus, strekflagg og malingstilstand. Kurver, trekanter, klypte former og alt rotert stryker sjekken og hele objektet fjernes

Bilder følger ISO 32000-1 §8.9, der bildeprøvene okkuperer enhetkvadratet mappet gjennom den gjeldende transformasjonsmatrisen. PDFiumPas inverterer den mappingen for å gjøre hvert overlevende sideområde-fragment tilbake til normaliserte bildekoordinater, clamper dem til enhetsintervallet, og konverterer deretter til pikselindekser ved avrunding innover: venstre og øvre kanter går gjennom Ceil, høyre og nedre gjennom Floor. Den retningen betyr noe. Avrunding utover ville la en delvis kolonne av kildepiksler fra den redigerte siden overleve ved fragmentkanten. De heltalls pikselgrensene konverteres deretter tilbake til normaliserte koordinater og brukes til å utlede fragmentmatrisen, så det beskjærte bitmapet lander nøyaktig på pikselgrensen det ble kuttet ved. Beskjæringen selv er en stride-bevisst radkopi på tvers av Gray-, BGR-, BGRx- og BGRA-formatene. Som med baner, fjernes et rotert eller skjevt bilde, eller ett hvis matrise har et degenerert skalauttrykk, i sin helhet

Slik beskjærer PDFiumPas et delvis redigert bilde i Delphi: det overlevende sideområde-fragmentet mappes tilbake gjennom den inverterte CTM-en til normaliserte bildekoordinater, clamper til enhetsintervallet og avrundes innover slik at ingen redigert pikselkolonne overlever
Ceil på venstre og øvre, Floor på høyre og nedre, så snittet lander på en hel pikselgrense
// Etter et vellykket SaveAsRedacted-kall
Writeln(Format('applied %d redaction(s) on %d page(s)',
  [Report.RedactionCount, Report.RedactedPageCount]));
Writeln(Format('scanned %d object(s), removed %d',
  [Report.ScannedObjectCount, Report.RemovedObjectCount]));
Writeln(Format('split text/path/image: %d / %d / %d',
  [Report.SplitTextObjectCount, Report.SplitPathObjectCount,
   Report.SplitImageObjectCount]));
Writeln(Format('preserved %d fragment(s)', [Report.PreservedFragmentCount]));
Writeln(Format('pruned %d resource name(s), swept %d object(s)',
  [Report.ResourcePruneReport.RemovedNameCount,
   Report.ResourcePruneReport.RemovedObjectCount]));

if Report.PreservedFragmentCount = 0 then
  // ingenting kunne splittes: hvert skjøtende objekt ble sluppet helt
  LogWholeObjectFallback(SourcePdf);

Hvorfor feiler PDFiumPas lukket på umappede tegn?

Fordi en glyf som ikke har noe reproduserbart Unicode-scalar ikke kan gjenoppbygges ærlig. Å rekonstruere en overlevende betyr å kalle tekstsette-API-et med en streng, og det krever et stabilt kodepunkt for hvert beholdt tegn. Symboliske subsettfonter med ødelagte eller fraværende ToUnicode-data kan gi en tom mapping, og re-encoding ved gjetting ville produsere utdata som ser riktige ut på skjermen mens de bærer et annet tegn under. PDFiumPas nekter: den beholdt-tegn-sjekken kaster, unntaket fanges inne i SaveAsRedacted, TPdfRedactionReport.Succeeded kommer tilbake False med meldingen i ErrorMessage, og funksjonen returnerer False. Samme regel gjelder splittbudsjettet, som kaster i stedet for stille å avkorte fragmentsettet. Når et dokument har fonter du ikke stoler på, og du vil ha den deterministiske gamle oppførselen, sett Options.PreservePartialObjects := False og hvert skjøtende objekt forsvinner helt

Ressursbeskjæring på tvers av delte scopes

Å splitte objekter etterlater foreldreløse, og å beskjære dem er ikke så enkelt som å diff-e side-nivå /Resources-ordlisten. ISO 32000-1 §7.8.3 lar samme ressursordliste refereres av flere sider, av Form XObjects, av mønstre og av annoteringsutseendestrømmer samtidig. Å slette et fontnavn fordi én side sluttet å bruke det, vil knekke en annen side som fortsatt gjør det. PruneUnusedPdfResources virker derfor per scope: den løser /Contents enten det er en direkte array, en indirekte referanse til en array eller en enkelt strøm, og samler deretter ressursbruk fra operatorene som faktisk navngir ressurser — Tf for fonter, Do for XObjects, gs for grafikktilstand, CS, cs, SCN og scn for fargerom og mønstre, sh for skyggelegginger, BDC og DP for marked-content-egenskaper, pluss /CS-oppføringen til inline-bilder. Når én ordliste deles av flere scopes, forbundes de brukte-navn-settene per kategori før noe fjernes

Ressursbeskjæring i PDFiumPas: tre scopes refererer én delt ressursordliste, deres brukte-navn-setter forbundes per kategori, og bare navnene ingen scope refererer fjernes før utilgjengelige objekter feies
Én ordliste kan tjene flere sider, form XObjects og utseendestrømmer, så PDFiumPas forbunder hvert brukte-navn-sett før ett eneste navn slippes

Bare navn bekreftet ureferert på tvers av hver scope som peker på ordlisten slippes. En scope som ikke kan parses med trygghet, blir i fred, som er den konservative retningen: en ubeskjært fil er bare større, en feil beskjært er korrupt. De overlevende ordlistene skrives tilbake som en sparsom inkrementell oppdatering som bærer de eksakte generasjonstallene, og en nåbarhet-omskrivning feier deretter objektene som ble utilgjengelige da navnene forsvant. TPdfResourcePruneReport rapporterer ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, bytetallene og et Succeeded-flagg. SaveAsRedacted kjører dette trinnet automatisk på den sanitiserte utdataen, så redigeringsstien inkluderer det allerede, men funksjonen er eksportert på strøm-nivå for pipelines som vil ha den på egen hånd

uses
  FPdfCompress;

procedure PruneResourceNames(const SourcePdf, TargetPdf: string);
var
  Source, Dest: TFileStream;
  Report: TPdfResourcePruneReport;
begin
  Source := TFileStream.Create(SourcePdf, fmOpenRead or fmShareDenyWrite);
  try
    Dest := TFileStream.Create(TargetPdf, fmCreate);
    try
      // AllowSignedDocument forblir False: en inkrementell omskriving ville
      // ugyldiggjøre byte-områdene en signatur dekker
      PruneUnusedPdfResources(Source, Dest, Report);
      if not Report.Succeeded then
        raise Exception.Create(Report.ErrorMessage);
      Writeln(Format('%d name(s) removed from %d scope(s), %d -> %d bytes',
        [Report.RemovedNameCount, Report.UpdatedScopeCount,
         Report.SourceByteCount, Report.OutputByteCount]));
    finally
      Dest.Free;
    end;
  finally
    Source.Free;
  end;
end;

Koble det inn i en dokumentpipeline

Redigeringsstien muterer aldri dokumentet du lastet inn. SaveAsRedacted fanger et isolert øyeblikksbilde, anvender /Redact-annoteringene der, striper vedlegg, kjører saniteringspasset som fjerner open action, kataloghandlinger, navnetrær, tilknyttede filer, AcroForm-en og metadataene, beskjærer ressurser, og skriver først da utdatastrømmen. Å gjenåpne den utdataen som et uavhengig dokument og trekke ut teksten på nytt, er verifikasjonstrinnet verdt å beholde i din egen testpakke, fordi det er den eneste sjekken som svarer på det opprinnelige spørsmålet — kan en leser fortsatt få strengen. En konsekvens å planlegge for: splitting erstatter sideobjekter, så ethvert FPDF_PAGEOBJECT-håndtak du holdt, er dødt etterpå, den samme levetid-fellen beskrevet i foreldede sideobjekt-håndtak etter en transform

To nabostykker gjør arbeidsflyten komplett. Å bestemme hvor redigeringsrektanglene går begynner vanligvis fra ekstrahert geometri, og blokk- og leserekkefølge-modellen i strukturerte tekstblokker og leserekkefølge er en bedre kilde til kandidatbokser enn rå tegnrekker. Å servere resultatet til en korrekturleser hører til herdningsreglene i bygge en sikker PDF-forhåndsvisning, der utfylling av skjema og JavaScript forblir av som standard. Sammen dekker de løkken de fleste compliance-arbeidsflyter trenger: lokalisere, redigere på operatornivå, verifisere ved gjenåpning, forhåndsvise trygt. Den fullstendige API-flaten, prøvenedlastingen og lisensvilkårene for komponenten bor på PDFium Delphi Component produktsiden