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

Комплектоване на дуплекс сканирания в Delphi

CollateDocumentsEx в PDF библиотеката PDFlibPas за Delphi обединява няколко отворени документа в един преплетен документ. Тя добавя по GroupSize страници от всеки източник на всеки кръг, приема списък от диапазони на страници за всеки източник и третира низходящ диапазон като 3-1 като обръщане на този източник. Едно извикване превръща купчина с лицеви страници и обърната купчина с гръбни страници в правилния ред на четене

Сценарият зад този API е обикновен и изключително разпространен: подаващ лист по лист скенер с еднолицев тракт минава през цялата купчина с лицето надолу, после операторът обръща купчината и минава отново. Резултатът са два PDF файла — лицеви страници по ред и гръбни страници в обратен ред. Това, което потребителят иска, е един файл: страница 1 лице, страница 1 гръб, страница 2 лице и така нататък. Тази статия разглежда проблема с подредбата и капана с дублиране на ресурси, който стои зад него. Ако интересът ви е чиста производителност при конкатенация, вижте бързо обединяване на PDF чрез изместване на референции на байтово ниво; ако входните файлове са твърде големи, за да се съберат в паметта, вижте обединяване и разделяне на гигабайтови PDF файлове с директен достъп

Скенерът произвежда две купчини, едната от които е обърната

Комплектоването не е обединяване. Обединяването конкатенира диапазони от страници; комплектоването ги преплита, а моделът на преплитане е свойство на физическото устройство, произвело входа. Сгрешите ли модела, файлът не е леко объркан — той е нечетим: всяка втора страница принадлежи на различен лист. Три променливи описват почти всеки реален случай: колко източника участват в ротацията, колко страници идват от всеки източник за кръг и дали някой източник трябва да се чете в обратен ред. CollateDocuments покрива първите две с обикновен масив от манипулатори на документи и цяло число GroupSize. CollateDocumentsEx добавя третото, като приема списък от диапазони на страници, разделени с точка и запетая, по един сегмент за източник, където празен сегмент означава всички страници на този източник, а низходящ диапазон го обръща. И двете функции добавят страници в края на текущо избрания документ и връщат 1 при успех, 0 при всякакъв отказ

Защо наивното комплектоване умножава размера на файла?

Защото картата за импортиране, съпоставяща номерата на обектите от източника с тези в целта, се изгражда наново при всяко извикване на копиране, а всичко достижимо от повече от един откъс се импортира по веднъж на откъс. Вътре в PDFlibPas TPDFDocument.CopyPagesFromDoc нулира своя NewIndObjList в началото на всяко извикване. Този списък е единствената памет, която копиращият механизъм има за вече пренесеното. Извикате ли го веднъж с диапазон от десет страници, споделен от всичките десет страници шрифт се вгражда веднъж. Извикате ли го десет пъти с по една страница, същият шрифт се вгражда десет пъти. Това има много по-голямо значение за сканирания, отколкото за текстови документи, защото сканираната страница е един голям изображен XObject, а споделените обекти са тези с реално тегло: вграден ICC профил, споделена верига от /DecodeParms, форма XObject с печат или воден знак, приложена върху всеки лист, шрифтът на слоя с OCR текст. Очевидният начин да напишете комплектоване с обхождане на кръговете е цикъл по кръговете, и точно този цикъл е патологичният случай

// Do not do this. Each CopyPageRanges call rebuilds the import map,
// so anything the two sources share internally is imported once per
// round instead of once per source.
var
  RoundIndex: Integer;
begin
  for RoundIndex := 1 to 12 do
  begin
    PDF.CopyPageRanges(Fronts, IntToStr(RoundIndex));
    PDF.CopyPageRanges(Backs, IntToStr(13 - RoundIndex));
  end;
end;

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

Импортирайте веднъж, после пренаредете дървото на страниците

Решението е да се разделят двата аспекта, слети в наивния цикъл. Копирането решава кои обекти съществуват в целта; подредбата решава къде седят страниците в дървото на страниците. CollateDocumentsEx копира всеки източник точно веднъж, с едно извикване на CopyPagesFromDoc с пълния диапазон на този източник, така че всеки източник получава една карта за импортиране и споделените ресурси се записват веднъж. Едва след като всеки източник е приземен, се извършва преплитането, и то изцяло чрез TPDFPageTree.MovePage

Преместванията на страници са безплатни в смисъла, който има значение тук. ISO 32000-1 §7.7.3 дефинира дървото на страниците като балансирана структура от речници на възли, чиито масиви /Kids държат индиректни референции, като /Count носи общия брой листа при всеки възел. Преместването на страница означава премахване на една индиректна референция от един масив /Kids, вмъкването ѝ в друг, коригиране и на двете стойности на /Count и пренасочване на /Parent на страницата. Никой поток със съдържание не се пипа, никой ресурс не се дублира, никой обект не се създава. Обектът на страницата запазва своя номер на обект, което е и причината номерата на обектите да остават стабилни по същия начин, както в замяна на страници със запазване на номерата на обектите. Има една допълнителна подробност, която наивно преместване на страница обърква, а MovePage — не. ISO 32000-1 §7.7.3.4 позволява /Resources, /MediaBox, /CropBox и /Rotate да се наследяват от родителски възел, вместо да бъдат зададени на самата страница. Страница, наследяваща ресурсите си от възел A и после преместена под възел B, безмълвно наследява нещо различно или изобщо нищо. Затова MovePage преди преместването разрешава наследената стойност и я записва в речника на страницата, така че страницата да носи собствените си атрибути през преместването

Какво всъщност прави преходът на пренареждане?

Той изпълнява сортиране чрез избор спрямо семантика на вмъкване на позиция. Желаният ред, относителен спрямо блока, се изчислява първо: обхождат се източниците в ротация, вземат се до GroupSize индекса от всеки, пропуска се изчерпан източник, повтаря се, докато всяка страница бъде поставена. Това дава пермутация върху добавения блок. Прилагането ѝ е неудобната част, защото MovePage е вмъкване, не размяна, така че всяко преместване измества с една позиция всичко между старата и новата позиция

Имплементацията поддържа масив Current, моделиращ къде в момента се намира всяка добавена страница, сканира напред от позиция K за страницата, която принадлежи на K, издава преместването, после плъзга записите в масива, за да отрази това, което преместването е направило с дървото. Това е O(n на квадрат) в операции с масива и нула в копия на обекти, което е правилният компромис за тази натовареност: комплектоване на 500 страници е четвърт милион разбърквания на цели числа и нито един байт дублирани данни за изображение. Низходящите диапазони и повтарящите се страници не се нуждаят от специална обработка в този проход, защото PLParsePageRangeList се извиква със сортиране изключено и с разрешени дубликати, така че заявеният ред оцелява разбора непокътнат

Обърнати диапазони и еднократното дуплекс обединяване

С обръщането, изразено като диапазон, случаят с двоен проход на планшетен скенер се свежда до едно извикване. Лицевите страници искат естествения си ред, а гръбните искат 12-1, а празният първи сегмент преди точката и запетаята казва, че първият източник допринася с всичките си страници

var
  PDF: TPDFlib;
  Target, Fronts, Backs: Integer;
begin
  PDF := TPDFlib.Create;
  try
    Target := PDF.NewDocument;
    if PDF.LoadFromFile('fronts.pdf', '') <> 1 then
      Exit;
    Fronts := PDF.SelectedDocument;
    if PDF.LoadFromFile('backs.pdf', '') <> 1 then
      Exit;
    Backs := PDF.SelectedDocument;
    PDF.SelectDocument(Target);
    // fronts 1..12 in order, backs scanned in reverse: F1 B12 F2 B11 ...
    if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 1 then
      PDF.SaveToFile('duplex.pdf');
  finally
    PDF.Free;
  end;
end;

Две поведения в този откъс заслужават изрично споменаване. Комплектованите страници се добавят към избрания документ, така че документ, създаден с NewDocument, допринася своята начална празна страница преди тях и трябва да я изтриете, ако не я искате. Освен това източниците могат да са неравни: при GroupSize 2 върху източник от три страници и източник от пет страници кръговете излизат A1 A2 B1 B2, после A3 B3 B4, когато A вече е почти изчерпан, после B5 самостоятелно, защото изчерпан източник просто се пропуска, вместо да се допълва

Отмяна, полета на формуляри и какво не се пренася

Всеки аргумент се валидира, преди целта да бъде докосната. Липсващ манипулатор на документ, избраният документ, посочен като собствен източник, GroupSize под едно, брой сегменти, който не съвпада с броя източници, диапазон, назоваващ страница, която източникът няма — всички те връщат 0 без промяна в целта. Провалът по време на копиране е по-трудният случай и се обработва чрез публичния DeletePages, а не суровия PageTree.DeletePages. Причината е конкретна. Копирането протича с включен MergeFormData, така че полетата на формуляра от източника вече са добавени към масива /AcroForm /Fields на целта, когато по-късен източник се провали. Изтриването на страниците на ниво дърво на страниците би премахнало страниците с уиджети и би оставило тези референции към полета висящи; публичният път разкача референциите към полето, контура и веригите от статии заедно със страниците

if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 0 then
  // Nothing was appended and the target is byte-identical to before.
  // 412 is the copy failure; 0 means the arguments were rejected
  // during validation, before any page was touched.
  Log(Format('collate rejected, LastErrorCode=%d', [PDF.LastErrorCode]));

Бъдете честни с потребителите си относно границите. Комплектоването пренася страниците, техните анотации и полетата на формуляра им и обединява списъка с полета на AcroForm, масива за ред на изчисление и речника с ресурси по подразбиране. То не пренася отметки от източника: дървото с контур на сканирана купчина лицеви страници почти винаги е празно, така че при дуплекс случая нищо не се губи, но ако комплектовате два авторски документа, техните контури остават назад и вие сами възстановявате навигацията. Именуваните дестинации, които са съществували само в каталога на източника, са в същото положение. Планирайте за това, преди да обещаете на клиент безпотребно комплектоване

PDFlibPas доставя функциите за комплектоване заедно с останалата повърхност за сглобяване на страници, така че работният процес за сканиране, извличането на база на диапазон и пътищата за големи файлове седят зад един компонент в Delphi и C++Builder. Пълната справка за API и пробна версия за изтегляне са на страницата на продукта losLab Delphi PDF library