Teknik Makale

Excel Geçerli XLSX'i Neden Onarıyor: OPC Paket Kuralları

LibreOffice'in ve her el yapımı okuyucunun sorunsuz açtığı bir XLSX dosyasında Excel "We found a problem with some content" iletisini gösterir; çünkü Excel, o okuyucuların görmezden geldiği iki şeyi zorunlu tutar: şemanın zorunlu kıldığı öznitelikler ve Open Packaging Conventions'ın tekillik kuralları. Üretilen parçalarla korunan parçaları birleştiren her yazıcının kolayca çiğneyebileceği üç kural şudur: bir <phoneticPr> öğesinin fontId değeri olmak zorundadır, her parça adı [Content_Types].xml içinde tam olarak bir Override alır ve iki kök relationship asla rId4 gibi bir tanımlayıcıyı paylaşamaz. Bu yazı her birini ve HotXLS'in — Delphi ve C++Builder için yerel Excel tablo bileşeni — paketi o kuralı sağlayacak biçimde nasıl yazdığını ele alır

Excel neden her okuyucunun kabul ettiği bir paketi reddediyor?

Çünkü onarım istemi bir parser hatası değil, şema ve paket doğrulayıcısıdır. Bir workbook kendi kütüphanenizden, LibreOffice'ten ve XML doğrulayıcılarından geçip round-trip yapabilir ve XLSX OPC relationship çözümlemesi makalesinde kullanılan OPC anlamında yapısal olarak sağlam olabilir: her parçaya ulaşılabilir, her hedef çözümlenebilir. DisplayAlerts kapalı izole bir COM örneğinde Workbooks.Open ile açın; çağrı yine de doğrudan başarısız olabilir. Etkileşimli olarak aynı dosya, onarım teklif eden bilinen iletiyi çıkarır; Excel'in yazmaya zahmet ettiği onarım günlüğü ise parçayı adlandırır ama kuralı söylemez. Bu tek istemin arkasına birkaç bağımsız ihlal saklanabilir ve Excel onları teker teker raporlamaz; workbook'u reddeder ve onları bulup analiz etmeyi size bırakır. Aşağıda her kuralı, onu ihlal etmenin doğal yolunu ve HotXLS'in paketi yerine nasıl yazdığını bulacaksınız; çünkü bunların her biri herhangi bir Delphi XLSX yazıcısının takılabileceği türden kurallar

Kural 1: phoneticPr fontId zorunlu, sıfırken de

<phoneticPr> öğesi, ECMA-376 Part 1 §18.4.3'te use="required" olarak bildirilmiş bir fontId özniteliği taşır ve 0 değeri yokluk değil, geçerli bir font indeksidir. Sıfırı ayarlanmamış sayıp özniteliği yalnızca Sheet.PhoneticFontId > 0 olduğunda yazan bir yazıcı, tam sayı alanlarının varsayılanı sıfır olduğu için doğal bir Delphi refleksi izler; ama fonetik fontu styles.xml içindeki ilk font olan her workbook için <phoneticPr type="noConversion"/> üretir ve buna takılmak kolaydır, çünkü indeks 0 zaten ilk fonttur. Excel böylece kendi yazdığı bir değeri geri alırken reddeder

Excel'in bir worksheet parçası için neden onarım istediği: phoneticPr öğesi fontId değerini ECMA-376 Part 1 içinde use required olarak bildiriyor, 0 font indeksi geçerli bir değer ve PhoneticFontId sıfırken özniteliği atan bir yazıcı phoneticPr type noConversion üretir; şema type ve alignment için varsayılan verirken fontId için hiç vermiyor
Bir özniteliği varsayılana eşit olduğunda atlamak yalnızca şema o varsayılanı bildirdiğinde güvenlidir; fonetik fontunu styles.xml içindeki ilk girdi olarak taşıyan bir şablon tam da bu duruma takılır
// Doğal refleks: sıfır ayarlanmamış sayılır, fontId düşer
phoneticXml:= '<phoneticPr';
if Sheet.PhoneticFontId > 0 then
  phoneticXml:= phoneticXml+ ' fontId="'+ IntToStr(Sheet.PhoneticFontId)+ '"';

// lxHandleX.pas, worksheet yazıcısı — öznitelik zorunlu, sıfır da dahil
phoneticXml:= '<phoneticPr fontId="'+ IntToStr(Sheet.PhoneticFontId)+ '"';
phoneticXml:= phoneticXml+ ' type="'+ XlsxEscapeAttr(Sheet.PhoneticType)+ '"';
if Sheet.PhoneticAlignment<> '' then
  phoneticXml:= phoneticXml+ ' alignment="'+ XlsxEscapeAttr(Sheet.PhoneticAlignment)+ '"';

HotXLS öğeyi yalnızca TXLSXWorksheet.PhoneticType boş değilse yazar, yani hiç fonetik ayar taşımayan workbook'lar etkilenmez. Yeni bir sayfada PhoneticFontId değerini sıfıra ayarlayıp kaydedin; xl/worksheets/sheet1.xml dosyası <phoneticPr fontId="0" içerir. Daha geniş ders şu: varsayılansa yazma yaklaşımı yalnızca şema bir varsayılan bildirdiğinde güvenlidir; o öğede type ve alignment varsayılana sahiptir, fontId ise sahip değildir

Kural 2: [Content_Types].xml içinde parça adı başına tek Override

İçerik tipi akışı her parça adını en fazla bir kez bildirebilir ve Excel, aynı PartName için ikinci bir Override girdisini, iki girdi aynı ContentType değerini taşısa bile bozulma sayar. HotXLS'te o akışı besleyen iki yazıcı var. BuildContentTypesXml, nesne modelinin ürettiği her parçayı bildirir: workbook, styles, shared strings, theme, worksheet'ler ve TXLSXWorkbook.CustomProperties.Count > 0 olduğunda /docProps/custom.xml. PreserveUnsupportedParts açıkken TXLSXOpaquePackage, kaynak paketten birebir yakaladığı her parça için bir Override ekler; böylece o baytlar çıkışta da bildirilmiş kalır. Çakışma, her iki tarafta da bulunan bir parçadan doğar. Özel belge özellikleri modele ayrıştırılır ama kaynak paketin docProps/custom.xml dosyası da opak biçimde yakalanır; naif bir birleştirme onu iki kez bildirir ve model, opak katmanın da koruduğu bir parçayı yeniden ürettiğinde chart ve pivot cache parçaları da aynı noktaya düşebilir. Modelin neleri yazdığını görmeyen bir opak yazıcı bilemez; ContentTypeOverridesXml işlevine o görünümün verilmesi nedeni budur

İki yazıcının [Content_Types].xml içinde nasıl çakışabileceği: BuildContentTypesXml docProps/custom.xml dosyasını nesne modelinden bildirirken TXLSXOpaquePackage birebir yakalanan aynı parça için bir Override ekleyebilir; HotXLS opak katmanı bu yüzden önce üretilen akışı ayrıştırır, adları OpcLowerPartName ile normalleştirir ve her çakışmayı modelin kazanmasına izin verir
Her yazıcı kendi içinde tutarlı olabilir; her parça adının bir kez görünmesi kısıtı ise yalnızca çıktılarının birleştirildiği dikişte var olduğu için HotXLS model akışını içeri geçirir
<!-- Naif bir birleştirmenin ürettiği -->
<Override PartName="/docProps/custom.xml"
    ContentType="application/vnd.openxmlformats-officedocument.custom-properties+xml"/>
...
<Override PartName="/docProps/custom.xml"
    ContentType="application/vnd.openxmlformats-officedocument.custom-properties+xml"/>

HotXLS, üretilen XML'i ContentTypeOverridesXml işlevine geçirir ve opak yazıcının bir şey yazmadan önce onu ayrıştırmasını sağlar. Doğruluğu iki ayrıntı taşır. OpcLowerPartName karşılaştırmadan önce adları küçük harfe çevirir, ters bölüleri düz bölüye döndürür ve baştaki bölüleri kırpar; çünkü OPC parça adları büyük-küçük harf duyarsız karşılaştırılır, model onları baştaki bölüyle yazar, opak katman ise ZIP öğe adlarını bölüsüz saklar. Bir de BuildContentTypesXml içindeki çağıran, Result + '</Types>' geçirerek kısmen kurulmuş belgeyi kapatır, böylece TXMLReader kesilmiş bir akış yerine well-formed girdi görür. Buradan çıkan kural, modelin önde olduğu ilk-gelen-kazanır kuralıdır: nesne modelinin bildirdiği her şey otoritedir, opak replay yalnızca boşlukları doldurur

// lxOpcPackage.pas — TXLSXOpaquePackage.ContentTypeOverridesXml
function TXLSXOpaquePackage.ContentTypeOverridesXml(const ExistingXml: WideString): WideString;
begin
  ...
  UsedNames.Sorted:= True;
  UsedNames.Duplicates:= dupIgnore;
  if ExistingXml<> '' then
    // Modelin ürettiği akışı ayrıştır ve bildirilen her PartName değerini topla.
    while Reader.Read do
      if (Reader.NodeType= xmlntElement)and (Reader.Name= 'Override') then
      begin
        Index:= Reader.AttributeIndex('PartName');
        if Index>= 0 then
          UsedNames.Add(String(OpcLowerPartName(Reader.Attribute[Index].Value)));
      end;
  for i:= 0 to FParts.Count- 1 do
  begin
    Part:= TXLSXOpaquePart(FParts[i]);
    if (Part.ContentType= '')or (LowerCase(ExtractFileExt(String(Part.PartName)))= '.rels')or
      (UsedNames.IndexOf(String(OpcLowerPartName(Part.PartName)))>= 0) then
      Continue;                       // zaten bildirilmiş ya da bir rels parçası
    UsedNames.Add(String(OpcLowerPartName(Part.PartName)));
    Result:= Result+ '<Override PartName="/'+ OpcXmlEscapeAttribute(Part.PartName)+
      '" ContentType="'+ OpcXmlEscapeAttribute(Part.ContentType)+ '"/>';
  end;
end;

Kural 3: relationship Id değerleri bir relationships parçası içinde tekil olmalı

Bir .rels parçasındaki her Relationship öğesi o parça içinde tekil bir Id taşımak zorundadır ve iki tanesi bir Id paylaşırsa Excel paketi reddeder. HotXLS paket düzeyindeki _rels/.rels dosyasını sabit tanımlayıcılarla yazar: workbook için rId1, core ve extended belge özellikleri için rId2 ve rId3, modelde varsa özel özellikler için rId4. Opak paket ise kaynaktan koruduğu kök relationship'leri ekler ve UsedIds listesinde zaten bulunan tanımlayıcıları yeniden numaralandırır. O liste yalnızca rId1 ile rId3 arasını biliyorsa ve modelin kendi özel özellik relationship'ini yazmak üzere olduğunu bilmiyorsa, özel özellik relationship'i de rId4 olan — Excel'in varsayılan olarak yazdığı budur — bir kaynak paket, aynı hedefe işaret eden iki rId4 girdisiyle çıkar. Bu yüzden RootRelationshipsXml, model özel özellikler yazdığında rId4 değerini rezerve eder ve kaynak kopyayı atlar. Çağıran BuildRootRelsXml ikinci argüman olarak Workbook.FCustomProps.Count > 0 geçirir; yani hem rezervasyon hem de atlama, modelin rId4 yazıp yazmayacağına karar veren aynı koşulla yürür. Paket kökünde yeniden numaralandırma güvenlidir, çünkü workbook içinde hiçbir şey kök relationship tanımlayıcılarına adla başvurmaz; aynı numara bir kat aşağıda yanlış olurdu, orada workbook.xml içindeki r:id öznitelikleri workbook relationships parçasındaki tanımlayıcılara bağlanır; MergeWorkbookRelationshipsXmlin ayrı bir tanımlayıcı haritası tutmasının nedeni de budur

HotXLS paketinin kökündeki relationship tanımlayıcı çakışması: model rId1 ile rId4 arasını yazar ve rId4 özel özellikler için ayrılmıştır, opak katman ise kaynaktan rId4 olarak gelen bir relationship'i yeniden oynattı çünkü UsedIds yalnızca rId1 ile rId3 arasını biliyordu; düzeltme EmitCustomProps geçerli olduğunda rId4 değerini baştan rezerve edip kalanı yeniden numaralandırıyor
Yeniden numaralandırma paket kökünde güvenlidir çünkü workbook içinde hiçbir şey kök tanımlayıcılara adla başvurmaz; aynı numara bir kat aşağıda workbook.xml içindeki her r:id bağını bozardı
// lxOpcPackage.pas — TXLSXOpaquePackage.RootRelationshipsXml
UsedIds.CaseSensitive:= False;
UsedIds.Add('rId1');
if EmitCustomProps then UsedIds.Add('rId4');   // model yazıcısı tarafından rezerve edildi
if EmitDocProps then
begin
  UsedIds.Add('rId2');
  UsedIds.Add('rId3');
end;
for i:= 0 to FRootRelationships.Count- 1 do
begin
  Rel:= TXLSXOpaqueRelationship(FRootRelationships[i]);
  // Özel özellikler modele ait; kaynak kopyayı yeniden oynatma.
  if EmitCustomProps and OpcEndsWith(LowerCase(Rel.RelType), '/custom-properties') then
    Continue;
  Id:= Rel.Id;
  if (Id= '')or (UsedIds.IndexOf(String(Id))>= 0) then
    Id:= AllocateRelationshipId(UsedIds);      // en düşük boş rIdN
  UsedIds.Add(String(Id));
  ...
end;

Üç kuralın ortak noktası ne?

Üçü de iki kaynağı olan ve paket değişmezlerinin tek bir sahibi bulunmayan bir yazıcıya karşı korur. Nesne modeli anladığı parçaları üretir; opak katman anlamadığı parçaları yeniden oynatır, böylece bir round-trip theme, extLst ve calcChain üzerine lossless round-trip notlarında anlatılan chart'ları, pivot cache'leri, özel XML'i ve daha fazlasını korur. Her iki taraf da kendi içinde tutarlı olabilir. OPC'nin tüm pakete koyduğu kısıtlar — tekil Override parça adları ve parça başına tekil relationship tanımlayıcıları — ancak ikisinin birleştirildiği dikişte var olur; o hâlde dikişin bir sahibi olmak zorundadır. fontId kuralı da bir kat aşağıda aynı biçimdedir: bir yazıcı neyi atlamak istediğini biliyor olabilir ve atlayamayacağını söyleyen şemaya hiç danışmayabilir. HotXLS bunu bir birleştirme sezgisiyle değil sabit bir öncelikle çözer. Önce model yazar; opak katman yazılanı görür ve her çakışmada geri çekilir. Değişmezleri dışarıdan kontrol etmek de ucuzdur: kaydedilmiş bir paketteki [Content_Types].xml dosyasını ve her .rels öğesini okuyun, yinelenen her PartName, Extension ya da Id için vakayı başarısız sayın. Bu kontrol Excel gerektirmiyor ve iki tekillik kuralını kapsıyor; fontId kuralı için hâlâ bir şema doğrulayıcı gerekiyor

Aralık değil formül olan yazdırma alanları

Yazdırma alanı, Excel'in kaydedilen kopyada karşılaştırdığı diğer şeydir: _xlnm.Print_Area tanımı orijinalde $A$1:$J$29 olarak raporlanan bir şablon, round-trip sonrasında da aynısını raporlamak zorundadır. Bu işin arkasında iki tuzak var. İçe aktarmada ilk tırnaksız ! işaretine kadar her şeyi kesen bir önek kırpma, OFFSET('Print Data'!$A$1,0,0,2,2) gibi dinamik bir yazdırma alanını $A$1,0,0,2,2) biçimine çevirir ve 'Print Data'!$A$1:$B$2,'Print Data'!$D$1:$E$2 gibi nitelenmiş bir birleşimin önekini yalnızca ilk parçasından düşürür. Dışa aktarmada saklanan PrintArea değerinin tamamına sayfa adını bir kez başa eklemek, $A$1:$B$2,$D$1:$E$2 gibi düz bir birleşime nitelenmiş bir ilk parça ile çıplak bir ikinci parça verir; Excel bunu ECMA-376 Part 1 §18.2.5 kapsamında bir _xlnm.Print_Area tanımı olarak kabul etmez

// İçe aktarma: öneki yalnızca kalan şey düz bir sqref ise kırp
function XlsxPrintAreaFromDefinition(const Formula: WideString): WideString;
begin
  Result:= Formula;
  if not XlsxReadFormulaSheetPrefixAt(Formula, 1, Prefix, SheetPart, Start) then
    Exit;
  Area:= Copy(Formula, Start, Length(Formula));
  if XlsxParseSqrefPart(Area, R1, C1, R2, C2) then
    Result:= Area;                    // 'Sheet'!$A$1:$J$29 -> $A$1:$J$29
end;                                  // OFFSET(...) dokunulmadan döner

// Dışa aktarma: virgülle ayrılmış her parçayı nitele, ya da hiçbirini
function XlsxPrintAreaDefinition(const SheetName, Area: WideString): WideString;
begin
  Result:= Area;
  ... split Area on ',' with StrictDelimiter ...
  for I:= 0 to Parts.Count- 1 do
    if not XlsxParseSqrefPart(WideString(Trim(Parts[I])), R1, C1, R2, C2) then
      Exit;                           // bir formül: olduğu gibi yaz
  Result:= '';
  for I:= 0 to Parts.Count- 1 do
  begin
    if I> 0 then Result:= Result+ ',';
    Result:= Result+ XlsxQuoteSheetName(SheetName)+ '!'+ WideString(Trim(Parts[I]));
  end;
end;

Eşleştirme kuralı iki tarafta da aynı: bir yazdırma alanı ancak her parçası düz aralık olarak ayrıştırılabiliyorsa çıplak aralıktır, aksi hâlde bir formüldür ve olduğu gibi taşınır. Bu kuralla adlandırılmış taban, sayfa nitelenmiş taban ve birleşim, tekrarlanan kaydet-aç döngülerini bozulmadan atlatır. Yazdırma alanlarının sayfa yapısı ve baskı modelinin geri kalanıyla nasıl etkileştiği sayfa koruması, sayfa yapısı ve yazdırma makalesinde ele alınır

Excel'in hangi kurala itiraz ettiğini nasıl bulursunuz?

Kendi doğrulayıcınızın yanlış olduğu varsayımıyla başlayın, çünkü o geçti. Open XML SDK doğrulayıcısı eksik fontId gibi bir şema ihlalini parçasıyla ve XPath'iyle adlandırır; altındaki paketleme katmanı ise yinelenen içerik tipi girdileri olan bir paketi hiç açmaz, bu yüzden her şeyden önce onu çalıştırın. Sessiz kaldığı ve Excel yine de onardığı durumda paketi ikiye bölerek arayın: arşivi açın, bir parçayı relationship'i ve Override girdisiyle birlikte silin, yeniden sıkıştırın ve açın; istem kaybolana kadar aday kümesini her seferinde yarıya indirin. Yukarıdaki üç ihlal de bu sırayla su yüzüne çıkar ve hiçbiri Excel'in kaydetmeyi teklif ettiği onarılmış dosyada görünmez, çünkü onarım sorunlu girdileri sessizce düşürür ya da yeniden numaralandırır. HotXLS yazıcısındaki iki tasarım kararını bilmekte fayda var. Tekilleştirme, modelin önde olduğu ilk-gelen-kazanır biçimindedir; yani kaynak paket, modelin de ürettiği bir parça için farklı bir içerik tipi bildirdiyse modelin bildirimi kazanır ve kaynağınki atılır. Bu, HotXLS'in yeniden ürettiği parçalar için doğrudur ve genel bir birleştirme değildir. Bir de üretilen içerik tipleri akışı üzerindeki ek TXMLReader geçişi, PreserveUnsupportedParts etkinken her kayıtta çalışır; nadiren birkaç kilobaytı geçen bir akış için küçük bir maliyettir. Bu kurallar yerindeyken her iki derlemenin de kaydettiği 4805 formüllü kredi şablonu Excel'de istem olmadan açılıyor, sıfır uyuşmazlıkla yeniden hesaplanıyor ve orijinalle aynı yazdırma alanını raporluyor

XLSX'i Delphi'den kendiniz yazıyorsanız kontrol listesi kısa: şemanın zorunlu işaretlediği her özniteliği değerine bakmadan yazın, her parça adını bir kez bildirin ve ona dokunan her yazıcı için relationships parçası başına tek bir kullanılmış tanımlayıcı listesi tutun. O listenin zaten var olmasını ve yalnızca kendi okuyucunuza değil Excel'e karşı da test edilmiş olmasını isterseniz, burada anlatılan paket yazıcısı HotXLS Delphi spreadsheet component içinde, dikişi korumaya değer kılan opak parça round-trip'iyle birlikte geliyor