Teknik Makale

Delphi'de PDF sürüm preflight'ı: RL ve GEO kuralları

Bir mimari CAD paketinin dışa aktardığı kat planı, tipik olarak ilk sayfasında iki viewport taşıyan bir /VP dizisine sahiptir ve her viewport, bir görüntüleyicinin metreleri doğrudan çizimden okuyabilmesi için bir /Measure girdisi taşır. Bir GeoPDF harita yaprağı aynı /VP dizisine ve aynı /Measure anahtarına sahiptir. Kat planı sıradan bir PDF 1.6'dır. Harita yaprağı ise PDF 1.7 artı Adobe Extension Level 3 ister. Anahtar adında duran bir sürüm denetimi her ikisi için de eklentiyi ister ve kat planı, hiç taşımadığı jeo-uzaysal içerikten bahseden bir mesajla kaydetme anında reddedilir

Tek bir /Measure anahtarı, iki farklı sözlük

Anahtar adı, viewport'un ölçülebilir olduğunu söyler; nasıl ölçülebildiğini yalnızca ardındaki sözlük söyler. Ölçüm sözlükleri PDF 1.6'da geldi ve ISO 32000-1 §12.9 doğrusal (rectilinear) türü tanımlar: /R içinde bir ölçek oranı dizgesi, /X ve /Y içinde sayı biçimi dizileri, uzaklık (/D) ve alan (/A) için öteki diziler. Jeo-uzaysal ölçüm sonra geldi; PDF 1.7 üzerine Adobe Extension Level 3'ün parçası olarak. /Subtype /GEO ile işaretlenir ve /GCS içinde coğrafi bir koordinat sistemi, isteğe bağlı bir görüntüleme koordinat sistemi için /DCS, /GPTS ve /LPTS içinde eşleşen nokta dizileri ve tercih edilen görüntüleme birimleri için /PDU taşır — yapılar Delphi'de GeoPDF viewport'larını, GPTS ve LPTS dizilerini okumak başlığında geziliyor

İnsanları tökezleten ayrıntı varsayılandır. §12.9, /Subtype girdisine varsayılan RL değerini verir ve pek çok CAD dışa aktarıcı bunun avantajını kullanıp girdiyi yazmaz. Eksik bir alt türü "bilinmiyor, kötüsünü varsay" diye okuyan bir denetleyici, o çizimlerin hepsini jeo-uzaysal bir belgeye çevirir. /Measure anahtarı üzerinde eşleşen bir denetleyici ise alt tür sorusuna hiç varmaz; çünkü anahtarı taşıyan viewport sözlüğü, cevabı veren ölçüm sözlüğünün bir üst düzeyindedir

PDFlibPas'ta tek /Measure anahtarı, iki sözlük: /RL ile ya da alt tür atlanarak yazılmış doğrusal ölçüm yalnızca PDF 1.6 isterken jeo-uzaysal bir sözlük 1.7 ExtensionLevel 3 ister; böylece preflight, anahtar düzeyindeki bir kuralın ayırt edemediği yerde ölçüm sözlüğünün içeriğine bakarak karar verir
Geçerli bir dosyayı engelleyen bir preflight, yavaş olandan kötüdür; çağıran, belgenin hiç taşımadığı bir özellik hakkında yetkili bir tanı alır — bu yüzden ayırt edici denetim bir düzey aşağıda durur

PDF Library for Delphi kaydetme preflight'ı karara nasıl varıyor?

PDF Library for Delphi (PDFlibPas), hiçbir çıktı yazılmadan önce SaveToFile içinde bir sürüm preflight'ı koşar. Her dolaylı nesneyi ziyaret eder, iç içe sözlüklere ve dizilere iner ve her düğümü bir özellik kuralı tablosuna karşı sınar. Her kural bir minimum PDF sürümü ve eklenti özellikleri için bir Adobe eklenti düzeyi adlandırır; bir eklenti kuralı, yalnızca PDF 1.7 ile katalogdaki eşleşen bir /Extensions /ADBE girdisi birlikte olduğunda karşılanabilir. Karşılaştırdığı hedef, belge sürümüdür — ya da LockSaveVersion ile sabitlediğiniz sürüm — artı katalogun bildirdiği eklenti düzeyi. Hem eşleşen hem hedefi aşan ilk kural kaydı bitirir: SaveToFile 0 döndürür ve LastErrorCode, PDFLIB_ERROR_VERSION_COMPLIANCE sabitinin değeri olan 602 olur

PDFlibPas'ta kaydetme anı sürüm preflight'ı: SaveToFile her nesneyi sabit bir özellik kuralı tablosuna karşı denetler; her kural minimum bir PDF sürümü ve eklenti özellikleri için yalnızca eşleşen /Extensions girdisiyle PDF 1.7'nin karşılayabileceği bir Adobe eklenti düzeyi taşır ve hedefin karşılayamadığı ilk eşleşme, kaydı 602 hatasıyla durdurur
GetInformation'ın 311, 312 ve 313 anahtarları reddi bir tanıya çevirir; gereken sürümü, tetikleyen özelliği ve kilitli hedefi bildirir, böylece çağıran tahmin yürütmek yerine dosyayı ya da kuralı düzeltir

Tek bir hata kodu neyi düzelteceğinizi söylemez. Üç GetInformation anahtarı gerisini taşır: 311, suçlu özelliğin gerektirdiği sürümdür (örneğin 1.7 ExtensionLevel 3); 312, özelliğin adıdır; 313 ise save hedefinin kilitli olduğu sürümdür ve hiçbir şey kilitli değilken boştur

var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
      if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
        // 311: gereken sürüm, 312: tetikleyen özellik,
        // 313: save hedefinin kilitli olduğu sürüm (kilit yoksa '')
        Writeln('Needs ', Pdf.GetInformation(311),
          ' for ', Pdf.GetInformation(312),
          ', locked at [', Pdf.GetInformation(313), ']');
  finally
    Pdf.Free;
  end;
end;

Ölçüm içeriği için tabloda üç kural vardır ve çıplak /Measure anahtarı üzerinde bilinçli olarak hiçbir kural yoktur. /VP ya da /UserUnit taşıyan bir sayfa PDF 1.6 ister. Bir /PtData sözlüğü eklenti düzeyi 3 ister. Bir ölçüm sözlüğü, /Subtype /GEO dediğinde ya da /GCS, /DCS, /GPTS, /LPTS ya da /PDUdan herhangi birini taşıdığında eklenti düzeyi 3 ister. Böylece /Subtype girdisini atlayan bir viewport ölçüm sözlüğü ile /RLyi açıkça yazan aynı düzeyde 1.6'da geçer; aynı sayfa 1.5'teyken /VP nedeniyle reddedilir. Bir /GPTS dizisi ekleyin, cevap 1.7 ExtensionLevel 3 olur

İçerik testi bilinçli olarak muhafazakârdır. Başıboş bir /GCS ya da /PDU da taşıyan doğrusal bir sözlük jeo-uzaysal sayılır. O dosyayı geçirmektense reddetmeyi tercih ederiz: o anahtarlar doğrusal modelde hiçbir şey ifade etmez; yazan ya bir jeo-uzaysal araçtır ya da dosyayı bozmuş bir şeydir ve GEO farkındalıklı bir görüntüleyici onları yorumlamayı deneyebilir. Yükleyici öbür yönde de aynı derecede titizdir. Özellik taraması belgenin sürüm numarasını 1.7'ye çıkarabilir ama bir girdi dosyası adına asla /Extensions bildirimi yazmaz — eklenti bildirmek içerik hakkında bir iddiadır ve onu yalnızca sizin kodunuz ya da yazma çağrıları yapmalıdır

Kaydetmeden önce bir çizimin viewport'larını denetlemek

312 bir jeo-uzaysal özellik adlandırıyordu ve dosyanın düz bir çizim olduğundan eminseniz, ölçüm sözlüklerine kendiniz bakın. Herkese açık viewport çağrıları yeterlidir. GetMeasureDictCoordinateSystem /Subtype girdisini okur ve RL için 1, GEO için 2 döndürür; girdi yokken 0 döner — ki §12.9'a göre bu da doğrusal demektir. Nokta dizisi sayıları, jeo-referans verisinin bulunup bulunmadığını söyler

// Yüklü belgedeki her ölçüm sözlüğünü raporla.
// Buradaki çağrıların hepsi yalnızca okur; hiçbiri dosyaya anahtar eklemez.
procedure ReportMeasureDicts(Pdf: TPDFlib);
var
  Page, VP, VPID, MD, Kind: Integer;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.SelectPage(Page);
    for VP := 1 to Pdf.GetPageViewPortCount do
    begin
      VPID := Pdf.GetPageViewPortID(VP);
      MD := Pdf.GetViewPortMeasureDict(VPID);
      if MD = 0 then
        Continue;
      Kind := Pdf.GetMeasureDictCoordinateSystem(MD); // 0 = atlanmış (RL), 1 = RL, 2 = GEO
      Writeln(Format('page %d viewport "%s": subtype %d, GPTS %d, LPTS %d',
        [Page, string(Pdf.GetViewPortName(VPID)), Kind,
         Pdf.GetMeasureDictGPTSCount(MD), Pdf.GetMeasureDictLPTSCount(MD)]));
    end;
  end;
end;

O döngüde iki çağrı bilinçli olarak eksiktir: GetMeasureDictGCSDict ve GetMeasureDictDCSDict. O ikisi, yazma için kurulmuş al-ya-da-oluştur erişimcileridir. /GCS girdisi olmayan bir ölçüm sözlüğünde GetMeasureDictGCSDict boş bir tane ekler ve belge için eklenti düzeyi 3 bildirir. Doğrusal bir kat planında sonda gibi çağırın ve sözlük artık gerçekten /GCS içerir; 1.6'da kilitli bir sonraki kayıt, tanınızın yeni yazdığı içerik nedeniyle doğru biçimde reddedilir. Bir koordinat sisteminin var olup olmadığını bilmeniz gerekiyorsa alt tür ve nokta dizisi sayıları bunu yan etkisiz cevaplar

Çıktıyı PDF 1.6'da kilitlemek

Sabitlenmiş bir sürüm, tüketicilerinizin bir tavanı olduğunda çıktıyı dürüst tutar. TPDFlib.LockSaveVersion '1.0' ile '1.7' arasındaki dizeleri kabul eder ve başkaları için 0 döndürür. Belge sürümünü sabitler ve yazıcı tarafındaki çağrıların onu kendi başına yükseltmesini durdurur; ama kaydetme preflight'ı yine kilitli değere karşı koşar. 1.6'da kilitli bir CAD çizimi tertemiz kaydedilir. 1.6'da kilitli gerçek bir GeoPDF 602 alır ve istediğiniz cevap tam olarak budur; çünkü öteki seçenek, başlığı içeriğinin ihtiyacından az söz veren bir dosyadır

if Pdf.LockSaveVersion('1.6') <> 1 then
  raise Exception.Create('unsupported version string');
try
  if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
    if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
      // 1.6 üstü gerçek içerik, örneğin bir GEO ölçüm sözlüğü
      raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
        [string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
finally
  Pdf.UnlockSaveVersion;
end;

Sürüme dokunmadan önce 311 ve 312'yi okuyun. Özellik gerçek bir jeo-uzaysal içerikse iki dürüst seçeneğiniz vardır: belgeyi eklenti bildirimiyle 1.7'de kilitlenmemiş bırakmak ya da yalnızca 1.6 tüketenine göndereceğiniz kopyadan jeo-uzaysal veriyi düşürmek. Hatayı susturmak için kilidi yükseltmek kaçınılacak seçenektir; gerçek soruyu gizler — aşağı akıştaki okuyucu gönderdiğinizi kaldırabilir mi? Jeo-uzaysal içeriği API üzerinden kurarken SetMeasureDictCoordinateSystem ve SetMeasureDictGPTSCount gibi SetMeasureDict* yazıcıları eklenti düzeyi 3'ü kendileri bildirir; böylece yazılan dosyalar preflight'a çoktan etiketlenmiş olarak gelir. Sınırlı ve kanıta dayalı denetim alışkanlığı mühendislik belgeleri için PDF/E-1 yazar modu preflight'ı başlığında da görülür; orada CAD çizimleri bir sürüm numarasıyla değil bir uyumluluk standardıyla karşılaşır

Kural taraması tabloyu neden yerinde okuyor?

Preflight'ın maliyeti kabaca nesne düğümü çarpı kuraldır ve düğüm sayısı, her dizinin içindeki her sayıyı ve adı kapsar. Bu, kural başına ek yükün göründüğünden daha önemli olduğunu gösterir. Tablodaki her kural iki AnsiString alanlı bir kayıttır: eşleşme dizgesi ve özellik adı. Böyle bir kaydı yerel bir değişkene kopyalamak, Delphi ya da FPC'nin iki referans sayımını artırmasını ve yerelin önceden tuttuğunu salıvermesini sağlar. Kural başına düğüm başına bir kez yapıldığında, hedef sürüme uygulanmayan ve bir sonraki satırda çöpe giden kurallar için bile dizge defter tutma bedeli ödenmiş olur

PDFlibPas'ta kural taraması: dizge alanları taşıyan bir kural kaydını kopyalamak her ziyarette referans sayılarını oynatırken, salt okunur tablo girdisini const parametre olarak geçmek dizgelere dokunmadan yerinde okur
Salt okunur tabloyu yerinde okumak, kural eşleme yolundan referans sayımı işini çıkarır; tam bir kaydetme yine de özellik algılama, nesne çözme ve serileştirme için ödeme yapar

Kural tablosu birim başlatılırken bir kez doldurulur ve sonra hiç değiştirilmez; tarama, tablo girdilerini eşleme koduna const kayıt parametreleriyle verir ve onları yaşadıkları yerde okur. const bir kayıt parametresi referansla geçirilir; bir kuralı sınamak iki dizge alanına da dokunmaz ve referans sayıları eski halinde kalır. Etki kural eşleme yolunda yoğunlaşır. Gerçek bir kaydetme ayrıca nesneleri çözer, ertelenmiş özellik algılamayı koşar ve çıktıyı serileştirir; o aşamalar kuralların nasıl geçirildiğinden etkilenmez. Kural sırası, eşikler ve 311 ile 312'deki ilk başarısızlık tanısı buna bağlı değildir ve hiçbir kural sonucu kaydetmeler arasında önbelleklenmez; yani iki kez düzenleyip kaydettiğiniz bir belge her iki kez de eksiksiz denetlenir

Aynı desen, dizge, arayüz ya da dinamik dizi içeren kayıtlardan oluşan bir tabloyu gezen her döngüde kendi Delphi kodunuzda da denetlemeye değer:

type
  TMyRule = record
    Name: string;      // yönetilen alan: kopyalar referans sayımı işine mal olur
    Threshold: Integer;
  end;

// Okuyucunun kendi kodu, bir desen olarak gösteriliyor
function Applies(const Rule: TMyRule; Value: Integer): Boolean;
begin
  Result := Value >= Rule.Threshold;  // const: kopya yok, refcount trafiği yok
end;

// Sıcak döngüde tablo girdisini doğrudan geçirin;
// önce yerel bir kayda atamayın
for I := 0 to High(Rules) do
  if Applies(Rules[I], V) then
    Inc(Hits);

Ölçüm viewport'ları, jeo-uzaysal sözlükler, sürüm kilitleme ve kaydetme anı uyumluluk denetimi, Delphi, C++Builder ve Lazarus projeleri için sunulan PDF Library for Delphi içinde gelir