Tekninen artikkeli

Kaksipuolisten skannausten kokoaminen Delphissä: PDF-lomitusyhdistäminen

CollateDocumentsEx PDFlibPas Delphi PDF-kirjastossa yhdistää useita avoimia asiakirjoja yhdeksi lomitetuksi asiakirjaksi. Se liittää GroupSize-määrän sivuja kustakin lähteestä joka kierroksella, hyväksyy lähdekohtaisen sivualueluettelon ja käsittelee laskevaa aluetta kuten 3-1 kyseisen lähteen käännöksenä. Yhdellä kutsulla etupinosta ja käänteisestä takapinosta tulee lukujärjestys

Tämän API:n taustalla oleva skenaario on arkinen ja erittäin yleinen. Yksipuolinen arkinsyöttöskanneri ajaa koko pinon kuvapuoli alaspäin, jonka jälkeen käyttäjä kääntää pinon ja ajaa sen uudelleen. Lopputuloksena on kaksi PDF-tiedostoa: etusivut järjestyksessä, takasivut käänteisessä järjestyksessä. Käyttäjä haluaa yhden tiedoston, jossa on sivu 1 etu, sivu 1 taka, sivu 2 etu ja niin edelleen. Tämä artikkeli käsittelee järjestysongelmaa ja sen alla piilevää resurssien kopioitumisansaa. Jos kiinnostuksen kohteena on sen sijaan puhdas yhdistämisnopeus, katso nopea PDF-yhdistäminen tavutason viittaussiirroilla; jos syötteet ovat liian suuria mahtuakseen kokonaan muistiin, katso gigatavun kokoisten PDF-tiedostojen yhdistäminen ja jakaminen suoralla käytöllä

Skanneri tuottaa kaksi pinoa, joista toinen on väärinpäin

Kokoaminen ei ole yhdistämistä. Yhdistäminen ketjuttaa sivualueita; kokoaminen lomittaa ne, ja lomituskuvio on ominaisuus, joka riippuu syötteen tuottaneesta fyysisestä laitteesta. Jos kuvio menee väärin, tiedosto ei ole hieman väärä vaan lukukelvoton: joka toinen sivu kuuluu eri arkille. Kolme muuttujaa kuvaa lähes kaikki todelliset tapaukset: kuinka monta lähdettä on kierrossa, kuinka monta sivua kustakin lähteestä tulee joka kierroksella ja tarvitseeko jokin lähde lukea takaperin. CollateDocuments kattaa kaksi ensimmäistä yksinkertaisella asiakirjakahvojen taulukolla ja GroupSize-kokonaisluvulla. CollateDocumentsEx lisää kolmannen hyväksymällä puolipisteillä erotetun sivualueluettelon, jossa yksi segmentti vastaa yhtä lähdettä, tyhjä segmentti tarkoittaa kyseisen lähteen kaikkia sivuja ja laskeva alue kääntää sen. Molemmat funktiot liittävät sisällön parhaillaan valitun asiakirjan loppuun ja palauttavat arvon 1 onnistuessaan, 0 hylätessään

Miksi naiivi kokoaminen moninkertaistaa tiedostokoon?

Koska tuontikartta, joka kuvaa lähdeobjektinumerot kohdeobjektinumeroiksi, rakennetaan uudelleen jokaisella kopiointikutsulla, ja kaikki useammasta kuin yhdestä lohkosta saavutettavissa oleva tuodaan kerran per lohko. PDFlibPasin sisällä TPDFDocument.CopyPagesFromDoc nollaa NewIndObjList-listansa jokaisen kutsun alussa. Tämä lista on ainoa muisti, joka kopioijalla on siitä, mitä se on jo tuonut. Kutsu se kerran kymmenen sivun alueella, ja kaikkien kymmenen sivun jakama fontti upotetaan kerran. Kutsu se kymmenen kertaa yhdellä sivulla kerrallaan, ja sama fontti upotetaan kymmenen kertaa. Tämä merkitsee paljon enemmän skannauksissa kuin tekstiasiakirjoissa, koska skannattu sivu on yksi suuri kuva-XObject, ja jaetut objektit ovat niitä, joilla on todellista painoarvoa: upotettu ICC-profiili, jaettu /DecodeParms-ketju, jokaiseen arkkiin sovellettu leima- tai vesileimalomake-XObject, OCR-tekstikerroksen fontti. Ilmeisin tapa kirjoittaa vuorotteleva kokoaminen on silmukka kierrosten yli, ja tuo silmukka on juuri se patologinen tapaus

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

Kaksitoista kierrosta, kaksi lähdettä, kaksikymmentäneljä tuontikarttaa. Mikään ei varoita siitä. Sivujärjestys on oikea, jokainen sivu piirtyy, ja ainoa oire on tiedosto, joka on useita kertoja syötteidensä summaa suurempi. 300 sivun eräajossa kerroin ei ole pyöristysvirhe, vaan se on ero säilytysbudjettiin mahtuvan arkiston ja siihen mahtumattoman välillä

Tuo kerran, järjestä sivupuu sen jälkeen uudelleen

Ratkaisu on erottaa kaksi asiaa, jotka naiivi silmukka oli sulauttanut yhteen. Kopiointi päättää, mitkä objektit ovat olemassa kohteessa; järjestäminen päättää, missä sivut sijaitsevat sivupuussa. CollateDocumentsEx kopioi jokaisen lähteen tasan kerran, yhdellä CopyPagesFromDoc-kutsulla kyseisen lähteen koko alueella, joten jokainen lähde saa yhden tuontikartan ja jaetut resurssit kirjoitetaan kerran. Vasta kun jokainen lähde on saapunut, tapahtuu lomitus, ja se tapahtuu kokonaan TPDFPageTree.MovePage-metodin kautta

Sivujen siirrot ovat tässä merkittävässä mielessä ilmaisia. ISO 32000-1 §7.7.3 määrittelee sivupuun tasapainotettuna solmusanakirjojen rakenteena, jonka /Kids-taulukot sisältävät epäsuoria viittauksia ja jonka /Count kantaa lehtien kokonaismäärän jokaisessa solmussa. Sivun siirtäminen tarkoittaa yhden epäsuoran viittauksen poistamista yhdestä /Kids-taulukosta, sen lisäämistä toiseen, molempien /Count-arvojen säätämistä ja sivun /Parent-osoittimen uudelleenkohdistamista. Yhtään sisältövirtaa ei kosketa, yhtään resurssia ei kopioida, yhtään objektia ei luoda. Sivuobjekti säilyttää objektinumeronsa, minkä vuoksi objektinumerot pysyvät vakaina samalla tavalla kuin artikkelissa sivujen korvaaminen objektinumerot säilyttäen. Yksi lisäseikka menee naiivissa sivusiirrossa pieleen, mutta MovePage-metodissa ei. ISO 32000-1 §7.7.3.4 sallii /Resources-, /MediaBox-, /CropBox- ja /Rotate-arvojen periytyä esi-isäsolmusta sen sijaan, että ne olisi ilmoitettu sivulla. Sivu, joka perii resurssinsa solmusta A ja siirretään sitten solmun B alle, perii äänettömästi jotain eri, tai ei mitään. MovePage siis ratkaisee perityn arvon ja kirjoittaa sen sivusanakirjaan ennen siirtoa, jotta sivu kantaa omat attribuuttinsa siirron yli

Mitä uudelleenjärjestelyvaihe todellisuudessa tekee?

Se ajaa valintalajittelun lisää-kohtaan-semantiikkaa vastaan. Haluttu lohkosuhteellinen järjestys lasketaan ensin: käydään lähteet läpi kierrossa, otetaan enintään GroupSize indeksiä kustakin, ohitetaan loppuun käytetty lähde, toistetaan kunnes jokainen sivu on sijoitettu. Tämä tuottaa permutaation liitetyn lohkon yli. Sen soveltaminen on hankala osa, koska MovePage on lisäys, ei vaihto, joten jokainen siirto siirtää kaikkea vanhan ja uuden position välissä yhdellä

Toteutus pitää yllä Current-taulukkoa, joka mallintaa, missä jokainen liitetty sivu tällä hetkellä sijaitsee, skannaa eteenpäin positiosta K sivua, joka kuuluu kohtaan K, antaa siirtokäskyn, ja liu'uttaa sitten taulukon merkinnät heijastamaan sitä, mitä siirto teki puulle. Se on O(n toiseen) taulukko-operaatioissa ja nolla objektikopioissa, mikä on oikea kompromissi tälle työkuormalle: 500 sivun kokoaminen on neljännesmiljoona kokonaislukujen siirtelyä eikä yhtäkään tavua kopioitua kuvadataa. Laskevat alueet ja toistuvat sivut eivät tarvitse erityiskäsittelyä tässä vaiheessa, koska PLParsePageRangeList-funktiota kutsutaan lajittelu pois päältä ja kaksoiskappaleet sallittuina, joten pyydetty järjestys säilyy jäsentämisen läpi ehjänä

Käännetyt alueet ja yhden kutsun kaksipuolinen yhdistäminen

Kun käännös ilmaistaan alueena, tasoskannerin kaksinkertaisen ajon tapaus supistuu yhdeksi kutsuksi. Etusivut haluavat luonnollisen järjestyksensä ja takasivut haluavat 12-1:n, ja tyhjä ensimmäinen segmentti ennen puolipistettä kertoo, että ensimmäinen lähde antaa kaikki sivunsa

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;

Kaksi käyttäytymistä tuossa katkelmassa on syytä mainita nimenomaisesti. Kootut sivut liitetään valitun asiakirjan loppuun, joten NewDocument-funktiolla luotu asiakirja antaa alustavan tyhjän sivunsa ennen niitä, ja se kannattaa poistaa, jos sitä ei haluta. Ja lähteet voivat olla epätasaiset: kun GroupSize on 2 kolmisivuisen ja viisisivuisen lähteen yli, kierrokset tulevat A1 A2 B1 B2, sitten A3 B3 B4 kun A on lähes lopussa, sitten B5 yksinään, koska loppuun käytetty lähde vain ohitetaan sen sijaan, että sitä täydennettäisiin

Peruutus, lomakekentät ja se, mikä ei tule mukaan

Jokainen argumentti validoidaan ennen kuin kohdetta kosketaan. Puuttuva asiakirjakahva, valittu asiakirja lueteltuna omana lähteenään, GroupSize alle yhden, segmenttien määrä, joka ei täsmää lähteiden määrän kanssa, alue, joka nimeää sivun, jota lähteessä ei ole: kaikki nämä palauttavat 0 kohteen pysyessä muuttumattomana. Kopioinnin aikainen virhe on vaikeampi tapaus, ja se käsitellään julkisen DeletePages-metodin kautta eikä raa'an PageTree.DeletePages-metodin kautta. Syy on tarkka. Kopiointi ajetaan MergeFormData päällä, joten lähteen lomakekentät on jo liitetty kohteen /AcroForm /Fields-taulukkoon siihen mennessä, kun myöhempi lähde epäonnistuu. Sivujen poistaminen sivupuutasolla riisuisi widget-sivut pois ja jättäisi nuo kenttäviittaukset roikkumaan; julkinen reitti irrottaa kenttä-, ääriviiva- ja artikkelisäieviittaukset sivujen ohella

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

Ole rehellinen käyttäjillesi rajoista. Kokoaminen kantaa mukanaan sivut, niiden merkinnät ja lomakekentät, ja se yhdistää AcroForm-kenttäluettelon, laskentajärjestystaulukon ja oletusresurssisanakirjan. Se ei kanna mukanaan lähteen kirjanmerkkejä: skannatun etupinon ääriviivapuu on lähes aina tyhjä, joten kaksipuolisessa tapauksessa mitään ei menetetä, mutta jos kokoat kaksi kirjoitettua asiakirjaa, niiden ääriviivat jäävät taakse ja navigointi täytyy rakentaa itse uudelleen. Nimetyt kohteet, jotka olivat vain lähteen luettelossa, ovat samassa tilanteessa. Suunnittele tämä ennen kuin lupaat asiakkaalle häviöttömän kokoamisen

PDFlibPas toimittaa kokoamisfunktiot yhdessä muun sivukokoamispintansa kanssa, joten skanneri-työnkulku, aluepohjainen erotus ja suurten tiedostojen polut istuvat kaikki yhden komponentin takana Delphissä ja C++Builderissa. Täysi API-viite ja kokeiluversio löytyvät losLab Delphi PDF -kirjaston tuotesivulta