Műszaki cikk

PDF-szöveg content-stream byte-leképezése Delphiben

Egy hibás karakter a számlaszámban, és az egyetlen kéznél lévő szerkesztési primitív az egész text run újraírása. A PDF Library for Delphi ezt a rést zárja be: a GetTextBlockCharContentLocation minden kinyert UTF-16 pozíciót visszaképez arra a content-stream utasításra, operandusra és kódolt bájttartományra, amely létrehozta, a ReplaceTextBlockCharSourceBytes pedig csak ezt a tartományt írja felül. A szövegkinyerés normál esetben eldob mindent, amire ehhez szükséged lenne. Megmarad a Unicode, a szélesség és a geometria, a származási hely pedig eltűnik, így a 3. blokk 7. pozícióján lévő karakter egyszerűen csak egy karakter. Melyik streamből jött, melyik utasításból, melyik operandusból, az operandus melyik bájtjából: mind elveszett. Minden erre épített pontszerkesztési stratégia kénytelen találgatni, általában a dekódolt contentben keres egy részszöveget, és reméli, hogy pontosan egyszer fordul elő. Valós oldalon ez ritkán igaz

Miért teszi tönkre az oldalt egy teljes text run újraírása?

Mert a run nem csak szöveg. Az ISO 32000-1 §9.4.3 szövegmegjelenítő operátorai között ott a TJ, amelynek operandusa stringeket és numerikus igazításokat váltogató tömb, és ezek a számok adják a tördelést. A [(AB) -120 (CD)] TJ alakú sor a két string között 120 ezred emnyi kernelést hordoz. Ha összefűzött szöveggel új Tj-t bocsátasz ki, a kernelés elveszik, a sor egy hajszálnyit újratördelődik, egy űrlapon pedig az érték kicsúszik a dobozából. Ugyanez az ellenvetés érvényes a fontra: az operandus bájtjai annak a kódolásnak a kódjai, amelyet a Tf választott, nem Unicode-karakterek, kompozit fontnál pedig kétbájtos CID-ek lehetnek, amelyeknek semmi kapcsolatuk nincs a kinyerő által visszaadott karakterrel. A run újragenerálásakor el kell találnod a font kódolását, a /ToUnicode térképet és a glyph-lefedettséget. A pontszerkesztés mindezt megkerüli azzal, hogy végig a bájttartományban marad

Mit ad vissza a GetTextBlockCharContentLocation?

A metódus egy karaktert kilencmezős rekordra old fel, és minden mező cím, nem érték. A ContentLayer az oldal /Contents tömbjének 1-alapú indexe, vagy 0, ha a karakter beágyazott contentből jött. A StreamObjectNumber és a StreamGeneration a tartalmazó streamet azonosítja. Az InstructionIndex a dekódolt content program 0-alapú pozíciója, az OperandIndex a szöveges operandus, az ArrayElementIndex pedig a TJ tömbön belüli elem, vagy -1 közvetlen string operandusnál. A SourceByteOffset és a SourceByteLength ezután a dekódolt stringen belüli bájttartományt nevezi meg

Var
  Lib: TPDFlib;
  ListID, Block, CharPos: Integer;
  ContentLayer, StreamObjectNumber, StreamGeneration: Integer;
  InstructionIndex, OperandIndex, ArrayElementIndex: Integer;
  SourceByteOffset, SourceByteLength, Flags: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    Lib.LoadFromFile('invoice.pdf', '');
    Lib.SelectPage(1);
    ListID:= Lib.ExtractPageTextBlocks(3);
    Try
      // A Block és CharPos a GetTextBlockText saját bejárásából érkezik
      If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
        ContentLayer, StreamObjectNumber, StreamGeneration,
        InstructionIndex, OperandIndex, ArrayElementIndex,
        SourceByteOffset, SourceByteLength, Flags)= 1 Then
      Begin
        // ContentLayer = 0: a glyph beágyazott Form XObjectban él
        // ArrayElementIndex = -1: sima Tj operandus, nem TJ tömb
      End;
    Finally
      Lib.ReleaseTextBlocks(ListID);
    End;
  Finally
    Lib.Free;
  End;
End;

A lekérdezés futásidőben nem kerül semmibe. Miközben a renderer dekódolja az egyes content rétegeket, regisztrálja a bejárt logikai spaneket, ezért a pozíció lekérdezése rendezett intervallumlista bináris keresése, nem pedig minden karakterhez az összes content span lineáris bejárása. Amikor kéred, semmit sem parsol újra; a térkép már az általad kifizetett extraction menetben elkészült. Ha már a találati koordinátákat adó PDF-szövegkereséssel enumerálod a találatokat, minden találathoz content locationt adni szinte ingyenes

Byte-ok szerkesztése, nem Unicode

A ReplaceTextBlockCharSourceBytes aktív PDF-fontkódolásban lévő nyers helyettesítő byte-ok AnsiString-jét várja. Ez az egész terv, és szándékos. Nincs transzkódolás, újrakódolás vagy fonttal kapcsolatos találgatás. A library a megnevezett tartomány fölé illeszti a byte-jaidat, majd újra kibocsátja a befoglaló content réteget. Ugyanazon TJ tömb szomszédos stringjei és a köztük lévő numerikus kernelések byte-ról byte-ra változatlanok maradnak. A fenti elrendezésben a [(AB) -120 (CD)] TJ B karakterének helye ArrayElementIndex 0, SourceByteOffset 1, SourceByteLength 1. Ha Z-re cseréled, a kibocsátott content (AZ)-t tartalmaz, utána továbbra is -120 és (CD) jön, mindkettő érintetlenül. A regressziós suite ezt pontosan ellenőrzi, mert a „megőriztük a kernelést” típusú állítások csendben szoktak valótlanná válni

Function EditableHere(Flags: Integer): Boolean;
Begin
  Result:= ((Flags and PDF_TEXT_CHAR_CONTENT_LOCATION_VALID)<> 0)and
    ((Flags and (PDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED or
      PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT or
      PDF_TEXT_CHAR_CONTENT_LOCATION_NESTED or
      PDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER or
      PDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED))= 0);
End;

// ...
If EditableHere(Flags) Then
Begin
  If Lib.ReplaceTextBlockCharSourceBytes(ListID, Block, CharPos, 'Z')= 1 Then
  Begin
    // A régi lista minden locationje most már elavult. Nyerj ki újra.
    Lib.ReleaseTextBlocks(ListID);
    ListID:= Lib.ExtractPageTextBlocks(3);
  End
  Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
    // A réteg a kinyerés óta megváltozott alattunk
  Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
    // Egy ellenőrizetlen flag, vagy egy későbbi verzióban hozzáadott flag
End;

Két működési részletet érdemes rögzíteni. A hívás ideiglenesen arra az oldalra vált, amelyről a szöveglista készült, majd siker és hiba esetén is visszaállítja az előzőleg kiválasztott oldalt, ezért nem mozgatja el csendben a kurzorodat. Siker esetén törli az oldal elem-pillanatképeit is, ami érvénytelenít minden olyan handlet, amelyet egy korábbi enumerációs menetből tartottál meg

Mely karakterek nem szerkeszthetők?

Hat kategória van, és a library mindegyiket megnevezi a Flags bitmaszkban ahelyett, hogy homályos hibával leállna. Ez fontosabb a boldog útvonalnál, mert valódi dokumentumokban a nem leképezhető esetek gyakoriak, és mindegyik más okból keletkezik

  • PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: több kinyert UTF-16 pozíció egyetlen forrásglyphből bontakozik ki. Egy /ToUnicode bejegyzés, amely egy kódot fi-re képez, két olyan karaktert ad, amelyek ugyanazt a bájttartományt osztják, ezért egyetlen forrásglyphként kezeld őket, és egyszer szerkeszd a tartományt
  • PDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: a karakter a tördelés során jött létre. A kikövetkeztetett szóköz a tipikus eset, és nincs hozzá forrásbyte, ezért a SourceByteOffset -1, a SourceByteLength pedig 0
  • PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: az olvasott szöveg egy /ActualText helyettesítéséből jött. A helyettesített string és a forrásbyte-ok között nincs egyedi visszafelé leképezés, ezért a location csak diagnosztikai
  • PDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: a glyph egy Form XObjectban van. A byte-ok címezhetők, de a Formot több oldal is rajzolhatja, ezért a high-level API-n keresztüli módosítás olyan szerkesztés lenne, amelyet nem kértél
  • PDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: az operandus egy UTF-16BE byte order markot hordozó hex string volt, amelyet a meglévő extraction útvonal dekódol a fontleképezés előtt. A dekódolt eredmény offsetjei már nem címezik az eredeti byte-okat, ezért a valid flag törlődik
  • PDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: a string operandus és a hozzá tartozó text-showing operátor két különböző streamben él

Az utolsó eset külön mondatot érdemel, mert a mérnökök rendszeresen feltételezik, hogy nem fordulhat elő. Az ISO 32000-1 §7.8.2 szerint az oldal /Contents tömbjének streamjei össze vannak fűzve, a köztük lévő határnak pedig csak lexikális határon kell lennie. Ezért a BT /F1 16 Tf 220 340 Td (CrossLayer) az egyik streamben, a Tj ET pedig a következőben teljesen szabályos oldal. A mapping megtartja a diagnosztikai pozíciót, de csak olvashatóként jelöli, mert az operátor utasításindexe másik réteghez tartozik, mint az operandus byte-jai, és ha az egyikkel címeznéd a másikat, sérülne a fájl

Honnan tudja a library, hogy a mapping még érvényes?

Ujjlenyomatokból, amelyeket közvetlenül az írás előtt ellenőriz. Minden extraction lista rögzíti a forrásoldalt, és minden content réteghez a réteg hosszát, valamint két független gördülő hash-t: egy FNV-1a és egy DJB2-szerű XOR hash-t. A ReplaceTextBlockCharSourceBytes minden parsolás előtt újraolvassa a célréteget, és mindhárom értéket összeveti. A réteg bármely bájtjának változása PDFLIB_ERROR_TEXT_LOCATION_STALE hibát ad, az írás nem történik meg. Ez szándékosan konzervatív: az ellenőrzés rétegenkénti, nem utasításonkénti, ezért ugyanazon content stream más pontján végzett, független szerkesztés is érvényteleníti a locationt. Ez a helyes kompromisszum: egy olyan streamben lévő offset, amely akár egy bájttal is eltolódott, nem „majdnem jó”, hanem csendes korrupció. Ugyanez a fegyelem érvényes a szerkesztési felület többi részére, beleértve a CTM- és clipping-kezelés content-stream state trackerét. Sikeres csere után dobd el a listát, és nyerd ki újra

Csak olvasható mapping Direct Accessen keresztül

A DAGetTextBlockCharContentLocation ugyanazt a rekordot adja vissza ugyanazzal a flag-szókinccsel egy Direct Access útvonalon megnyitott oldalhoz. Felépítéséből adódóan csak diagnosztikai: a ReplaceTextBlockCharSourceBytes a kiválasztott szerkeszthető dokumentumon dolgozik, a Direct Access pedig olvasási útvonal. A location-adatok a fájlhandle bezárása után is megmaradnak a szövegblokk-listában, ezért offline audithoz is használhatók

FileHandle:= Lib.DAOpenFileReadOnly('audit.pdf', '');
Try
  PageRef:= Lib.DAFindPage(FileHandle, 1);
  DirectList:= Lib.DAExtractPageTextBlocks(FileHandle, PageRef, 3);
  Try
    Lib.DAGetTextBlockCharContentLocation(DirectList, Block, 1,
      ContentLayer, StreamObjectNumber, StreamGeneration,
      InstructionIndex, OperandIndex, ArrayElementIndex,
      SourceByteOffset, SourceByteLength, Flags);
    // A locationök a DACloseFile után is olvashatók maradnak
  Finally
    Lib.DAReleaseTextBlocks(DirectList);
  End;
Finally
  Lib.DACloseFile(FileHandle);
End;

Használd kérdések megválaszolására, ne módosításra. Mely oldalak hordoznak olyan szöveget, amelyet soha nem tudnál helyben szerkeszteni? A korpusz mekkora része érkezik /ActualText felülírásokkal? Melyik gyártó kimenete osztja szét az operátorokat content rétegek között? Miután minden karakternek címe van, ezek olcsó lekérdezések, és érdemes lefuttatni őket, mielőtt korrekciós pipeline mellett köteleződnél el

Hol ér véget a pontszerkesztés?

A pontszerkesztés szike, nem szövegmotor. Helyben módosít byte-okat, ezért az eredetinél szélesebb vagy keskenyebb csere nem tördel újra, nem csomagol új sort, és a környező kerneléseket sem frissíti. Egy számjegy másik számjegyre cseréje monospace mezőben jó illeszkedés. Egy bekezdés újragépelése nem az. Biztonsági eszköznek pedig kifejezetten nem való: a glyph-byte-ok felülírása az eredeti byte-okat visszanyerhetően hagyja a fájl revíziótörténetében, ezért minden titkossági követelményhez a valódi redakció tartozik, amely eltávolítja a tartalmat, nem csak eltakarja. Cserébe őszinte eredményt kapsz. Minden karakterhez vagy tartozik végrehajtható byte-cím, vagy egy névvel jelzett flag, amely elmagyarázza, miért nem, az ujjlenyomat-ellenőrzés pedig a régi mappinget kemény hibává teszi a sérült oldal helyett. A karakter és content-byte közötti mapping, valamint a helyben végzett forrásbyte-csere a PDF Library for Delphi text extraction és content editing felületének része, amely natív Object Pascal PDF library Delphihez, C++Builderhez és Lazarushoz