Techninis straipsnis

PDF rankinis redagavimas teksto redaktoriuje ir vėlesnis pataisymas

HotPDF gali įrašyti įkeltą dokumentą diagnostine forma, skirta žmogui skaityti ir redaguoti, o vėliau, jums atlikus pakeitimus, failą pataisyti. SaveLoadedDocumentDiagnostic įrašo dokumentą su determinuota žodynų raktų tvarka, kiekvienu objektu kaip įprastu netiesioginiu objektu, be objektų srautų, be kryžminių nuorodų srauto ir be linearizavimo. RepairDiagnosticDocument tada atkuria baitų poslinkius, srautų ilgius, kryžminių nuorodų lentelę ir trailer, kuriuos jūsų atliktas redagavimas sugadino

Ši pora egzistuoja dėl vieno darbo eigos tikslo: suprasti arba pataisyti tai, kas iš tikrųjų yra PDF viduje. Kiekvienas programuotojas, taisęs atvaizdavimo problemą, kada nors norėjo atverti failą, pakeisti vieną skaičių viename žodyne ir pažiūrėti į rezultatą. Įprastame PDF faile tai neįmanoma, nes turinys suspaustas į objektų srautus, o kiekvienas įterptas baitas pastumia poslinkius, kuriuos kryžminių nuorodų lentelė fiksuoja tiksliai

Kuo skiriasi diagnostinis įrašymas?

Kartu keičiasi šeši nustatymai. Žodynų raktai įrašomi determinuota tvarka, todėl dviejų to paties dokumento įrašymų failai yra palyginami. Diagnostinis išvesties režimas anotuoja struktūrą. Priverstinai atliekamas pilnas perrašymas, todėl niekas nepaliekama kaip nepaliesta prieauginė (incremental) redakcija. Linearizavimas išjungtas, nes linearizuotas failas skirtas žiniatinklio žiūryklei, o ne skaitytojui. Kryžminių nuorodų srautai išjungti, todėl lentelė yra paprastas tekstas. Ir objektų srautai išjungti, todėl joks objektas nėra paslėptas suspaustame konteineryje

Rezultatas yra failas, kurio antraštėje šalia įprasto %PDF- parašo yra diagnostinis žymeklis, o jo turinį galima peržiūrėti bet kuriame redaktoriuje. Objektai išdėstyti vienas po kito, žodynai yra įskaitomi, o turinio srautai yra ten, kur juos galima rasti. Pataisymo įrankis tikrina šį žymeklį ir atmeta failus, kuriuose jo nėra, nes įprasto PDF kompaktiškos struktūros baitų diapazonų pataisymo įrankis interpretuoti neturėtų bandyti

Diagrama, lyginanti įprastą gamybinį PDF įrašymą su HotPDF diagnostiniu įrašymu, kuris sukuria paprastus, įskaitomus objektus ir paprasto teksto xref lentelę, atveriamą bet kuriame teksto redaktoriuje
Įprastas įrašymas paslepia objektus suspaustuose srautuose, o diagnostinis rašytojas kiekvieną objektą išdėsto atskirame bloke su paprasto teksto kryžminių nuorodų lentele — rezultatas yra failas, kurį galima peržiūrėti ir keisti bet kuriame redaktoriuje
uses
  HPDFDoc;

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice.pdf') <= 0 then
      Exit;
    // Įskaitoma, po vieną objektą bloke, niekas nepaslėpta objektų srautuose
    Pdf.SaveLoadedDocumentDiagnostic('invoice-diagnostic.pdf');
  finally
    Pdf.Free;
  end;
end;

Kodėl redagavimas sugadina failą ir ką reikia atkurti?

PDF faile trys dalykai koduoja baitų pozicijas arba baitų kiekius. Kryžminių nuorodų lentelė kiekvieną objekto numerį susieja su baitų poslinkiu. startxref reikšmė failo pabaigoje nurodo į šią lentelę. Ir kiekvienas srauto žodynas turi /Length įrašą, nurodantį, kiek baitų užima srauto turinys

Pakeitus vieną simbolį bet kur prieš failo pabaigą, po jo einantys poslinkiai tampa klaidingi. Pridėjus eilutę į turinio srautą, jo /Length irgi tampa klaidingas. Dauguma žiūryklių tokį failą atmes, o tos, kurios jį atkuria, tai daro skenuodamos, o tai klaidą tik paslepia, o ne pataiso. Pataisymas visus tris dalykus atkuria iš to, ką baitai iš tikrųjų dabar rodo

Diagrama, rodanti, kaip rankinis redagavimas pastumia baitų poslinkius, todėl kryžminių nuorodų lentelė, startxref rodyklė ir srautų ilgiai pasensta, kol RepairDiagnosticDocument visus tris atkuria iš faktinio išdėstymo
Pridėjus baitų, pasistumia visi po jų einantys poslinkiai, todėl užfiksuotos pozicijos, startxref rodyklė ir deklaruoti srautų ilgiai nebeatitinka realybės — pataisymas kiekvieną iš jų atkuria iš to, ką redaguotas failas dabar iš tikrųjų turi
var
  Options: THPDFDiagnosticRepairOptions;
  Report: THPDFDiagnosticRepairReport;
begin
  Options := THPDFDiagnosticRepairOptions.Default;
  Options.VerifyOutput := True;         // prieš pakeičiant įkelti rezultatą ir jį patikrinti

  if THotPDF.RepairDiagnosticDocument('invoice-diagnostic.pdf',
       'invoice-fixed.pdf', Options, Report) then
    Writeln(Format('%d objects, %d streams, %d lengths corrected, ' +
      '%d xref rows rebuilt, %d pages verified',
      [Report.ObjectCount, Report.StreamCount,
       Report.RepairedStreamLengthCount, Report.RebuiltXRefEntryCount,
       Report.VerifiedPageCount]))
  else
    Writeln(Format('repair failed (%d): %s',
      [Ord(Report.Status), string(Report.Diagnostic)]));
end;

Kopijuoti baitus, o ne perserializuoti

Projektinis sprendimas, dėl kurio šiuo įrankiu galima pasitikėti, yra tas, kad jis niekada neįkelia dokumento ir neišveda jo iš naujo. Jis kopijuoja patvirtintų objektų baitų diapazonus, keičia tik tas /Length reikšmes, kurios yra klaidingos, ir aplink jas atkuria lentelę, trailer ir startxref

Įkėlimą ir pakartotinį serializavimą būtų kur kas lengviau įgyvendinti, tačiau tai tyliai keistų dalykus. Nežinoma objekto sintaksė būtų normalizuota, eilučių escape simboliai perrašyti pageidaujama rašytojo forma, o jau užkoduoti srautų turiniai galėtų būti perkoduoti. Derinimo įrankiui tai nepriimtina: jūs pakeitėte vieną skaičių, o diff turėtų rodyti vieną skaičių, o ne tūkstantį atsitiktinių formatavimo pakeitimų. Jei norite tiksliai žinoti, kas pasikeitė tarp dviejų failų, tai palyginimo uždavinys, aprašytas straipsnyje dviejų PDF failų palyginimas pagal struktūrą ir pikselius

Diagrama, lyginanti HotPDF pataisymą, pažodžiui kopijuojantį patvirtintus baitų diapazonus, su įkėlimo ir perserializavimo keliu, kuris normalizuoja sintaksę, perrašo eilučių escape simbolius ir perkoduoja srautų turinius
Pataisymas kopijuoja patvirtintus diapazonus baitas po baito ir pataiso tik klaidingas /Length reikšmes, todėl jūsų rankomis sukurtas diff išlieka nepaliestas — pakartotinis įkėlimas ir serializavimas tyliai perrašytų sintaksę, escape simbolius ir kodavimus, kurių jūs niekada nelietėte

Dvi analizės taisyklės, saugančios nuo klaidingų objektų

Baitų diapazonų skeneris susiduria su akivaizdžia spąstų problema: tekstas, panašus į objekto antraštę, gali pasitaikyti srauto turinyje arba eilutėje. Naivus skenavimas sukurtų kryžminių nuorodų eilutę, rodančią į turinio srauto vidurį, ir gautas failas būtų sugadintas nauju ir dar labiau klaidinančiu būdu

HotPDF to išvengia praleisdama visą kiekvieno patvirtinto objekto baitų diapazoną, naudodama jo užfiksuotą pabaigos poslinkį, todėl niekas objekto viduje niekada nelaikoma kandidatu. Tarp objektų leidžiami tik PDF tarpo simboliai ir komentarai; bet kas kita reiškia, kad sritis nesuprasta, ir jos baitai neturi patekti į išvestį

Srautų ilgiams reikia lygiai tiek pat dėmesio. Tikrasis turinio ilgis neapima eilutės pabaigos skirtuko, kurį rašytojas įterpia prieš endstream. Kai deklaruotas ilgis tiksliai pataiko į žymeklį, ta deklaracija priimama. Kai nepataiko, pataisymo įrankis atlieka ribotą endstream ir endobj poros paiešką ir atima lygiai vieną skirtuką — CR, LF arba CRLF. Suklydus čia vienu baitu, gaunamas failas, kuris atsiveria ir atvaizduoja puslapį su vienu paklydusiu operatoriumi turinio srauto pabaigoje

Ką trailer leidžiama išsaugoti

Atkurtas trailer nėra seno kopija. /Size, /Prev ir /XRefStm visi aprašo ankstesnį failo išdėstymą ir yra perskaičiuojami arba pašalinami. Išlieka tik tie įrašai, kurių taikiniai buvo patvirtinti: /Info, /ID ir /Encrypt. O /Root turi rodyti lygiai į vieną Catalog objektą — ne nulį ir ne kelis, o būtent tai dažnai gaunasi faile, sukurtame kopijuojant ir įterpiant objektų blokus

Išvesties saugumas tvarkomas taip, kaip turėtų būti įrankyje, dirbančiame su failais, kurie žmonėms svarbūs. Rezultatas įrašomas į laikiną failą tikslinio katalogo viduje, tada patikrinamas jį įkeliant įprastu keliu su išjungtu kryžminių nuorodų atkūrimu, todėl failas, kuris atsiveria tik atkūrimo dėka, laikomas nesėkme. Tik po to, kai puslapių medis patvirtinamas, laikinas failas atomiškai pakeičia tikslinį. Nepavykęs pataisymas niekada neperrašo esamos išvesties

Biudžetai ir sąžiningi nesėkmės kodai

Trys ribos apibrėžia darbą: įvesties baitai, kandidatų objektų skaičius ir didžiausias objekto numeris, kurių numatytosios reikšmės yra 8 GiB, du milijonai ir du milijonai. Objekto numerio riba svarbesnė, nei atrodo, nes reta lentelė su keliais objektais, sunumeruotais milijardų diapazone, kitaip milžiniškai išpūstų atkurtą kryžminių nuorodų lentelę. Klasikinės kryžminių nuorodų lentelės taip pat atmeta poslinkius, kuriems reikėtų daugiau nei dešimties dešimtainių skaitmenų — tai formato riba, o ne įgyvendinimo pasirinkimas

Nesėkmės praneša konkrečią būseną, o ne tiesiog loginę reikšmę: neteisingi argumentai, per didelis šaltinis, ne diagnostinis failas, viršyta objektų riba, neteisingas objektas, dubliuotas objektas, trūkstamas katalogas, neteisingas trailer, išvesties klaida arba nepavykęs patvirtinimas. Kiekviena atitinka skirtingą taisymą. Dubliuotas objektas reiškia, kad objekto bloką įklijavote du kartus. Trūkstamas katalogas dažniausiai reiškia, kad objektas, laikantis /Type /Catalog, buvo pažeistas. Nepavykęs patvirtinimas reiškia, kad struktūra dabar nuosekli, bet puslapių medis neįsikelia, o tai paprastai yra /Kids masyvas, redaguotas nesutampant su savo /Count

Dvi praktinės pastabos užbaigia šį ciklą. Saugokite ne tik pataisytą, bet ir diagnostinį failą, kad kitas redagavimas prasidėtų nuo įskaitomo pagrindo. Ir netiekite diagnostinės išvesties galutiniams vartotojams: ji didesnė už įprastą įrašymą, nes niekas nesuspausta į objektų srautus, o kompaktiško failo gamybinis kelias yra įprastas įrašymo kelias, aprašytas straipsnyje objektų srautai ir prieaugio atnaujinimai. Kai tikslas yra rasti, kas negerai, o ne tai pakeisti, automatizuotas kelias, aprašytas straipsnyje preflight ataskaitos automatizavimas, atsako greičiau nei baitų skaitymas

Diagnostinis įrašymas, pataisymas, preflight ir palyginimas veikia su tuo pačiu įkelto dokumento modeliu Delphi ir C++Builder aplinkose; pilnas funkcijų sąrašas pateiktas HotPDF Delphi PDF komponento puslapyje