Техническа статия

Остарял текст след редактиране: кешът FPDF_TEXTPAGE на PDFium

Извиквате AddText, за да поставите ред върху PDF страница с PDFiumPas, след което веднага извиквате FindFirst, за да потвърдите, че печатът е добавен, но търсенето връща празен резултат. Текстът е на страницата — Acrobat го показва — но компонентът TPdf на PDFiumPas поддържа отделна кеширана структура FPDF_TEXTPAGE, анализирана веднъж от потока със съдържанието на страницата, а самата редакция не обновява тази структура автоматично. Ако я запитате, преди да е обновена, ще прочетете страницата точно такава, каквато е била преди промяната, а не след нея

Защо PDFium връща остарял текст веднага след редакция

PDFiumPas обвива графичния енджин PDFium на Google за Delphi и C++Builder, а неговите извиквания за текст и редактиране достигат до две различни подсистеми вътре в този енджин. FPDF_TEXTPAGE принадлежи към страната за четене: FPDFText_LoadPage обхожда потока със съдържанието на страницата веднъж и изгражда текстовата страница — кодове на символи, позиции, метрики на шрифтовете и граници на думите — а PDFiumPas кешира тази структура, докато страницата остане заредена. Извикванията за редактиране като FPDFPage_InsertObject или FPDFPage_GenerateContent работят с напълно различно представяне, графа от обекти и потока на съдържанието на страницата, а PDFium не пренася сам тези промени към вече отворена текстова страница. Повторното ѝ изграждане при всяка редакция би направило пакетното редактиране неприемливо бавно, затова дизайнът заменя тази цена с правило — притежателят на манипулатора го затваря след редакция, променяща съдържанието, а следващото четене изгражда нов

Текстовият кеш на TPdf отвътре: FTextPage, LoadTextPage и UnloadTextPage

TPdf проследява кеширания манипулатор в едно частно поле, FTextPage, и обвива жизнения му цикъл с два метода. LoadTextPage проверява дали FTextPage е nil и само тогава извиква FPDFText_LoadPage за текущата страница; ако манипулаторът вече съществува, LoadTextPage го използва повторно, без да проверява дали страницата се е променила, откакто е бил изграден. UnloadTextPage е другата половина: той затваря нативния манипулатор с FPDFText_ClosePage, отново задава FTextPage на nil и изтрива кеширания списък с уеб връзки и всяка текуща сесия за търсене, защото и двете са извлечени от същата текстова страница и остаряват по същата причина

Повторното използване на LoadTextPage без проверка е именно причината последователността да е важна. Всяко текстово запитване към TPdfText, FindFirst, GetWebLinks — първо преминава през LoadTextPage, така че докато FTextPage все още съдържа манипулатора отпреди редакцията, никое от тези извиквания няма как да разбере, че е настъпила промяна. Навигацията по страниците никога не е била рискът тук: UnloadPage, който се изпълнява при смяна на страница, презареждане и затваряне на документа, винаги е затварял текстовата страница заедно със самата страница. Въпросът винаги е бил за редакции върху страницата, на която все още се намирате

Кои методи на PDFiumPas обновяват кеша автоматично

Собствените методи на TPdf за редактиране на страници — AddText, SetText, SetTextPositions, AddPath, RemoveObject и InsertFormObjectFromXObject — всеки извиква UnloadTextPage, преди да извика UpdatePage (FPDFPage_GenerateContent на PDFium), за да сериализира промяната в потока със съдържанието. След всяко от тези извиквания следващото Text, FindFirst или GetWebLinks извикване изгражда текстовата страница от съдържанието в текущото му състояние и не е нужно допълнително извикване от ваша страна

var
  Pdf: TPdf;
  Index: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    Pdf.AddText('Reviewed by J. Alvarez', 'Helvetica', 10, 72, 40, clBlack, 255, 0);
    // AddText already closed the cached text page, so this FindFirst
    // call rebuilds it fresh before it searches
    Index := Pdf.FindFirst('Reviewed by J. Alvarez');
    if Index >= 0 then
      ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
  finally
    Pdf.Free;
  end;
end;

Моделът, който все още се поврежда: кеширане на суровия манипулатор TextPage

TPdf предоставя активния манипулатор чрез свойство само за четене TextPage за редкия случай, в който трябва да извикате функция FPDFText_*, която PDFiumPas не е обвил. Тази възможност за заобикаляне е и единственото място, където автоматичното обезсилване не може да помогне: след като копирате стойността FPDF_TEXTPAGE от свойството в локална променлива, PDFiumPas няма как да разбере, че все още я държите, нито как да обнови копието ви, когато UnloadTextPage се изпълни другаде в кода

var
  Pdf: TPdf;
  RawHandle: FPDF_TEXTPAGE;
  StaleCount: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    RawHandle := Pdf.TextPage;    // FPDFText_LoadPage handle, cached in FTextPage
    Pdf.SetText(0, 'Amended Clause 4.2');
    // SetText already closed RawHandle and set Pdf.TextPage back to nil.
    // Calling any FPDFText_* function against the old value now touches a
    // handle PDFium has already freed — undefined behavior, not a bug you
    // can catch with a nil check
    StaleCount := FPDFText_CountChars(RawHandle);
  finally
    Pdf.Free;
  end;
end;

Използването на манипулатор след изпълнението на FPDFText_ClosePage върху него е неопределено поведение в самия PDFium, а не конвенция на PDFiumPas, която можете да пренебрегнете — той може да върне последните известни данни, да не върне нищо или да срине процеса, като приложният код не бива да зависи от това кое ще се случи при конкретна компилация. Безопасното правило е ограничено: прочетете Pdf.TextPage отново непосредствено преди извикването на FPDFText_*, което го изисква, и никога не пазете копие през оператор, който може да редактира страницата

Групирайте редакциите, след което направете едно запитване

Това не означава, че след всяко извикване на AddText или RemoveObject е нужно защитно текстово запитване, което да проверява резултата. Всеки метод за редактиране вече заплаща цената за затваряне на текстовата страница веднъж; запитването след всяка отделна редакция в цикъл плаща тази цена отново без полза, защото FPDFText_LoadPage обхожда целия поток със съдържанието наново всеки път, когато се изпълни

var
  Pdf: TPdf;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'watermarked.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    // Strip every text object that looks like a draft watermark. Each
    // RemoveObject call already invalidates the cache on its own, so
    // nothing needs refreshing by hand between iterations
    for I := Pdf.ObjectCount - 1 downto 0 do
      if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
        Pdf.RemoveObject(I, True);

    // Query once, after the whole batch is done, not once per removal
    if Pdf.FindFirst('DRAFT') < 0 then
      ShowMessage('Watermark cleared');
  finally
    Pdf.Free;
  end;
end;

Същата логика за групиране важи конкретно и за състоянието на търсенето. FindNext и FindPrevious продължават сесия, започната от FindFirst, а UnloadTextPage прекратява тази сесия заедно с всичко останало, така че повторното извикване на FindNext след редакция вместо ново извикване на FindFirst води до изключение, а не до тихо продължаване на търсене в съдържание, което вече не съществува. Приемайте всяка редакция като твърда граница както за текстовото съдържание, така и за позицията в търсенето, и оставяйте едно ново FindFirst след редакциите да възобнови търсенето

Мястото на това поведение в извличането и работата с анотации

Извличането на обикновен текст — четене на текста на страница без промяна — никога не се сблъсква с нищо от това, защото няма редакция, която да обезсили манипулатор. За начина, по който Text, правоъгълниците на символите и границите на думите работят върху непроменена страница, придружаващата статия за извличане на текст с PDFiumPas разглежда темата без жизнения цикъл на кеша на текстовата страница, който тази статия добавя

Жизненият цикъл на кеша е най-важен при работни процеси, които редактират и веднага действат върху резултата: поставяне на корекция и търсене на нея, редактиране на абзац и потвърждаване, че е изчезнал, или намиране на фраза за закрепване на анотация за маркиране непосредствено след вмъкване на текст близо до нея. Последният случай заслужава отделно внимание — анотациите за маркиране с четириъгълни точки се позиционират от правоъгълниците на символите, прочетени от текстовата страница, така че анотация, изградена от координати, заснети преди редакция, ще маркира неправилното място, след като редакцията бъде приложена

API за редактиране и текст на TPdf са част от PDFium Component за Delphi и C++Builder, а продуктовата страница съдържа пълния справочник за методите при редактиране, извличане и търсене, разгледани тук