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/ToUnicodebejegyzés, amely egy kódotfi-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ánytPDF_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 aSourceByteOffset-1, aSourceByteLengthpedig 0PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: az olvasott szöveg egy/ActualTexthelyettesí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 diagnosztikaiPDF_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élPDF_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ődikPDF_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