Műszaki cikk

Operátorszintű PDF-kitakarás Delphiben PDFiumPasszal

Valaki fekete dobozt fest egy név fölé, semmit nem lelapít, kiküldi a fájlt, és a lektor kijelöli a téglalapot, majd beilleszti a nevet egy e-mailbe. A PDFiumPas operátorszintű kitakarással válaszol: a SaveAsRedacted csak azokat a Unicode skalárokat törli, amelyek karakterdobozai hozzáérnek egy kitakaró téglalaphoz, a túlélőket az eredeti betűtípusból, méretből, mátrixból, megjelenítési módból és színből építi újra, és a tengelypárhuzamos útvonalakat és képeket nem egészben ejti, hanem körbevágja

Miért nem kitakarás egy festett téglalap

A rajzolási művelet, amelyet egy tartalomstream tetejére adnak, semmit sem rejt el, mert alatta a szövegmegjelenítő operátorok továbbra is a streamben vannak, és továbbra is kódpontokra képeződnek le. Az ISO 32000-1 §9.4 a szövegobjektumot a BT és ET közötti pozicionáló és megjelenítő operátorok sorozataként definiálja; egy utána rajzolt kitöltött téglalap egyszerűen egy újabb operátor ugyanabban a streamben. A kinyerés az operátorokat járja, nem a pixeleket, így a takart sztring épségben jön vissza. A valódi kitakarásnak az operandust kell eltávolítania, nem a kimenetet elrejtenie

A kézenfekvő biztonságos megvalósítás kíméletlen: megtalálja minden oldalobjektumot, amelynek befoglaló doboza metsz egy kitakaró téglalapot, és egészben törli az objektumot. Ezt tették a korábbi PDFiumPas kiadások, és ez helyes, de drága. Egyetlen Tj egy teljes táblázatsort is hordozhat, így egy számlaszám feketévé tétele magával vitte a dátumot, a leírást és az összeget. Egy véletlenül teljes szélességű táblázatsávot adó téglalap kitöltés az egész oldalon eltűnt. Egy számlalogó azért tűnt el, mert a kitakarás az egyik sarkát metszte. A 3.101.0-s verzió a döntést egy szinttel lejjebb viszi, az oldalobjektumtól az operandusig

Mit töröl valójában az operátorszintű kitakarás?

A PDFiumPas Unicode skalárokat töröl, nem szövegobjektumokat. A SaveAsRedacted alatt a komponens karakter–oldalobjektum hozzárendelést épít a betöltött szöveges oldalból, majd a vizsgált objektum tulajdonában lévő minden karakterhez beolvassa a karakterdobozt, és azt metszi minden kitakaró téglalappal. A téglalaphoz hozzáérő karakterek törlésre jelölődnek; a többi túlélőként. Ha semmi nem metsz, az objektum teljesen érintetlen marad. Ha minden karakter metsz, az objektum egészben törlődik, pontosan mint korábban. Csak a vegyes eset vált ki bontást

PDFiumPas operátorszintű kitakarás összehasonlítva a teljes objektum törlésével Delphiben: a régi út egy teljes szövegobjektumot ejt, amikor egy számlaszám takarva van, míg a bontó út csak a metsző karaktereket törli, és minden túlélőt saját szövegobjektumként bocsát ki újra
Csak a vegyes eset vált ki bontást: semmi metszés esetén az objektum érintetlen marad, minden metszés esetén egészben törlődik

Minden túlélő ezután saját szövegobjektumként bocsátódik ki, amely az eredeti betűtípus-handle-ből, az eredeti betűméretből, a karakterenkénti szövegmátrixból, az eredeti szövegmegjelenítési módból és a szülőobjektum kitöltési és körvonalállapotából épül, ideértve a körvonalvastagságot, a vonalcsatlakozást, a vonalvéget és a szaggatott mintát. A betűtípus-handle újrafelhasználása új feloldás helyett az, ami a glyphokat metrikailag azonosvá teszi, és a karakterenkénti mátrix újrafelhasználása az, ami a kerninget és a szóközözést a helyén tartja az elrendezés újbóli futtatása nélkül. Az ára az objektumszám: egy megtartott karakter egy szövegobjektummá válik, ezért létezik a TPdfRedactionOptions.MaxSplitObjects mint kemény felső határ a generált töredékekre

procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
  Pdf: TPdf;
  Options: TPdfRedactionOptions;
  Report: TPdfRedactionReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := SourcePdf;   // a fájl már tartalmaz /Redact annotációkat
    Pdf.Active := True;

    Options := TPdfRedactionOptions.Default;
    Options.PreservePartialObjects := True;    // operátorszintű bontás (az alapértelmezett)
    Options.RemoveIntersectingAnnotations := True;
    Options.MaxSplitObjects := 20000;          // felső határ a generált töredékekre

    if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
      raise Exception.Create(Report.ErrorMessage);   // hibázzon zártan, ne küldje ki
  finally
    Pdf.Free;
  end;
end;

A téglalapok körbevágódnak, a forgatott geometria nem

Az útvonalak csak akkor bonthatók, ha a PDFiumPas be tudja bizonyítani, hogy az útvonal tengelypárhuzamos téglalap. A bizonyítás szándékosan szűk: az objektummátrixnak mindkét nyírási tagja 0.0001 alatt kell legyen, az útvonalnak négy-hat szegmensből kell állnia, amely MOVETO-val kezdődik és csak LINETO-val folytatódik, és a transzformált pontoknak az objektumhatárok mind a négy sarkára kell esniük 0.01 tűrésen belül. A vizsgálaton átmenő útvonal egymást követő téglalapkivonással szűkül, minden kitakaró téglalap a túlélő halmazt bal, jobb, alatta és felette sávokra faragja, és minden keletkező sáv az eredeti kitöltési móddal, körvonaljelzővel és festési állapottal jön létre újra. A görbék, háromszögek, kivágott alakzatok és bármi, ami forgatva van, megbukik a vizsgálaton, és az objektum egészben törlődik

A képek az ISO 32000-1 §8.9-et követik, ahol a képminták az aktuális transzformációs mátrix által képezett egységnégyzetet foglalják el. A PDFiumPas megfordítja azt a leképezést, hogy minden túlélő oldalterület-töredéket visszaalakítson normalizált képkoordinátákba, azokat az egységintervallumra szorítja, majd pixelindexekké alakítja befelé kerekítéssel: a bal és felső él a Ceil-ön megy át, a jobb és alsó a Floor-ön. Ez az irány számít. Kifelé kerekítve a takart oldalról származó részleges forráspixel-oszlop túlélné a töredék szélét. Az egész pixelhatárokat ezután visszaalakítják normalizált koordinátákba, és abból származtatják a töredékmátrixot, így a levágott bitkép pontosan arra a pixelhatárra esik, amelynél elvágták. Maga a kivágás stride-tudatos sormásolás a Gray, BGR, BGRx és BGRA formátumokban. Az útvonalakhoz hasonlóan egy forgatott vagy nyírt kép, vagy amelynek mátrixában degenerált skáltag van, egészben törlődik

Hogyan vág körbe részben kitakart képet a PDFiumPas Delphiben: a túlélő oldalterület-töredék a megfordított CTM-en keresztül normalizált képkoordinátákba tér vissza, az egységintervallumra szorítódik és befelé kerekítődik, így egyetlen takart pixeloszlop sem él túl
Ceil balra és fent, Floor jobbra és lent, így a vágás egész pixelhatárra esik
// Egy sikeres SaveAsRedacted hívás után
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
  // semmi nem bontható: minden metsző objektum egészben eldobódott
  LogWholeObjectFallback(SourcePdf);

Miért hibázik zártan a PDFiumPas a leképezetlen karaktereknél?

Mert egy glyph, amelynek nincs reprodukálható Unicode skalárja, nem építhető újra becsületesen. Egy túlélő rekonstruálása azt jelenti, hogy a szövegbeállító API-t sztringgel hívja meg, ami minden megtartott karakterhez stabil kódpontot igényel. A törött vagy hiányzó ToUnicode adatú szimbolikus részhalmaz betűtípusok üres leképezést adhatnak, és találgatás alapú újrakódolás olyan kimenetet adna, amely a képernyőn helyesnek néz ki, miközben mást hordoz alatta. A PDFiumPas visszautasítja: a megtartott karakterekre vonatkozó vizsgálat kivételt dob, a kivétel a SaveAsRedacted belül elkapásra kerül, a TPdfRedactionReport.Succeeded False-dal tér vissza az üzenettel az ErrorMessage-ban, és a függvény False-ot ad. Ugyanaz a szabály vonatkozik a bontási keretre, amely kivételt dob, nem pedig csendben csonkolja a töredékhalmazt. Amikor egy dokumentumban olyan betűtípusok vannak, amelyekben nem bízik, és a determinisztikus régi viselkedést szeretné, állítsa Options.PreservePartialObjects := False-ra, és minden metsző objektum egészben távozik

Erőforrás-tisztítás megosztott hatókörökben

Az objektumok bontása árvákat hagy maga után, és azok kitisztítása nem olyan egyszerű, mint a lap szintű /Resources szótár átvizsgálása. Az ISO 32000-1 §7.8.3 engedi, hogy ugyanaz az erőforrásszótár egyszerre hivatkozzon több oldal, Form XObjectek, mintázatok és annotáció megjelenítési streamek által. Egy betűtípusnév törlése azért, mert egy lap már nem használja, megtöri a másik lapot, amely még igen. A PruneUnusedPdfResources ezért hatókörönként dolgozik: feloldja a /Contents-t, legyen az közvetlen tömb, tömbre mutató indirekt hivatkozás vagy egyetlen stream, majd erőforráshasználatot gyűjt azokból az operátorokból, amelyek ténylegesen neveznek erőforrásokat — Tf betűtípusokra, Do XObjectekre, gs grafikai állapotra, CS, cs, SCN és scn színterekre és mintázatokra, sh árnyalatokra, BDC és DP megjelölt tartalom tulajdonságokra, plusz a beágyazott képek /CS bejegyzése. Amikor egy szótárt több hatókör oszt meg, a használt-név halmazok kategóriánként unióra kerülnek, mielőtt bármi törlődne

Erőforrás-tisztítás a PDFiumPasban: három hatókör egy megosztott erőforrásszótárra hivatkozik, a használt-név halmazaik kategóriánként unióra kerülnek, és csak azok a nevek törlődnek, amelyekre egyetlen hatókör sem hivatkozik, mielőtt az elérhetetlen objektumok kisöprésre kerülnének
Egy szótár több oldalt, form XObjectet és megjelenítési streamet szolgálhat ki, ezért a PDFiumPas minden használt-név halmazt unióz, mielőtt egyetlen nevet ejtene

Csak azok a nevek törlődnek, amelyeket minden, a szótárra mutató hatókör megerősítetten nem hivatkozik. A hatókör, amely nem értelmezhető biztosan, érintetlen marad, ami a konzervatív irány: egy tisztítatlan fájl csupán nagyobb, egy tévesen tisztított sérült. A túlélő szótárak pontos generációs számokkal, ritka inkrementális frissítés formájában íródnak vissza, és egy elérhetőségi átírás ezután kisöpri az objektumokat, amelyek a nevek eltűnése után elérhetetlenné váltak. A TPdfResourcePruneReport jelent ScannedScopeCount-ot, UpdatedScopeCount-ot, RemovedNameCount-ot, RemovedObjectCount-ot, a bájtszámokat és egy Succeeded jelzőt. A SaveAsRedacted ezt a lépést automatikusan lefuttatja a fertőtlenített kimeneten, így a kitakarási út már tartalmazza, de a függvény stream szinten exportált azoknak a folyamatoknak, amelyek önállóan akarják

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
      // Az AllowSignedDocument False marad: egy inkrementális újraírás
      // érvénytelenítené az aláírás által lefedett bájtartományokat
      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;

Bekötése egy dokumentumfolyamatba

A kitakarási út soha nem módosítja a betöltött dokumentumot. A SaveAsRedacted izolált pillanatképet készít, ott alkalmazza a /Redact annotációkat, eltávolítja a mellékleteket, lefuttatja a fertőtlenítő lépést, amely eltávolítja a nyitó műveletet, a katalógusműveleteket, a névfákat, a társított fájlokat, az AcroFormot és a metaadatokat, megtisztítja az erőforrásokat, és csak ezután írja a kimeneti streamet. Annak a kimenetnek az újranyitása független dokumentumként és a szöveg újra kinyerése az az ellenőrzési lépés, amelyet érdemes a saját tesztkészletében tartani, mert ez az egyetlen vizsgálat, amely megválaszolja az eredeti kérdést — tudja-e még egy megjelenítő megszerezni a sztringet. Egy következmény, amire számítani kell: a bontás kicseréli az oldalobjektumokat, így minden FPDF_PAGEOBJECT handle, amelyet tartott, utána halott, ugyanaz az élettartam-csapda, amelyet a elavult oldalobjektum handle-ek transzformáció után cikk ír le

Két szomszédos darab teszi teljessé a munkafolyamatot. Annak eldöntése, hova kerülnek a kitakaró téglalapok, általában kinyert geometriából indul, és a strukturált szövegblokkok és olvasási sorrend blokk- és olvasásirend-modell jobb forrás a jelölt dobozokra, mint a nyers karakterfutamok. Az eredmény lektorhoz juttatása a biztonságos PDF-előnézet építése keményítési szabályaihoz tartozik, ahol az űrlapkitöltés és a JavaScript alapból kikapcsolva marad. Együtt lefedik a legtöbb megfelelőségi munkafolyamat által igényelt hurkot: megtalálás, operátorszintű kitakarás, ellenőrzés újranyitással, biztonságos előnézet. A teljes API-felület, a próbaverzió letöltése és a komponens licencfeltételei a PDFium Delphi Component termékoldalon élnek