Vienas neteisingas simbolis sąskaitos numeryje, o vienintelė turima redagavimo operacija perrašo visą teksto seką. PDF Library for Delphi panaikina šią spragą: GetTextBlockCharContentLocation kiekvieną išgautą UTF-16 poziciją susieja su turinio srauto instrukcija, operandu ir užkoduotu baitų diapazonu, iš kurio ji atsirado, o ReplaceTextBlockCharSourceBytes perrašo tik tą diapazoną. Įprastas teksto išgavimas paprastai išmeta viską, ko tam reikėtų. Gaunate Unicode, pločius ir geometriją, o kilmė išgaruoja, todėl 7 pozicijos simbolis 3 bloke lieka tiesiog simbolis. Kuris srautas jį sukūrė, kuri instrukcija, kuris operandas, kuris to operando baitas – viskas prarasta. Kiekviena taškinio redagavimo strategija, pastatyta ant tokio rezultato, turi spėti, dažniausiai ieškodama iškoduotame turinyje poeilutės ir tikėdamasi, kad ji pasitaiko lygiai vieną kartą. Tikrame puslapyje taip nebūna
Kodėl visos teksto sekos perrašymas sugadina puslapį?
Nes seka nėra vien tekstas. ISO 32000-1 §9.4.3 teksto rodymo operatoriai apima TJ, kurio operandas yra masyvas, kaitaliojantis eilutes su skaitiniais pataisymais, o būtent tie skaičiai sudaro tipografiką. Eilutėje [(AB) -120 (CD)] TJ tarp dviejų eilučių yra 120 tūkstantųjų em tarpas. Išveskite naują Tj su sujungtu tekstu ir tarpas dings, eilutė vos pastebimai persitvarkys, o formoje reikšmė išslinks iš savo laukelio. Tas pats prieštaravimas taikomas šriftui: operando baitai yra kodai ta koduote, kurią pasirinko Tf, o ne Unicode, ir sudėtiniame šrifte jie gali būti dviejų baitų CID be jokio ryšio su simboliu, kurį gavote iš ekstraktoriaus. Regeneruodami seką turėtumėte teisingai atkurti šrifto koduotę, jo /ToUnicode žemėlapį ir glifų aprėptį. Taškinis redagavimas viso to išvengia, nes niekada neišeina iš baitų srities
Ką grąžina GetTextBlockCharContentLocation?
Metodas vieną simbolį išsprendžia į devynių laukų įrašą, o kiekvienas laukas yra adresas, ne reikšmė. ContentLayer yra 1 pagrindu skaičiuojamas indeksas puslapio /Contents masyve arba 0, kai simbolis gautas iš įdėto turinio. StreamObjectNumber ir StreamGeneration identifikuoja turintį srautą. InstructionIndex yra 0 pagrindu skaičiuojama pozicija iškoduotoje turinio programoje, OperandIndex – teksto eilutės operandas, o ArrayElementIndex – elementas TJ masyve arba -1 tiesioginio eilutės operando atveju. Tada SourceByteOffset ir SourceByteLength nurodo baitų diapazoną tos iškoduotos eilutės viduje
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
// Block ir CharPos gaunami iš jūsų atliekamo GetTextBlockText nuskaitymo
If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags)= 1 Then
Begin
// ContentLayer = 0 reiškia, kad glifas yra įdėtame Form XObject
// ArrayElementIndex = -1 reiškia paprastą Tj operandą, ne TJ masyvą
End;
Finally
Lib.ReleaseTextBlocks(ListID);
End;
Finally
Lib.Free;
End;
End;
Paieška užklausos metu nieko nekainuoja. Kai atvaizduotuvas iškoduoja kiekvieną turinio sluoksnį, jis registruoja einamus loginius intervalus, todėl pozicijos užklausa yra dvejetainė paieška surikiuotame intervalų sąraše, o ne linijinis kiekvieno turinio intervalo tikrinimas kiekvienam simboliui. Kai klausiate, niekas neanalizuojama iš naujo; žemėlapis sukurtas per išgavimo etapą, už kurį jau sumokėjote. Jei jau išvardijate radinius su PDF teksto paieška, grąžinančia radinių koordinates, turinio vietos pridėjimas prie kiekvieno radinio beveik nieko nekainuoja
Redaguojame baitus, o ne Unicode
ReplaceTextBlockCharSourceBytes priima aktyvaus PDF šrifto koduotėje esančių neapdorotų pakaitinių baitų AnsiString. Toks yra visas dizainas, ir jis sąmoningas. Nieko neperkoduojama, niekas neperkoduoja iš naujo, niekas nespėja šrifto. Biblioteka įterpia jūsų baitus į nurodytą tikslinės eilutės diapazoną ir iš naujo išveda turinio sluoksnį, kuriame ji yra. Gretimos eilutės tame pačiame TJ masyve ir tarp jų esantys skaitiniai tarpai lieka baitas po baito identiški. Paimkime ankstesnį išdėstymą: radus B eilutėje [(AB) -120 (CD)] TJ, gaunama ArrayElementIndex 0, SourceByteOffset 1 ir SourceByteLength 1. Pakeiskite jį į Z ir išvestyje bus (AZ), po kurio vis dar eis nepakeisti -120 ir (CD). Regresijos rinkinys tikrina būtent tai, nes teiginys „išsaugojome kerningą“ yra tokio tipo, kuris tyliai nustoja galioti
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
// Kiekviena vieta sename sąraše dabar pasenusi. Išgaukite iš naujo.
Lib.ReleaseTextBlocks(ListID);
ListID:= Lib.ExtractPageTextBlocks(3);
End
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
// Nuo išgavimo sluoksnis pasikeitė
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
// Nepatikrinome vėliavos arba ją pridėjo vėlesnė versija
End;
Verta įsidėmėti dvi veikimo detales. Iškvietimas laikinai persijungia į puslapį, iš kurio išgautas teksto sąrašas, ir tiek sėkmės, tiek nesėkmės atveju grąžina anksčiau pasirinktą puslapį, todėl jūsų žymeklis tyliai nepajudinamas. Sėkmės atveju jis taip pat išvalo puslapio elementų momentines kopijas, todėl negaliojančiomis tampa visos rankenos, kurias laikėte iš ankstesnio išvardijimo etapo
Kurių simbolių negalima redaguoti?
Yra šešios kategorijos, o biblioteka kiekvieną jų įvardija Flags bitų kaukėje, užuot neaiškiai pranešusi apie nesėkmę. Tai svarbiau už sėkmingą pagrindinį atvejį, nes realiuose dokumentuose nesusiejami atvejai dažni, o kiekvieno jų priežastis kitokia
PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: kelios išgautos UTF-16 pozicijos išplečiamos iš vieno šaltinio glifo./ToUnicodeįrašas, vieną kodą susiejantis sufi, duoda du simbolius, dalijančius tą patį baitų diapazoną, todėl laikykite juos vienu šaltinio glifu ir redaguokite diapazoną vieną kartąPDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: simbolis sugeneruotas maketo metu. Įterpti žodžių tarpai yra įprastas atvejis, o šaltinio baitų jie neturi, todėlSourceByteOffsetgrąžina -1, oSourceByteLength– 0PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: perskaitytas tekstas atkeliavo iš/ActualTextpakaitalo. Nėra unikalaus atvirkštinio susiejimo iš pakeistos eilutės į šaltinio baitus, todėl vieta skirta tik diagnostikaiPDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: glifas yra Form XObject viduje. Baitai adresuojami, bet Form gali būti piešiamas keliuose puslapiuose, todėl redagavimas per aukšto lygio API būtų pakeitimas, kurio neprašėtePDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: operandas buvo šešioliktainė eilutė su UTF-16BE baitų tvarkos žyme, kurią esamas išgavimo kelias iškoduoja prieš šrifto susiejimą. Poslinkiai iškoduotame rezultate nebeadresuoja pradinių baitų, todėl galiojimo vėliava išvalomaPDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: eilutės operandas ir jo teksto rodymo operatorius yra dviejuose skirtinguose srautuose
Paskutinis atvejis vertas atskiro sakinio, nes inžinieriai dažnai mano, kad taip būti negali. ISO 32000-1 §7.8.2 sako, kad puslapio /Contents masyvo srautai sujungiami, o riba tarp jų privalo būti tik leksinė riba. Todėl BT /F1 16 Tf 220 340 Td (CrossLayer) viename sraute ir Tj ET kitame yra visiškai teisėtas puslapis. Susiejimas išlaiko diagnostinę vietą, bet pažymi ją tik skaitymui, nes operatoriaus instrukcijos indeksas priklauso kitam sluoksniui nei operando baitai, o naudojant vieną kito adresavimui failas būtų sugadintas
Kaip biblioteka žino, kad žemėlapis vis dar galioja?
Pirštų atspaudai, patikrinami iškart prieš rašymą. Kiekvienas išgavimo sąrašas įrašo šaltinio puslapį ir kiekvienam turinio sluoksniui sluoksnio ilgį bei dvi nepriklausomas slenkančias maišas: FNV-1a ir DJB2 tipo xor maišą. Prieš analizuodamas ką nors ReplaceTextBlockCharSourceBytes dar kartą perskaito tikslinį sluoksnį ir palygina visas tris reikšmes. Bet koks bet kurio baito pakeitimas sluoksnyje grąžina PDFLIB_ERROR_TEXT_LOCATION_STALE, o rašymas nevyksta. Tai sąmoningai konservatyvu: tikrinama kiekvieno sluoksnio, o ne kiekvienos instrukcijos būsena, todėl ir nesusijęs pakeitimas tame pačiame turinio sraute padaro vietą negaliojančią. Tai teisingas kompromisas: poslinkis sraute, kuris pasislinko nors vienu baitu, nėra beveik teisingas, o tylus sugadinimas. Ta pati disciplina taikoma likusiam redagavimo paviršiui, įskaitant turinio srauto CTM ir clipping būsenos sekiklį. Po kiekvieno sėkmingo pakeitimo išmeskite sąrašą ir išgaukite dar kartą
Tik skaitymo susiejimas per Direct Access
DAGetTextBlockCharContentLocation pateikia tą patį įrašą puslapiui, atvertam per Direct Access kelią, su tuo pačiu vėliavų žodynu. Pagal konstrukciją tai tik diagnostikos funkcija: ReplaceTextBlockCharSourceBytes veikia su pasirinktu redaguojamu dokumentu, o Direct Access yra tik skaitymo kelias. Vietos duomenys išlieka teksto blokų sąraše ir uždarius failo rankeną, todėl juos galima naudoti neprisijungus atliekamam auditui
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);
// Vietos lieka skaitomos po DACloseFile
Finally
Lib.DAReleaseTextBlocks(DirectList);
End;
Finally
Lib.DACloseFile(FileHandle);
End;
Naudokite tai klausimams atsakyti, o ne dalykams keisti. Kuriuose puslapiuose yra teksto, kurio niekada negalėtumėte redaguoti vietoje? Kokia šio korpuso dalis turi /ActualText perrašas? Kurio tiekėjo išvestis suskaido operatorius per turinio sluoksnius? Kai kiekvienas simbolis turi adresą, tokios užklausos pigios, todėl verta jas atlikti prieš pasirenkant taisymo konvejerį
Kur baigiasi taškinis redagavimas
Taškinis redagavimas yra skalpelis, ne teksto variklis. Jis keičia baitus vietoje, todėl už originalą platesnis ar siauresnis pakaitinis tekstas nepersidėlios, nebus naujai sulaužytas eilutėmis ir neatnaujins aplinkinių tarpų. Vieno skaitmens pakeitimas kitu monospaciniame laukelyje tinka. Pastraipos perrašymas – ne. Ir tai kategoriškai nėra saugumo priemonė: perrašius glifo baitus pradiniai baitai lieka atkuriami iš failo versijų istorijos, todėl viskas, kam reikia konfidencialumo, priklauso tikram redagavimui, kuris pašalina turinį, o ne jį uždengia. Už šiuos apribojimus gaunate sąžiningą elgesį. Kiekvienas simbolis turi baito adresą, su kuriuo galima veikti, arba įvardytą vėliavą, paaiškinančią, kodėl jo nėra, o piršto atspaudo patikra pasenusį žemėlapį paverčia kietąja klaida, o ne sugadintu puslapiu. Simbolio susiejimas su turinio baitais ir šaltinio baitų pakeitimas vietoje yra PDF Library for Delphi teksto išgavimo ir turinio redagavimo paviršiaus dalis, vietinė Object Pascal PDF biblioteka Delphi, C++Builder ir Lazarus