Én feil tegn i et fakturanummer, og det eneste redigeringsprimitivet du har for hånden, skriver hele tekstløpet på nytt. PDF Library for Delphi lukker dette gapet: GetTextBlockCharContentLocation kobler hver uttrukket UTF-16-posisjon tilbake til content-stream-instruksjonen, operanden og det kodede byteområdet som produserte den, og ReplaceTextBlockCharSourceBytes overskriver bare dette området. Tekstuthenting kaster normalt bort alt du trenger til dette. Du får Unicode, bredder og geometri, mens opphavet forsvinner, så tegnet på posisjon 7 i blokk 3 bare er et tegn. Hvilken strøm produserte det, hvilken instruksjon, hvilken operand, hvilken byte inne i operanden: borte. Enhver punktredigeringsstrategi som bygges oppå det, må gjette, vanligvis ved å søke etter en dekodet delstreng i innholdet og håpe at den forekommer nøyaktig én gang. På en ekte side gjør den ikke det
Hvorfor ødelegger det siden å skrive hele tekstløpet på nytt?
Fordi løpet ikke bare er tekst. Tekstvisningsoperatorene i ISO 32000-1 §9.4.3 inkluderer TJ, der operanden er en matrise som fletter strenger sammen med numeriske justeringer, og tallene er satsen. En linje som er lagt ut som [(AB) -120 (CD)] TJ, har en 120-tusendels-em-kern mellom de to strengene. Sender du ut en ny Tj med den sammenkjedede teksten, er kerningen borte, linjen flyter litt ut og på et skjema flytter verdien seg ut av boksen. Den samme innvendingen gjelder fonten: Operand-byte-ene er koder i kodingen som Tf valgte, ikke Unicode, og i en sammensatt font kan de være to-byte-CID-er uten forhold til tegnet du leste ut. Regenererer du løpet, må du ha rett om fontens koding, /ToUnicode-kartet og glyph-dekningen. Punktredigering omgår alt dette ved aldri å forlate byte-domenet
Hva returnerer GetTextBlockCharContentLocation?
Metoden løser ett tegn til en record med ni felt, og hvert felt er en adresse, ikke en verdi. ContentLayer er den 1-baserte indeksen i sidens /Contents-matrise, eller 0 når tegnet kom fra nestet innhold. StreamObjectNumber og StreamGeneration identifiserer strømmen som inneholder det. InstructionIndex er den 0-baserte posisjonen i det dekodede innholdsprogrammet, OperandIndex er tekststreng-operanden og ArrayElementIndex er elementet inne i en TJ-matrise eller -1 for en direkte strengoperand. SourceByteOffset og SourceByteLength navngir deretter byteområdet inne i den dekodede strengen
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 og CharPos kommer fra din egen gjennomgang av GetTextBlockText
If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags)= 1 Then
Begin
// ContentLayer = 0 betyr at glyphen ligger i et nestet Form XObject
// ArrayElementIndex = -1 betyr en vanlig Tj-operand, ikke en TJ-matrise
End;
Finally
Lib.ReleaseTextBlocks(ListID);
End;
Finally
Lib.Free;
End;
End;
Oppslaget koster ingenting ved spørringstidspunktet. Mens rendereren dekoder hvert innholdslag, registrerer den de logiske spennene den går gjennom, så en posisjonsspørring er et binærsøk over en ordnet intervalliste, ikke en lineær skanning av alle innholdsspenn for hvert tegn. Ingenting parses på nytt når du spør; kartet ble bygget under uthentingspasset du allerede betalte for. Hvis du allerede teller treff med PDF-tekstsøk som returnerer treffkoordinater, er det nesten gratis å legge til en innholdsposisjon per treff
Rediger byte, ikke Unicode
ReplaceTextBlockCharSourceBytes tar en AnsiString med rå erstatningsbyte i den aktive PDF-fontkodingen. Det er hele designet, og det er med hensikt. Ingenting transkoder, ingenting omkoder og ingenting gjetter hvilken font som brukes. Biblioteket skjøter byte-ene dine over det navngitte området i målstrengen og skriver det inneholdende innholdslaget på nytt. Nabo-strenger i den samme TJ-matrisen og de numeriske kerningene mellom dem forblir byte-identiske. Ta layouten over: Å finne B i [(AB) -120 (CD)] TJ gir ArrayElementIndex 0, SourceByteOffset 1 og SourceByteLength 1. Bytt det ut med Z, så inneholder det utsendte innholdet (AZ), fortsatt fulgt av -120 og (CD), begge urørt. Regresjonstesten hevder nøyaktig dette, fordi "vi bevarte kerningen" er den typen påstand som stille slutter å være sann
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
// Alle posisjonene i den gamle listen er nå foreldede. Hent ut på nytt.
Lib.ReleaseTextBlocks(ListID);
ListID:= Lib.ExtractPageTextBlocks(3);
End
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
// Laget ble endret under oss etter uthentingen
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
// Et flagg vi ikke kontrollerte, eller et flagg som kom i en senere versjon
End;
To praktiske detaljer er verdt å ta inn over seg. Kallet bytter midlertidig til siden tekstlisten ble hentet fra og gjenoppretter den tidligere valgte siden både ved suksess og ved feil, så det flytter ikke markøren din i stillhet. Ved suksess tømmer det også øyeblikksbildene av sideelementene, noe som ugyldiggjør alle håndtak du holdt fra en tidligere opptellingsrunde
Hvilke tegn kan ikke redigeres?
Seks kategorier, og biblioteket navngir hver av dem i Flags-bitmasken i stedet for å feile vagt. Dette betyr mer enn happy path, fordi tilfellene uten mapping er vanlige i ekte dokumenter, og hvert av dem har sin egen grunn
PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: Flere uttrukne UTF-16-posisjoner utvides fra én kild glyph. En/ToUnicode-oppføring som mapper én kode tilfi, gir deg to tegn som deler ett byteområde, så behandle dem som én kild glyph og rediger området én gangPDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: Tegnet ble syntetisert under layout. Implisitte ordmellomrom er det vanlige tilfellet, og de har ingen kildebyte i det hele tatt, såSourceByteOffsetkommer tilbake som -1 ogSourceByteLengthsom 0PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: Teksten du leste, kom fra en/ActualText-erstatning. Det finnes ingen entydig omvendt mapping fra den erstattede strengen til kildebyte, så posisjonen er bare diagnostiskPDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: Glyphen ligger inne i et Form XObject. Byte-ene kan adresseres, men Form-en kan tegnes av flere sider, så redigering gjennom high-level API-et ville vært en redigering du ikke ba omPDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: Operanden var en hex-streng med et UTF-16BE-byteordremarkør, som den eksisterende uthentingsstien dekoder før fontmapping. Offset-er i det dekodede resultatet adresserer ikke lenger originalbyte-ene, så valid-flagget fjernesPDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: Strengoperanden og tekstvisningsoperatoren ligger i to forskjellige strømmer
Det siste punktet fortjener en egen setning, fordi ingeniører rutinemessig antar at det ikke kan skje. ISO 32000-1 §7.8.2 sier at strømmene i en sides /Contents-matrise settes sammen, og at skillet mellom dem bare må falle ved en leksikalsk grense. Derfor er BT /F1 16 Tf 220 340 Td (CrossLayer) i én strøm og Tj ET i den neste en helt lovlig side. Mappingen beholder den diagnostiske posisjonen, men merker den som skrivebeskyttet, fordi instruksjonsindeksen til operatoren tilhører et annet lag enn byte-ene til operanden, og bruk av den ene til å adressere den andre ville ødelagt filen
Hvordan vet biblioteket at kartet fortsatt er gyldig?
Fingeravtrykk, kontrollert umiddelbart før skriving. Hver uthentingsliste registrerer kildesiden og, for hvert innholdslag, laglengden og to uavhengige rullerende hasher: en FNV-1a-hash og en DJB2-lignende xor-hash. Før ReplaceTextBlockCharSourceBytes parser noe som helst, leser den mållaget på nytt og sammenligner alle tre verdiene. Enhver byteendring hvor som helst i laget returnerer PDFLIB_ERROR_TEXT_LOCATION_STALE, og skrivingen skjer ikke. Dette er med hensikt konservativt: Kontrollen er per lag, ikke per instruksjon, så en urelatert endring et annet sted i den samme content stream-en også ugyldiggjør posisjonen din. Det er riktig avveiing: Et offset inn i en strøm som har flyttet seg med bare én byte, er ikke nesten riktig, men stille korrupsjon. Den samme disiplinen styrer resten av redigeringsflaten, inkludert tilstandssporeren for CTM og clipping i content stream. Etter en vellykket erstatning skal listen forkastes og hentes ut på nytt
Skrivebeskyttet mapping gjennom Direct Access
DAGetTextBlockCharContentLocation gir deg den identiske record-en for en side som er åpnet gjennom Direct Access-stien, med det identiske flaggordforrådet. Den er bare diagnostisk, av konstruksjon: ReplaceTextBlockCharSourceBytes arbeider på det valgte redigerbare dokumentet, mens Direct Access er en lesesti. Posisjonsdataene overlever i tekstblokklisten etter at filhåndtaket er lukket, noe som gjør dem brukbare til offline-kontroll
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);
// Posisjoner kan fortsatt leses etter DACloseFile
Finally
Lib.DAReleaseTextBlocks(DirectList);
End;
Finally
Lib.DACloseFile(FileHandle);
End;
Bruk det til å svare på spørsmål, ikke til å endre ting. Hvilke sider inneholder tekst du aldri kunne redigere på stedet? Hvor mye av korpuset kommer med /ActualText-overstyringer? Hvilken leverandørs output deler operatorer over flere innholdslag? Dette er billige spørringer når hvert tegn har en adresse, og de er verdt å kjøre før du binder deg til en korrigeringspipeline
Hvor punktredigering stopper
Punktredigering er en skalpell, ikke en tekstmotor. Den endrer byte på stedet, så erstatningstekst som er bredere eller smalere enn originalen, flyter ikke på nytt, brytes ikke på nytt og oppdaterer ikke kerningene rundt seg. Å bytte ett siffer med et annet i et felt med fast bredde passer godt. Å skrive et avsnitt på nytt gjør det ikke. Og det er bestemt ikke et sikkerhetsverktøy: Å overskrive glyph-byte lar originalbyte-ene være gjenopprettelige fra filens revisjonshistorikk, så alt med krav om konfidensialitet hører hjemme i ekte redaksjon som fjerner innholdet i stedet for å dekke det. Til gjengjeld får du ærlighet. Hvert tegn har enten en byteadresse du kan handle på eller et navngitt flagg som forteller hvorfor det ikke har det, og fingeravtrykkskontrollen gjør et foreldet kart til en hard feil i stedet for en ødelagt side. Kobling fra tegn til innholdsbyte og erstatning av kildebyte på stedet leveres som del av flaten for tekstuthenting og innholdsredigering i PDF Library for Delphi, det native Object Pascal PDF-biblioteket for Delphi, C++Builder og Lazarus