Teknisk artikel

Kollationera duplexskanningar i Delphi: PDF-interleave-sammanslagning

CollateDocumentsEx i PDFlibPas-biblioteket för Delphi slår samman flera öppna dokument till ett interfolierat dokument. Funktionen lägger till GroupSize sidor från varje källa per omgång, tar emot en sidintervallslista per källa, och behandlar ett fallande intervall som 3-1 som en omvändning av den källan. Ett enda anrop förvandlar en framsidesstapel och en omvänd baksidesstapel till läsordning

Scenariot bakom detta API är vardagligt och extremt vanligt. En arkmatad skanner med enkelsidig bana kör hela stapeln med framsidan nedåt, sedan vänder operatören stapeln och kör den igen. Resultatet blir två PDF-filer: framsidor i ordning, baksidor i omvänd ordning. Vad användaren vill ha är en enda fil, sida 1 fram, sida 1 bak, sida 2 fram, och så vidare. Den här artikeln handlar om ordningsproblemet och resursduplicerings-fällan som ligger under det. Om din fråga i stället gäller ren sammanslagningsgenomströmning, se snabb PDF-sammanslagning genom byte-nivå referensförskjutning; om indata är för stora för att rymmas i minnet alls, se sammanslagning och uppdelning av gigabyte-stora PDF-filer med direktåtkomst

Skannern producerar två staplar, en av dem baklänges

Kollationering är inte sammanslagning. En sammanslagning konkatenerar sidintervall; en kollationering interfolierar dem, och interfolieringsmönstret är en egenskap hos den fysiska enhet som producerade indata. Får du mönstret fel är filen inte lite fel, den är oläsbar: varannan sida hör till ett annat ark. Tre variabler beskriver nästan varje verkligt fall: hur många källor som ingår i rotationen, hur många sidor som kommer från varje källa per omgång, och om någon källa behöver läsas baklänges. CollateDocuments täcker de två första med en enkel array av dokumenthandtag och ett GroupSize-heltal. CollateDocumentsEx lägger till det tredje genom att ta emot en semikolonseparerad lista med sidintervall, ett segment per källa, där ett tomt segment betyder alla sidor i den källan och ett fallande intervall omvänder den. Båda funktionerna lägger till i slutet av det aktuellt valda dokumentet och returnerar 1 vid lyckat resultat, 0 vid varje avvisning

Varför multiplicerar den naiva kollationeringen filstorleken?

Eftersom importkartan som mappar källobjektnummer till målobjektnummer byggs om vid varje kopieringsanrop, och allt som kan nås från mer än en bit importeras en gång per bit. Inuti PDFlibPas återställer TPDFDocument.CopyPagesFromDoc sin NewIndObjList i toppen av varje anrop. Den listan är det enda minne kopieraren har av vad den redan fört över. Anropa den en gång med ett tiosidigt intervall och ett typsnitt som delas av alla tio sidorna bäddas in en gång. Anropa den tio gånger med en sida i taget och samma typsnitt bäddas in tio gånger. Det spelar mycket större roll för skanningar än för textdokument, eftersom en skannad sida är ett enda stort bild-XObject och de delade objekten är de med verklig vikt: en inbäddad ICC-profil, en delad /DecodeParms-kedja, ett stämpel- eller vattenmärke-form-XObject som appliceras på varje ark, OCR-textlagrets typsnitt. Det uppenbara sättet att skriva en round-robin-kollationering är en loop över omgångar, och den loopen är precis det patologiska fallet

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

Tolv omgångar, två källor, tjugofyra importkartor. Ingenting varnar dig. Sidordningen är korrekt, varje sida renderas, och det enda symptomet är en fil flera gånger större än summan av sina indata. På ett jobb med 300 sidor är multiplikatorn inte ett avrundningsfel, den är skillnaden mellan ett arkiv som ryms inom lagringsbudgeten och ett som inte gör det

Importera en gång, ordna sedan om sidträdet

Lösningen är att separera de två angelägenheter som den naiva loopen hade smält samman. Kopiering avgör vilka objekt som finns i målet; ordning avgör var sidorna sitter i sidträdet. CollateDocumentsEx kopierar varje källa exakt en gång, i ett enda CopyPagesFromDoc-anrop med den källans fullständiga intervall, så varje källa får en importkarta och delade resurser skrivs en gång. Först efter att varje källa har landat sker interfolieringen, och den sker helt genom TPDFPageTree.MovePage

Sidflyttar är gratis i den mening som spelar roll här. ISO 32000-1 §7.7.3 definierar sidträdet som en balanserad struktur av nodordböcker vars /Kids-arrayer innehåller indirekta referenser, med /Count som bär bladtotalen vid varje nod. Att flytta en sida innebär att ta bort en indirekt referens från en /Kids-array, infoga den i en annan, justera båda /Count-värdena, och peka om sidans /Parent. Ingen innehållsström rörs, ingen resurs dupliceras, inget objekt skapas. Sidobjektet behåller sitt objektnummer, vilket också är anledningen till att objektnumren förblir stabila på samma sätt som i sidutbyte som bevarar objektnummer. Det finns en detalj till som en naiv sidflytt gör fel och som MovePage inte gör. ISO 32000-1 §7.7.3.4 låter /Resources, /MediaBox, /CropBox och /Rotate ärvas från en förfadernod i stället för att anges på sidan. En sida som ärver sina resurser från nod A och sedan flyttas under nod B ärver tyst något annat, eller ingenting alls. MovePage löser därför upp det ärvda värdet och skriver det på sidordboken innan flytten sker, så att sidan bär sina egna attribut med sig genom flytten

Vad gör omordningspasset egentligen?

Det kör en urvalssortering mot infoga-vid-semantik. Den önskade blockrelativa ordningen beräknas först: gå igenom källorna i rotation, ta upp till GroupSize index från varje, hoppa över en källa som är uttömd, upprepa tills varje sida är placerad. Det ger en permutation över det tillagda blocket. Att tillämpa den är den besvärliga delen, eftersom MovePage är en infogning, inte en swap, så varje flytt förskjuter allt mellan den gamla och den nya positionen med ett steg

Implementationen håller en Current-array som modellerar var varje tillagd sida för närvarande sitter, söker framåt från position K efter sidan som hör hemma på K, utför flytten, och glider sedan array-posterna för att spegla vad flytten gjorde med trädet. Det är O(n kvadrat) i array-operationer och noll i objektkopior, vilket är rätt avvägning för denna arbetsbelastning: en kollationering på 500 sidor är en kvarts miljon heltalsomflyttningar och inte en byte duplicerad bilddata. Fallande intervall och upprepade sidor behöver ingen särskild hantering i detta pass eftersom PLParsePageRangeList anropas med sortering inaktiverad och dubbletter tillåtna, så den begärda ordningen överlever parsningen intakt

Omvända intervall och engångsanropet för duplexsammanslagning

Med omvändning uttryckt som ett intervall kollapsar det plana dubbelpass-fallet till ett enda anrop. Framsidorna vill ha sin naturliga ordning och baksidorna vill ha 12-1, och det tomma första segmentet före semikolonet säger att den första källan bidrar med alla sina sidor

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;

Två beteenden i det utdraget är värda att uttrycka explicit. De kollationerade sidorna läggs till i det valda dokumentet, så ett dokument skapat med NewDocument bidrar med sin ursprungliga tomma sida före dem, och du bör ta bort den om du inte vill ha den. Och källorna kan vara ojämna: med GroupSize 2 över en tresidig och en femsidig källa kommer omgångarna ut som A1 A2 B1 B2, sedan A3 B3 B4 när A nästan är slut, sedan B5 ensam, eftersom en uttömd källa helt enkelt hoppas över i stället för att fyllas ut

Återställning, formulärfält, och vad som inte följer med

Varje argument valideras innan målet rörs. Ett saknat dokumenthandtag, det valda dokumentet listat som sin egen källa, ett GroupSize under ett, ett segmentantal som inte matchar källantalet, ett intervall som namnger en sida källan inte har: alla dessa returnerar 0 med målet oförändrat. Fel under kopiering är det svårare fallet, och det hanteras genom det publika DeletePages snarare än det råa PageTree.DeletePages. Anledningen är specifik. Kopieringen körs med MergeFormData aktiverat, så källformulärfälten har redan lagts till i målets /AcroForm /Fields-array vid den tidpunkt en senare källa misslyckas. Att ta bort sidorna på sidträdsnivå skulle strippa widget-sidorna och lämna de fältreferenserna hängande; den publika vägen kopplar bort fält-, disposition- och artikeltrådsreferenserna tillsammans med sidorna

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

Var ärlig med dina användare om gränserna. Kollationeringen för med sig sidor, deras annoteringar och deras formulärfält, och den slår samman AcroForm-fältlistan, beräkningsordningsarrayen och standardresursordboken. Den för inte med sig källdispositioner (bookmarks): dispositionsträdet för en skannad framsidesstapel är nästan alltid tomt, så inget går förlorat i duplexfallet, men om du kollationerar två redigerade dokument stannar deras dispositioner kvar och du bygger om navigationen själv. Namngivna destinationer som bara fanns i källkatalogen är i samma situation. Planera för det innan du lovar en kund en förlustfri kollationering

PDFlibPas levererar kollationeringsfunktionerna tillsammans med resten av sin sidmonteringsyta, så skannerarbetsflödet, den intervallbaserade extraktionen och de stora filvägarna sitter alla bakom en komponent i Delphi och C++Builder. Den fullständiga API-referensen och en provbygge finns på produktsidan losLab Delphi PDF-bibliotek