Technical Article

Пакетна проверка на PDF файлове в Delphi с CLI на PDFium Component

Пакетният инструмент за проверка (preflight) е конзолна програма без прозорец, насочена към папка с PDF файлове, която проверява всеки от тях спрямо посочените от вас стандарти за съответствие и оставя машинночетими доказателства за това, което е открила. Никой не стои да я наблюдава. Тя се изпълнява в два часа сутринта под cron или планировчика на задачи на Windows (Task Scheduler), или като етап в CI тръбопровод (CI pipeline), а следващият човек, който ще се интересува от нейния изход, е или планировчик, четящ код за изход, или одитор, отварящ отчет седмици по-късно. Това променя значението на понятието „правилноâ€? Двигателят за проверка на PDFium Component, PDF библиотека с изходен код за Delphi, C++Builder и Lazarus, прави самите повиквания за валидиране почти тривиални. Работата, която решава дали инструментът си заслужава усилията, се намира около тези повиквания: кой профил сте проверили, какво е казал кодът за изход на планировчика и дали отчетът, който би уловил грешка, все още съществува, когато някой започне да го търси.

Договорът: какво всъщност може да види планировчикът

Един CI бегач (runner) или планировчикът на задачи на Windows вижда точно две неща от вашия инструмент: кода за изход и всички файлове, които е оставил след себе си. Логове, конзолни цветове, извеждане на напредъка: всичко това е за човек, който гледа на живо, а в два часа сутринта няма никой. Така че коригирайте речника на кодовете за изход, преди да докоснете API, и го направете лесен и предвидим:

  • 0: всеки файл съответства на всеки заявен профил
  • 1: поне един файл е генерирал констатации от проверката
  • 2: самият инструмент е аварирал при поне един файл (повреден входен файл, заключване, срив)

Разликата между кодове 1 и 2 е тази, която екипите пропускат и по-късно съжаляват. Повреден PDF файл, който не се отваря, не е грешка при валидирането. Ако го причислите към код 1, купчина повредени сканирания ще се появят на вашите табла за управление като внезапен срив на съответствието, изпращайки някой да преследва регресия на стандартите, която никога не се е случвала, докато истинската история е повреден скенер по веригата.

Още два елемента принадлежат към договора. Първият е време на изчакване (timeout) за всеки файл. Необичаен PDF файл с хиляди страници и дълбоко вложени структури от обекти може да задържи един единствен процес на валидиране с минути, а нощният прозорец за изпълнение няма търпение за това. Прекратете задачата за този файл при крайния срок, отчетете го като повреда на инструмента и продължете с пакета. Вторият е карантинна директория: преместете всеки изтекъл по време или неотваряем входен файл настрани, вместо да го оставяте на място. В рамките на няколко месеца тази директория тихо ще натрупа най-лошите документи, които вашите реални клиенти изпращат, и този корпус от файлове струва повече за тестовете преди пускане в експлоатация от всеки синтетичен пример, който бихте могли да напишете на ръка.

Избор на стандарти и защо нивото на съответствие е важно

Изброяването TPdfPreflightStandard обхваща фамилиите, които се срещат в практиката: ppsPdfA за архивно съответствие по ISO 19005, ppsPdfUa за достъпност по ISO 14289, ppsPdfX за предпечатна подготовка, плюс ppsPdfE, ppsPdfR и ppsPdfVT за инженерна дейност, растерни изображения и променливи данни. В рамките на дадена фамилия ядрото разчита нивото на съответствие, което документът декларира, и го докладва за всеки стандарт в ConformanceName на резултата. Именуването на фамилията рядко е достатъчно, тъй като нивото е мястото, където е реалната разлика. PDF/A-2b обещава само визуално възпроизвеждане и нищо повече. PDF/A-3a добавя изискване за логическо маркиране на структурата (tagging) и позволява вградени изходни файлове, което е много по-висока летва за сканирани материали, които изобщо нямат дърво на таговете. Сгрешете това в която и да е посока и пакетният процес ще ви излъже. Ако вашата политика за съхранение всъщност изисква PDF/A-2b, но отхвърляте файлове поради липсващи тагове на структурата, отчетът ще се напълни с констатации, които никой никога няма да коригира. Приемете всеки етикет PDF/A, без да проверявате нивото, и ще одобрите документи, които отговарят на по-ниски стандарти от обещаните. Изискванията за достъпност от държавни купувачи все по-често добавят PDF/UA към всичко това, което не добавя допълнителни разходи към изпълнението, тъй като BuildPdfPreflightReport (от модула FPdfPreflightReport) приема набор от стандарти:

Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);

Едно извикване оценява и двата стандарта и връща един единствен консолидиран запис с отчет.

Защо празният списък с констатации не означава успешна проверка

Отчетът изброява констатациите за всеки стандарт, а празният списък с проблеми означава само „нÐ?са открити проблеми в стандартите, които действително са били изпълнениâ€? Това е много по-тясно твърдение от „файлъÑ?съответства на стандарта, който ви интересуваâ€? и разликата между двете е мястото, където пакетната проверка тихомълком се компрометира. Грешка в конфигурацията, която изключва ppsPdfA от набора, генерира точно същия празен списък с проблеми, както и наистина изряден файл. Затова се отнасяйте към мълчанието с подозрение. Обходете Report.Results и проверете две неща за всеки стандарт, който сте искали да тествате: че запис за резултат за него изобщо съществува и че неговият флаг IsCompliant, подкрепен от Status = pfsPass, е истина (true). Нощна задача, която приравнява „беÐ?намерени проблемиâ€?с „готоÐ?за архивиранеâ€? без никога да потвърди кои стандарти са били оценени, е класическият начин папка с несъответстващи файлове да преминава успешно с месеци, докато външен одитор не отвори някой от тях с veraPDF и целият архив не бъде поставен под въпрос.

Втори капан се крие в това какво всъщност представлява констатацията. Всяко TPdfPreflightIssue съдържа Code, Category, Description и Recommendation, и посочва нарушеното правило, а не страница или обект. Това е проектно решение с последствия за цикъла на обратна връзка. Отчетът казва на екипа, създаващ файловете, какъв клас дефект съществува (например невграден шрифт или липсващ XMP идентификатор), а намирането на конкретния нарушаващ обект е работа на следващия инструмент за отстраняване, а не на валидатора. Изграждайте вашите потребители на отчети спрямо стабилните стойности на Code, а не спрямо четимия за хора текст на описанието, който може да бъде пренаписан при следващи версии без предупреждение.

Отчетни файлове за машини и за дежурния специалист

Записът с отчета записва същите констатации в пет формата: SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile и SaveMarkdownToFile, всеки със съответна функция от тип ToJson, когато искате низа в паметта, а не на диска. Устоявайте на изкушението да изберете само един. Пишете JSON за конвейера (pipeline), така че CI да може да го прикачи към записа на задачата и да анализира кодовете за проблеми и статусите на отделните стандарти, без да чете неструктуриран текст. Пишете HTML за човека, който получава известие при авария, защото той се отваря във всеки браузър без никакви допълнителни инструменти. Двата формата заедно струват само един допълнителен ред код на файл и спестяват на дежурния ви инженер най-неприятната задача при пакетна обработка, а именно декомпилиране на необработен JSON блок в два часа сутринта, за да разбере кой файл се е повредил. Една дисциплина е по-важна от избора на формат: извеждайте името на всеки отчет от името на входния файл, а не от времеви печат, в противен случай две успоредни изпълнения ще преплетат отчети, които вече не можете да свържете с техните входящи файлове.

Праговете на сериозност принадлежат на конфигурацията, а не на кода. Анотация без алтернативно описание е критична грешка за портал за подаване на PDF/UA и пренебрежима бележка за вътрешен архив, въпреки че това е идентична констатация и в двата случая. Осигурете ниво за отхвърляне за всеки профил, така че политиката да може да се променя без повторно компилиране, и отбележете действащото ниво в самото резюме на задачата. През следващото тримесечие никой няма да помни под какъв праг е работил пакетът през миналия октомври, а резюмето е единственото място, където тази информация се пази.

Изолиране на файлове, така че един лош PDF да не провали целия пакет

procedure RunPreflightBatch(const InputDir, ReportDir: string;

  out FilesWithFindings, ToolFailures: Integer);

var

  SR: TSearchRec;

  Pdf: TPdf;

  Report: TPdfPreflightReport;

begin

  FilesWithFindings := 0;

  ToolFailures := 0;

  if FindFirst(InputDir + '*.pdf', faAnyFile, SR) = 0 then

  try

    repeat

      Pdf := TPdf.Create(nil);   // fresh instance per file: no state bleed

      try

        try

          Pdf.FileName := InputDir + SR.Name;

          Pdf.Active := True;

          if not Pdf.Active then  // load failures are silent, not raised

            raise EPdfError.Create('Cannot open ' + SR.Name);

          Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);

          Report.SaveJsonToFile(ReportDir + ChangeFileExt(SR.Name, '.json'));

          Report.SaveHtmlToFile(ReportDir + ChangeFileExt(SR.Name, '.html'));

          if Report.TotalIssueCount > 0 then

            Inc(FilesWithFindings);

        except

          on E: Exception do

          begin

            Inc(ToolFailures);   // exit-code-2 territory, not a validation verdict

            WriteLn(ErrOutput, SR.Name + ': ' + E.Message);

          end;

        end;

      finally

        Pdf.Free;

      end;

    until FindNext(SR) <> 0;

  finally

    FindClose(SR);

  end;

end;

Три съзнателни решения са заложени в този цикъл. Нов TPdf обект за всеки файл гарантира, че един документ, който повреди състоянието на ядрото, не може да отрови следващите го файлове. Явната проверка на Active намира своето място, тъй като Active := True потиска грешките при зареждане, вместо да ги повдига; премахнете тази защита и непълен файл ще премине към повикването за валидиране, преди да се срине някъде по веригата с подвеждащо съобщение. Вътрешният блок try..except се намира в обхвата за отделния файл нарочно, така че едно изключение да увеличи брояча на грешките и цикълът да продължи. Искате чисти отчети за 4999 добри файла, дори когато файл 5000 е повреден. Освен това и двата формата на отчетите се записват на диска преди отчитането на крайния резултат, което означава, че доказателствата оцеляват, дори ако по-късно се появи грешка в логиката за сумиране.

След това съпоставянето на кодовете за изход се свежда до няколко реда в проектния файл:

begin

  RunPreflightBatch(ParamStr(1), ParamStr(2), Findings, Failures);

  if Failures > 0 then

    Halt(2)

  else if Findings > 0 then

    Halt(1);

  // falling through exits with 0: every file conformed

end.

Какво проверката (preflight) няма да направи вместо вас

Ядрото открива; то не поправя. Констатация за невграден шрифт или зависимо от устройството цветово пространство е задание за работа за този, който генерира файловете, а валидаторът няма как да го пачне на място. Така че планирайте цикъла на обратна връзка съзнателно. Отчетите трябва да попадат там, където екипът производител действително ги чете, в противен случай ени и същи проблеми ще се появяват всяка нощ, докато някой накрая не попита защо процентът на съответствие никога не се подобрява. Също така си струва да се направи кръстосана проверка на извадка от резултати спрямо независим валидатор, veraPDF за PDF/A или инструмента за проверка на Acrobat за PDF/X, преди външен одитор да ги провери вместо вас. Когато две ядра се разминават по отношение на реален клиентски файл, този документ не е досадна пречка; той е точно този случай на регресия, който е липсвал в тестовете на вашата версия. Запазете го, наименувайте го и го изпълнявайте при всяко изграждане.

Още една полезна комбинация трябва да се отбележи. Същото ядро за валидиране управлява интерактивните проверки в потребителския интерфейс за преглед, така че този CLI без интерфейс и ориентираната към аналитици работна среда за преглед на входящи PDF файлове могат да споделят общ речник за валидиране, вместо да се раздалечават с времето. И тъй като [ppsPdfA, ppsPdfUa] оценява достъпността в същия проход, страната на PDF/UA от пакета се съгласува перфектно с разработката от страна на визуализатора, като например изграждането на достъпен PDF четец в Delphi. Профилите, форматите на отчетите и пълният API за проверка са документирани на продуктовата страница за PDFium Component.