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

Зачерване на PDF на ниво оператори в Delphi

Някой рисува черна кутия върху име, не флатва нищо, предава файла, и рецензентът избира правоъгълника и поставя името в имейл. PDFiumPas отговаря на това с зачерване на ниво оператори: SaveAsRedacted изтрива само Unicode скаларите, чиито кутии на знаци докосват зачерваващ правоъгълник, преизгражда оцелелите от оригиналния шрифт, размер, матрица, режим на рендериране и цвят и изрязва изравнените по оси пътища и изображения, вместо да ги изпуска цели

Защо нарисуван правоъгълник не е зачерване

Операция за рисуване, добавена отгоре на поток от съдържание, не скрива нищо, защото операторите за показване на текст отдолу все още са в потока и все още се картографират към кодови точки. ISO 32000-1 §9.4 дефинира текстов обект като последователност от оператори за позициониране и показване вътре в BT и ET; запълнен правоъгълник, нарисуван след това, е просто друг оператор в същия поток. Извличането обхожда операторите, а не пикселите, така че покритият низ се връща непокътнат. Истинското зачерване трябва да премахне операнда, а не да замъгли изхода

Очевидната безопасна имплементация е брутална: намери всеки обект на страница, чиято обграждаща кутия пресича зачерваващ правоъгълник, и изтрий целия обект. Това правеха по-ранните версии на PDFiumPas, и е коректно, но скъпо. Един-единствен Tj може да носи цял ред от таблица, така че зачервяването на един номер на сметка взимаше със себе си датата, описанието и сумата. Правоъгълно запълване, което случайно е лента от таблица на цяла широчина, изчезваше по цялата страница. Лого на фактура изчезваше, защото зачервяването отрязваше единия му ъгъл. Версия 3.101.0 мести решението едно ниво надолу, от обекта на страница към операнда

Какво реално изтрива зачерването на ниво оператори?

PDFiumPas изтрива Unicode скалари, не текстови обекти. По време на SaveAsRedacted компонентът изгражда картографиране от знаци към обекти на страница от заредената текстова страница, после за всеки знак, притежаван от обекта под тест, прочита кутията на знака и пресича тази кутия с всеки зачерваващ правоъгълник. Знаци, които докосват правоъгълник, се маркират за премахване; останалите се маркират като оцелели. Ако нищо не пресича, обектът се оставя напълно на мира. Ако всеки знак пресича, обектът се премахва цял, точно както преди. Само смесеният случай задейства разделяне

Зачерване на ниво оператори в PDFiumPas, сравнено с изтриване на цели обекти в Delphi: старият път изпуска цял текстов обект, когато един номер на сметка е покрит, докато пътят на разделяне изтрива само пресичащите се знаци и преизлъчва всеки оцелял като собствен текстов обект
Само смесеният случай задейства разделяне: нищо пресичащо се оставя обекта на мира, всичко пресичащо се го премахва цял

Всеки оцелял после се преизлъчва като собствен текстов обект, изграден от оригиналната дръжка на шрифта, оригиналния размер на шрифта, текстовата матрица за знак, оригиналния режим на текстово рендериране и състоянието на запълване и очертаване на родителския обект, включително широчина на щриха, съединение на линии, завършване на линии и dash масив. Преизползването на дръжката на шрифта, вместо да се разрешава нова, е това, което държи глифовете метрично идентични, а преизползването на матрицата за знак е това, което държи кърнинга и разстоянието между думите на място, без да се пуска подредбата наново. Цената е броят обекти: един задържан знак става един текстов обект, което е причината TPdfRedactionOptions.MaxSplitObjects да съществува като твърд таван на генерираните фрагменти

procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
  Pdf: TPdf;
  Options: TPdfRedactionOptions;
  Report: TPdfRedactionReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := SourcePdf;   // файлът вече носи /Redact анотации
    Pdf.Active := True;

    Options := TPdfRedactionOptions.Default;
    Options.PreservePartialObjects := True;    // разделяне на ниво оператори (по подразбиране)
    Options.RemoveIntersectingAnnotations := True;
    Options.MaxSplitObjects := 20000;          // таван на генерираните фрагменти

    if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
      raise Exception.Create(Report.ErrorMessage);   // провали се затворено, не доставяй
  finally
    Pdf.Free;
  end;
end;

Правоъгълниците се изрязват, завъртяната геометрия не

Пътища се разделят само когато PDFiumPas може да докаже, че пътят е правоъгълник, изравнен по осите. Доказателството е нарочно тясно: матрицата на обекта трябва да има и двата shear члена под 0.0001, пътят трябва да се състои от четири до шест сегмента, започващи с MOVETO и продължаващи само с LINETO, а трансформираните точки трябва да кацат на всичките четири ъгъла на границите на обекта в толеранс 0.01. Път, който минава тази проверка, се редуцира чрез последователно изваждане на правоъгълници, като всеки зачерваващ правоъгълник издълбава множеството оцелели в ленти вляво, вдясно, отдолу и отгоре, а всяка получена лента се преизгражда с оригиналния режим на запълване, флага за очертаване и състоянието на рисуване. Криви, триъгълници, клипнати форми и всичко завъртяно не минават проверката и целият обект се премахва

Изображенията следват ISO 32000-1 §8.9, където семплите на изображението заемат единичния квадрат, картиран през текущата матрица на трансформация. PDFiumPas обръща това картографиране, за да превърне всеки оцелял фрагмент в пространството на страницата обратно в нормализирани координати на изображението, ограничава ги до единичния интервал и после конвертира към пикселни индекси, като закръгля навътре: левият и горният ръб минават през Ceil, десният и долният през Floor. Посоката има значение. Закръглянето навън би оставило частична колона изходни пиксели от зачервената страна да оцелее на ръба на фрагмента. Целочислените пикселни граници после се конвертират обратно в нормализирани координати и се използват за извеждане на матрицата на фрагмента, така че изрязаният bitmap каца точно на пикселната граница, на която е бил отрязан. Самото изрязване е копиране на редове, съобразено със stride, през форматите Gray, BGR, BGRx и BGRA. Както при пътищата, завъртяно или изкривено изображение, или такова, чиято матрица има дегенериращ член на мащаба, се премахва изцяло

Как PDFiumPas изрязва частично зачервено изображение в Delphi: оцелелият фрагмент в пространството на страницата се картира обратно през обърнатата CTM в нормализирани координати на изображението, ограничава се до единичния интервал и се закръгля навътре, така че никоя зачервена пикселна колона не оцелява
Ceil отляво и отгоре, Floor отдясно и отдолу, така че разрезът каца на цяла пикселна граница
// След успешно извикване на SaveAsRedacted
Writeln(Format('applied %d redaction(s) on %d page(s)',
  [Report.RedactionCount, Report.RedactedPageCount]));
Writeln(Format('scanned %d object(s), removed %d',
  [Report.ScannedObjectCount, Report.RemovedObjectCount]));
Writeln(Format('split text/path/image: %d / %d / %d',
  [Report.SplitTextObjectCount, Report.SplitPathObjectCount,
   Report.SplitImageObjectCount]));
Writeln(Format('preserved %d fragment(s)', [Report.PreservedFragmentCount]));
Writeln(Format('pruned %d resource name(s), swept %d object(s)',
  [Report.ResourcePruneReport.RemovedNameCount,
   Report.ResourcePruneReport.RemovedObjectCount]));

if Report.PreservedFragmentCount = 0 then
  // нищо не е могло да се раздели: всеки пресичащ се обект е изпуснат цял
  LogWholeObjectFallback(SourcePdf);

Защо PDFiumPas се проваля затворено при немапирани знаци?

Защото глиф, който няма възпроизводим Unicode скалар, не може да бъде преизграден честно. Реконструирането на оцелял значи извикване на API-то за задаване на текст с низ, а това изисква стабилна кодова точка за всеки задържан знак. Символни частични шрифтове със счупени или липсващи данни ToUnicode могат да дадат празно картографиране, а прекодирането чрез догадки би произвело изход, който изглежда правилен на екрана, докато носи различен знак отдолу. PDFiumPas отказва: проверката на задържаните знаци хвърля, изключението се хваща вътре в SaveAsRedacted, TPdfRedactionReport.Succeeded се връща False със съобщението в ErrorMessage, а функцията връща False. Същото правило важи за бюджета на разделяне, който хвърля, вместо тихо да отрязва множеството фрагменти. Когато документът има шрифтове, на които не вярвате, и искате детерминистичното старо поведение, задайте Options.PreservePartialObjects := False и всеки пресичащ се обект си отива цял

Подрязване на ресурси през споделени обхвати

Разделянето на обекти оставя сираци зад себе си, а подрязването им не е толкова просто, колкото диф на речника /Resources на ниво страница. ISO 32000-1 §7.8.3 позволява един и същ речник на ресурси да бъде референциран едновременно от няколко страници, от Form XObject-и, от шарки и от appearance stream-и на анотации. Изтриването на име на шрифт, защото една страница е спряла да го ползва, ще счупи друга страница, която още го ползва. PruneUnusedPdfResources затова работи за обхват: разрешава /Contents, независимо дали е директен масив, непряка препратка към масив или един поток, после събира използването на ресурси от операторите, които реално именуват ресурси — Tf за шрифтове, Do за XObject-и, gs за графично състояние, CS, cs, SCN и scn за цветови пространства и шарки, sh за сенки, BDC и DP за свойства на маркирано съдържание, плюс записа /CS на вградени изображения. Когато един речник е споделен от няколко обхвата, множествата използвани имена се обединяват за категория, преди нещо да бъде премахнато

Подрязване на ресурси в PDFiumPas: три обхвата референцират един споделен речник на ресурси, техните множества използвани имена се обединяват за категория, и само имената, които никой обхват не референцира, се премахват, преди недостижимите обекти да бъдат изметени
Един речник може да обслужва няколко страници, Form XObject-и и appearance stream-и, затова PDFiumPas обединява всяко множество използвани имена, преди да изпусне едно-единствено име

Само имена, потвърдени като нереференцирани във всеки обхват, който сочи към речника, се изпускат. Обхват, който не може да бъде парсиран с увереност, се оставя непипнат, което е консервативната посока: неподрязан файл е само по-голям, погрешно подрязан е повреден. Оцелелите речници се записват обратно като разредено инкрементално обновяване, носещо точните номера на генерации, а после пренаписване на достижимостта измета обектите, станали недостижими, след като имената изчезнат. TPdfResourcePruneReport докладва ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, байтовите броилки и флаг Succeeded. SaveAsRedacted пуска тази стъпка автоматично върху санитизирания изход, така че пътят на зачерването вече я включва, но функцията е изнесена на ниво поток за конвейери, които я искат сама

uses
  FPdfCompress;

procedure PruneResourceNames(const SourcePdf, TargetPdf: string);
var
  Source, Dest: TFileStream;
  Report: TPdfResourcePruneReport;
begin
  Source := TFileStream.Create(SourcePdf, fmOpenRead or fmShareDenyWrite);
  try
    Dest := TFileStream.Create(TargetPdf, fmCreate);
    try
      // AllowSignedDocument остава False: инкременталното пренаписване би
      // анулирало байтовите диапазони, които подписът покрива
      PruneUnusedPdfResources(Source, Dest, Report);
      if not Report.Succeeded then
        raise Exception.Create(Report.ErrorMessage);
      Writeln(Format('%d name(s) removed from %d scope(s), %d -> %d bytes',
        [Report.RemovedNameCount, Report.UpdatedScopeCount,
         Report.SourceByteCount, Report.OutputByteCount]));
    finally
      Dest.Free;
    end;
  finally
    Source.Free;
  end;
end;

Включване в документен конвейер

Пътят на зачерването никога не мутира документа, който сте заредили. SaveAsRedacted прихваща изолирана снимка, прилага там анотациите /Redact, разсъблича прикачени файлове, пуска санитизиращия проход, който премахва open action, действията на каталога, дърветата от имена, свързаните файлове, AcroForm-ата и метаданните, подрязва ресурсите и чак тогава записва изходния поток. Отварянето на този изход наново като независим документ и повторното извличане на текста е стъпката за проверка, която си заслужава да държите в собствената си тестова кутия, защото е единствената проверка, която отговаря на оригиналния въпрос — може ли читател още да получи низа. Едно последствие, за което да планирате: разделянето заменя обекти на страница, така че всяка дръжка FPDF_PAGEOBJECT, която сте държали, е мъртва след това, същият капан за живот, описан в остарели дръжки на обекти на страница след трансформация

Две съседни парчета правят работния поток пълен. Решаването къде отиват зачерваващите правоъгълници обикновено тръгва от извлечена геометрия, а моделът на блокове и ред на четене в структурирани текстови блокове и ред на четене е по-добър източник на кандидатски кутии от сурови последователности от знаци. Показването на резултата на рецензент принадлежи към правилата за заздравяване в изграждане на защитен PDF преглед, където попълването на формуляри и JavaScript остават изключени по подразбиране. Заедно те покриват цикъла, от който повечето работни потоци за съответствие се нуждаят: локализирай, зачерви на ниво оператори, провери чрез повторно отваряне, покажи безопасно. Пълната API повърхност, пробното изтегляне и лицензионните условия за компонента живеят на продуктовата страница на PDFium Delphi Component