Techninis straipsnis

Dvipusio skenavimo derinimas Delphi: PDF puslapių supynimas

CollateDocumentsEx Delphi PDF bibliotekoje PDFlibPas sujungia kelis atvertus dokumentus į vieną supintą dokumentą. Funkcija kiekvienu ratu prideda po GroupSize puslapių iš kiekvieno šaltinio, priima kiekvienam šaltiniui atskirą puslapių intervalų sąrašą ir mažėjančią seką, tokią kaip 3-1, laiko to šaltinio apvertimu. Vienu kvietimu priekinis rietuvas ir apverstas galinis rietuvas virsta skaitymo tvarka

Situacija už šios API yra kasdieniška ir itin dažna. Lapinis skeneris su vienpusiu keliu perleidžia visą rietuvą priekine puse žemyn, tada operatorius rietuvą apverčia ir perleidžia dar kartą. Gaunami du PDF failai: priekinės pusės tvarkingai, galinės pusės atbulai. Vartotojas nori vieno failo — 1 puslapio priekinė pusė, 1 puslapio galinė pusė, 2 puslapio priekinė pusė ir taip toliau. Šis straipsnis skirtas eiliškumo problemai ir po ja slypinčiam resursų dubliavimo spąstui. Jei jus domina grynas sujungimo pralaidumas, žr. straipsnį greitas PDF sujungimas per baitų lygio nuorodų poslinkį; jei įvesties failai per dideli, kad tilptų atmintyje, žr. gigabaitinių PDF failų sujungimą ir skaidymą su tiesiogine prieiga

Skeneris sukuria du rietuvus, vienas iš jų — atbulas

Derinimas nėra sujungimas. Sujungimas jungia puslapių intervalus vienas po kito; derinimas juos supina, o supynimo raštas priklauso nuo fizinio įrenginio, sukūrusio įvestį. Suklyskite su raštu, ir failas bus ne šiek tiek klaidingas, o visiškai neskaitomas: kas antras puslapis priklausys kitam lapui. Beveik kiekvieną realų atvejį apibūdina trys kintamieji: kiek šaltinių dalyvauja rotacijoje, kiek puslapių paimama iš kiekvieno šaltinio per vieną ratą ir ar kurį nors šaltinį reikia skaityti atbulai. CollateDocuments apima pirmus du dalykus paprastu dokumentų rankenų masyvu ir GroupSize sveikuoju skaičiumi. CollateDocumentsEx prideda trečią, priimdama kabliataškiu atskirtą puslapių intervalų sąrašą, po vieną segmentą kiekvienam šaltiniui, kur tuščias segmentas reiškia visus to šaltinio puslapius, o mažėjantis intervalas jį apverčia. Abi funkcijos prideda duomenis dabar pasirinkto dokumento pabaigoje ir grąžina 1 sėkmės atveju, 0 — bet kokio atmetimo atveju

Kodėl paprastas derinimas padaugina failo dydį?

Todėl, kad importo žemėlapis, susiejantis šaltinio objektų numerius su tikslo objektų numeriais, atkuriamas iš naujo kiekvieno kopijavimo iškvietimo metu, o viskas, kas pasiekiama iš daugiau nei vieno fragmento, importuojama po kartą kiekvienam fragmentui. PDFlibPas viduje TPDFDocument.CopyPagesFromDoc kiekvieno iškvietimo pradžioje atstato savo NewIndObjList. Šis sąrašas — vienintelė kopijuotojo atmintis apie tai, kas jau perkelta. Iškvietus jį kartą su dešimties puslapių intervalu, visiems dešimčiai puslapių bendras šriftas įterpiamas vieną kartą. Iškvietus jį dešimt kartų po vieną puslapį, tas pats šriftas įterpiamas dešimt kartų. Tai svarbiau nuskaitytiems dokumentams nei teksto dokumentams, nes nuskaitytas puslapis yra vienas didelis vaizdo XObject, o bendri objektai — tie, kurie tikrai daug sveria: įterptas ICC profilis, bendra /DecodeParms grandinė, kiekvienam lapui pritaikytas antspaudo ar vandenženklio formos XObject, OCR teksto sluoksnio šriftas. Akivaizdus būdas parašyti kaitinį derinimą — ciklas per ratus, ir šis ciklas kaip tik yra patologinis atvejis

// 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;

Dvylika ratų, du šaltiniai, dvidešimt keturi importo žemėlapiai. Niekas apie tai neįspėja. Puslapių tvarka teisinga, kiekvienas puslapis atvaizduojamas teisingai, o vienintelis simptomas — failas kelis kartus didesnis nei jo įvesties sumos. 300 puslapių partijos darbe šis daugiklis nėra apvalinimo paklaida — tai skirtumas tarp archyvo, tinkančio saugojimo biudžetui, ir tokio, kuris netinka

Importuok kartą, tada pertvarkyk puslapių medį

Sprendimas — atskirti du rūpesčius, kuriuos paprastas ciklas buvo sulydęs. Kopijavimas nusprendžia, kokie objektai atsiras tiksle; tvarkymas nusprendžia, kur puslapiai atsidurs puslapių medyje. CollateDocumentsEx kiekvieną šaltinį kopijuoja lygiai vieną kartą, vienu CopyPagesFromDoc iškvietimu su viso to šaltinio intervalu, todėl kiekvienas šaltinis gauna po vieną importo žemėlapį, o bendri resursai įrašomi vieną kartą. Tik kai visi šaltiniai jau perkelti, prasideda supynimas, ir jis vyksta vien per TPDFPageTree.MovePage

Puslapių perkėlimas šia prasme nieko nekainuoja. ISO 32000-1 §7.7.3 apibrėžia puslapių medį kaip subalansuotą mazgų žodynų struktūrą, kurios /Kids masyvai laiko netiesiogines nuorodas, o /Count kiekviename mazge neša lapų suminį skaičių. Puslapio perkėlimas reiškia vienos netiesioginės nuorodos pašalinimą iš vieno /Kids masyvo, jos įterpimą į kitą, abiejų /Count reikšmių koregavimą ir puslapio /Parent perkreipimą. Nekeičiamas nei vienas turinio srautas, nedubliuojamas nei vienas resursas, nekuriamas nei vienas objektas. Puslapio objektas išlaiko savo objekto numerį, todėl objektų numeriai išlieka stabilūs taip pat, kaip aprašyta straipsnyje apie puslapių pakeitimą, išsaugantį objektų numerius. Yra dar viena detalė, kurią paprastas puslapio perkėlimas sugadina, o MovePage — ne. ISO 32000-1 §7.7.3.4 leidžia /Resources, /MediaBox, /CropBox ir /Rotate paveldėti iš protėvio mazgo, o ne nurodyti pačiame puslapyje. Puslapis, kuris savo resursus paveldi iš mazgo A ir tada perkeliamas po mazgu B, tyliai paveldi kitą reikšmę arba visai jokios. MovePage dėl to prieš perkėlimą išsprendžia paveldėtą reikšmę ir įrašo ją į puslapio žodyną, todėl puslapis perkėlimo metu išlaiko savo pačias savybes

Ką iš tikrųjų daro pertvarkymo etapas?

Jis atlieka pasirinkimo rikiavimą pagal įterpimo semantiką. Pageidaujama bloko vidinė tvarka apskaičiuojama pirmiausia: einama per šaltinius rotacijos tvarka, iš kiekvieno paimama iki GroupSize indeksų, praleidžiamas išsekęs šaltinis, kartojama, kol visi puslapiai bus išdėstyti. Taip gaunama pridėto bloko permutacija. Šio rezultato pritaikymas — nepatogi dalis, nes MovePage yra įterpimas, ne sukeitimas, todėl kiekvienas perkėlimas pastumia viską tarp senos ir naujos pozicijos per vieną

Realizacija palaiko Current masyvą, modeliuojantį, kur šiuo metu yra kiekvienas pridėtas puslapis, ieško nuo pozicijos K į priekį puslapio, kuris turėtų būti K vietoje, atlieka perkėlimą, tada pastumia masyvo įrašus taip, kad jie atspindėtų, ką perkėlimas padarė medžiui. Tai O(n kvadratu) masyvo operacijų atžvilgiu ir nulis objektų kopijų atžvilgiu, o tai — teisingas kompromisas šiai užduočiai: 500 puslapių derinimas yra ketvirtis milijono sveikaskaitinių perstūmimų ir nė vienas dubliuoto vaizdo baitas. Mažėjantiems intervalams ir pasikartojantiems puslapiams šiame etape nereikia jokio specialaus apdorojimo, nes PLParsePageRangeList kviečiama su išjungtu rikiavimu ir leidžiamais dublikatais, todėl prašoma tvarka nepakinta per skaitymą

Apversti intervalai ir vienas dvipusio derinimo kvietimas

Kai apvertimas išreiškiamas per intervalą, plokščiaskeneriško dvigubo perleidimo atvejis susitraukia į vieną kvietimą. Priekinės pusės nori savo natūralios tvarkos, o galinės pusės nori 12-1, o tuščias pirmasis segmentas prieš kabliataškį reiškia, kad pirmasis šaltinis prisideda visais savo puslapiais

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;

Verta aiškiai paminėti du šio kodo fragmento elgesio bruožus. Suderinti puslapiai pridedami prie pasirinkto dokumento, todėl dokumentas, sukurtas su NewDocument, prieš juos prideda savo pradinį tuščią puslapį, ir jį reikia ištrinti, jei jo nenorite. Be to, šaltiniai gali būti nelygūs: su GroupSize lygiu 2 prie trijų ir penkių puslapių šaltinių, ratai išeina A1 A2 B1 B2, tada A3 B3 B4, kai A beveik išseko, tada vien B5, nes išsekęs šaltinis tiesiog praleidžiamas, o ne užpildomas

Atšaukimas, formos laukai ir tai, kas nepersikelia

Kiekvienas argumentas patikrinamas prieš paliečiant tikslą. Trūkstama dokumento rankena, pasirinktas dokumentas, nurodytas kaip savas šaltinis, GroupSize mažesnis už vienetą, segmentų skaičius, neatitinkantis šaltinių skaičiaus, intervalas, nurodantis puslapį, kurio šaltinis neturi: visa tai grąžina 0, tikslui liekant nepakitusiam. Nesėkmė kopijavimo metu — sudėtingesnis atvejis, ir ji tvarkoma per viešąjį DeletePages, o ne per žemesnio lygio PageTree.DeletePages. Priežastis konkreti. Kopijavimas vyksta su įjungtu MergeFormData, todėl šaltinio formos laukai jau būna pridėti prie tikslo /AcroForm /Fields masyvo iki tada, kai vėlesnis šaltinis suklysta. Puslapių ištrynimas puslapių medžio lygyje pašalintų valdiklio puslapius ir paliktų tuos laukų nuorodas kabančias; viešasis kelias atsieja lauko, struktūros ir straipsnių gijų nuorodas kartu su puslapiais

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]));

Būkite sąžiningi su savo vartotojais dėl šio mechanizmo ribų. Derinimas perkelia puslapius, jų anotacijas ir formos laukus, jis sujungia AcroForm laukų sąrašą, skaičiavimo tvarkos masyvą ir numatytąjį resursų žodyną. Jis neperkelia šaltinio žymelių: nuskaityto priekinės pusės rietuvo struktūros medis beveik visada tuščias, todėl dvipusio derinimo atveju niekas neprarandama, tačiau jei suderinate du parengtus dokumentus, jų struktūros medžiai lieka atgal, o navigaciją teks atkurti pačiam. Pavadintos paskirties vietos, gyvavusios tik šaltinio kataloge, atsiduria toje pačioje padėtyje. Suplanuokite tai, prieš žadėdami klientui nuostolių neturintį derinimą

PDFlibPas siunčia derinimo funkcijas kartu su likusia savo puslapių surinkimo funkcionalu paviršiaus dalimi, todėl skenerio darbo eiga, intervalais pagrįstas ištraukimas ir dideliems failams skirti keliai visi telpa už vieno komponento Delphi ir C++Builder aplinkose. Visa API dokumentacija ir bandomoji versija — produkto puslapyje losLab Delphi PDF biblioteka