Един сгрешен character във invoice number и единственият наличен edit primitive пренаписва целия text run. PDF Library for Delphi запълва тази празнина: GetTextBlockCharContentLocation връща всяка извлечена UTF-16 позиция към content-stream instruction-а, operand-а и encoded byte range-а, които са я породили, а ReplaceTextBlockCharSourceBytes overwrite-ва само този range. Text extraction-ът обикновено изхвърля всичко, което би ви трябвало за това. Получавате Unicode, widths и geometry, а provenance-ът се изпарява, така че character-ът на position 7 в block 3 е просто character. Кой stream го е породил, коя instruction, кой operand, кой byte в този operand: изчезнали са. Всяка point-edit strategy, построена върху това, трябва да гадае, обикновено като търси substring в decoded content и се надява да се среща точно веднъж. На истинска страница това не се случва
Защо пренаписването на целия text run поврежда страницата?
Защото run-ът не е само текст. Text-showing operator-ите в ISO 32000-1 §9.4.3 включват TJ, чийто operand е array, в който string-ове се редуват с numeric adjustments, а тези числа са typesetting-ът. Ред, оформен като [(AB) -120 (CD)] TJ, носи kern от 120 хилядни от em между двата string-а. Emit-нете нов Tj със слепен текст и kern-ът изчезва, редът се reflow-ва с малко, а във form стойността се измества извън кутията си. Същото възражение важи за font-а: operand bytes са codes в encoding-а, избран от Tf, а не Unicode, и при composite font може да са двубайтови CIDs без никаква връзка с character-а, който extractor-ът ви е върнал. Regenerate-нете run-а и трябва да сте прави за font encoding-а, /ToUnicode map-а и glyph coverage-а. Point editing избягва всичко това, като никога не напуска byte domain-а
Какво връща GetTextBlockCharContentLocation?
Method-ът разрешава един character до record от девет полета и всяко поле е address, а не value. ContentLayer е 1-based index в page /Contents array или 0, когато character-ът идва от nested content. StreamObjectNumber и StreamGeneration идентифицират съдържащия stream. InstructionIndex е 0-based позиция в decoded content program, OperandIndex е text-string operand-ът, а ArrayElementIndex е element-ът вътре в TJ array или -1 за direct string operand. След това SourceByteOffset и SourceByteLength назовават byte range-а вътре в този decoded string
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 и CharPos идват от ваш scan на GetTextBlockText
If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags)= 1 Then
Begin
// ContentLayer = 0 означава, че glyph-ът живее в nested Form XObject
// ArrayElementIndex = -1 означава plain Tj operand, не TJ array
End;
Finally
Lib.ReleaseTextBlocks(ListID);
End;
Finally
Lib.Free;
End;
End;
Lookup-ът не струва нищо по време на query. Докато renderer-ът decode-ва всеки content layer, той регистрира logical span-овете, които обхожда, така че query за position е binary search върху подреден interval list, а не linear scan на всеки content span за всеки character. Нищо не се reparse-ва, когато поискате данните; map-ът е построен в extraction pass-а, за който вече сте платили. Ако вече enumerate-вате hits с PDF text search, който връща hit coordinates, добавянето на content location за всеки hit е почти безплатно
Редактиране на bytes, не на Unicode
ReplaceTextBlockCharSourceBytes приема AnsiString от raw replacement bytes в active PDF font encoding. Това е целият дизайн и е умишлено. Нищо не transcode-ва, нищо не re-encode-ва, нищо не гадае за font-а. Library-то splice-ва вашите bytes върху назования range на target string-а и re-emit-ва съдържащия content layer. Съседните string-ове в същия TJ array и numeric kern-овете между тях остават byte-identical. Вземете горния layout: намирането на B в [(AB) -120 (CD)] TJ дава ArrayElementIndex 0, SourceByteOffset 1 и SourceByteLength 1. Replace-нете го с Z и emitted content ще съдържа (AZ), последвано пак от -120 и (CD), и двете недокоснати. Regression suite-ът проверява точно това, защото твърдението „запазихме kerning-а“ е от онези, които тихо престават да бъдат верни
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
// Всеки location в стария list вече е stale. Извлечете отново
Lib.ReleaseTextBlocks(ListID);
ListID:= Lib.ExtractPageTextBlocks(3);
End
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
// Layer-ът се е променил след extraction-а
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
// Flag, който не сме проверили, или flag, добавен от по-нова версия
End;
Два оперативни детайла си струва да се запомнят. Call-ът временно превключва към страницата, от която е извлечен text list-ът, и възстановява преди това избраната page както при успех, така и при failure, така че не мести cursor-а ви мълчаливо. При success той изчиства page element snapshot-ите, което инвалидира всички handles, които сте държали от по-ранен enumeration pass
Кои characters не могат да се редактират?
Шест категории, като library-то назовава всяка в Flags bitmask-а, вместо да се проваля неясно. Това е по-важно от happy path-а, защото в реални document-и unmappable случаите са чести и всеки има различна причина
PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: няколко извлечени UTF-16 позиции се разгръщат от един source glyph./ToUnicodeentry, който map-ва един code къмfi, ви дава два characters, споделящи един byte range, така че ги третирайте като един source glyph и редактирайте range-а веднъжPDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: character-ът е синтезиран по време на layout. Inferred word spaces са обичайният случай и нямат source bytes, затоваSourceByteOffsetидва като -1, аSourceByteLengthе 0PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: прочетеният текст е дошъл от/ActualTextreplacement. Няма уникално reverse mapping от substituted string към source bytes, затова location-ът е само диагностиченPDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: glyph-ът е във Form XObject. Bytes са addressable, но Form-ът може да се рисува от няколко pages, така че editing през high-level API би било edit, който не сте поискалиPDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: operand-ът е бил hex string с UTF-16BE byte order mark, който съществуващият extraction path decode-ва преди font mapping. Offset-ите към decoded result вече не адресират original bytes, затова valid flag-ът е изчистенPDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: string operand-ът и неговият text-showing operator живеят в два различни stream-а
Последният случай заслужава собствено изречение, защото инженерите редовно приемат, че не може да се случи. ISO 32000-1 §7.8.2 казва, че stream-овете в page /Contents array се concatenat-ват и разделянето между тях трябва да пада само на lexical boundary. Следователно BT /F1 16 Tf 220 340 Td (CrossLayer) в един stream и Tj ET в следващия са напълно законна page. Mapping-ът запазва diagnostic position-а, но го маркира read-only, защото instruction index-ът на operator-а принадлежи на различен layer от bytes на operand-а и използването на едното за адресиране на другото би повредило файла
Как library-то разбира дали map-ът още е валиден?
Чрез fingerprints, проверени непосредствено преди write. Всеки extraction list записва source page-а плюс за всеки content layer дължината на layer-а и два независими rolling hash-а: FNV-1a hash и DJB2-style xor hash. Преди ReplaceTextBlockCharSourceBytes да parse-не каквото и да е, той прочита target layer-а отново и сравнява и трите стойности. Всяка byte промяна където и да е в layer-а връща PDFLIB_ERROR_TEXT_LOCATION_STALE и write-ът не се извършва. Това е умишлено консервативно: проверката е per layer, не per instruction, така че дори несвързан edit на друго място в същия content stream инвалидира location-а ви. Това е правилният trade: offset към stream, изместен дори с един byte, не е почти попадение, а silent corruption. Същата дисциплина управлява останалата editing surface, включително content-stream state tracker-а за CTM и clipping. След всяка успешна replacement изхвърлете list-а и извлечете отново
Read-only mapping през Direct Access
DAGetTextBlockCharContentLocation ви дава идентичния record за page, отворена през Direct Access path-а, с идентичния flag vocabulary. По конструкция той е само диагностичен: ReplaceTextBlockCharSourceBytes работи върху избрания editable document, а Direct Access е read path. Location data-та остава в text block list-а след затваряне на file handle-а, което я прави използваема за offline auditing
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);
// Location-ите остават readable след DACloseFile
Finally
Lib.DAReleaseTextBlocks(DirectList);
End;
Finally
Lib.DACloseFile(FileHandle);
End;
Използвайте го, за да отговаряте на въпроси, а не за да променяте неща. Кои pages съдържат text, който никога не бихте могли да редактирате на място? Каква част от този corpus пристига с /ActualText overrides? Output-ът на кой vendor разделя operator-и между content layers? Това са евтини query-та, когато всеки character има address, и си струва да ги пуснете, преди да се ангажирате с correction pipeline
Къде спира point editing-ът
Point editing е scalpel, не text engine. Той променя bytes на място, така че replacement text, който е по-широк или по-тесен от оригинала, няма да reflow-не, няма да rewrap-не и няма да обнови kern-овете около себе си. Замяната на един digit с друг в monospaced field е добър fit. Препечатването на paragraph не е. И категорично не е security tool: overwrite-ването на glyph bytes оставя оригиналните bytes recoverable от revision history на файла, затова всичко с изискване за confidentiality принадлежи на true redaction, която премахва content вместо да го покрива. В замяна на тези ограничения получавате честност. Всеки character или има byte address, върху който можете да действате, или named flag, който казва защо няма, а fingerprint check-ът превръща stale map-а в hard error вместо в corrupted page. Character-to-content-byte mapping и in-place source byte replacement са част от text extraction и content editing surface-а на PDF Library for Delphi, native Object Pascal PDF library за Delphi, C++Builder и Lazarus