Excel menampilkan "Kami menemukan masalah pada sebagian konten" pada XLSX yang dibuka LibreOffice dan semua reader buatan sendiri tanpa keluhan, karena Excel menegakkan dua hal yang diabaikan reader tersebut: atribut yang diwajibkan skema dan aturan keunikan Open Packaging Conventions. HotXLS, komponen spreadsheet Excel native untuk Delphi dan C++Builder, kena persis hal itu di v2.382.5 saat output-nya pertama kali melewati instance COM Excel sungguhan, dan tiga penyebabnya adalah <phoneticPr> tanpa fontId, Override ganda di [Content_Types].xml, serta dua relationship root yang berbagi rId4
Kenapa Excel menolak package yang diterima semua reader lain?
Karena prompt repair itu validator skema dan package, bukan kegagalan parser. Korpus HotXLS sudah berminggu-minggu me-round-trip template kredit 4805 formula lewat library, lewat LibreOffice, dan lewat validator XML di test suite. File hasil save sehat secara struktural dalam pengertian OPC yang dipakai di artikel resolusi relationship OPC XLSX: setiap part terjangkau, setiap target bisa diresolusi. Lalu ada mesin Windows dengan Excel 16.0 build 20326, runner korpus membuka template hasil save lewat Workbooks.Open di instance COM terisolasi dengan DisplayAlerts mati, dan panggilan itu gagal total. Secara interaktif file yang sama memunculkan dialog akrab yang menawarkan repair, dan log repair-nya, kalau Excel repot menulisnya, menyebut part-nya tapi tidak menyebut aturannya. Tiga cacat independen bersembunyi di satu prompt itu, dan Excel tidak melaporkannya satu per satu; ia menolak workbook-nya dan membiarkan Anda mencari dan menganalisisnya sendiri. Berikut ini masing-masing aturannya, baris HotXLS yang melanggarnya, dan perbaikan yang sudah dirilis, karena semuanya adalah aturan yang bisa tersandung oleh writer XLSX Delphi mana pun
Aturan 1: fontId pada phoneticPr wajib, bahkan saat nilainya nol
Elemen <phoneticPr> membawa atribut fontId yang dideklarasikan use="required" di ECMA-376 Part 1 §18.4.3, dan nilai 0 adalah indeks font yang legal, bukan ketiadaan. Writer worksheet HotXLS yang lama memperlakukan nol sebagai "belum diatur" dan hanya mengeluarkan atributnya ketika Sheet.PhoneticFontId > 0. Itu refleks Delphi yang wajar, karena field integer default-nya nol, tapi hasilnya <phoneticPr type="noConversion"/> untuk workbook yang font fonetiknya kebetulan merupakan font pertama di styles.xml, yang persis dibawa template kredit di korpus HotXLS. Excel lalu menolak, di jalan masuk kembali, nilai yang ia sendiri tulis
// lxHandleX.pas, writer worksheet — sebelum v2.382.5
phoneticXml:= '<phoneticPr';
if Sheet.PhoneticFontId > 0 then
phoneticXml:= phoneticXml+ ' fontId="'+ IntToStr(Sheet.PhoneticFontId)+ '"';
// v2.382.5 — atributnya wajib, termasuk nol
phoneticXml:= '<phoneticPr fontId="'+ IntToStr(Sheet.PhoneticFontId)+ '"';
phoneticXml:= phoneticXml+ ' type="'+ XlsxEscapeAttr(Sheet.PhoneticType)+ '"';
if Sheet.PhoneticAlignment<> '' then
phoneticXml:= phoneticXml+ ' alignment="'+ XlsxEscapeAttr(Sheet.PhoneticAlignment)+ '"';
HotXLS tetap mengeluarkan elemen itu hanya ketika TXLSXWorksheet.PhoneticType tidak kosong, jadi workbook yang tidak pernah membawa setting fonetik tidak terpengaruh. Uji regresi PhoneticSettings_DefaultFontIsExplicit menetapkan PhoneticFontId ke nol pada sheet baru, menyimpannya, lalu menegaskan bahwa <phoneticPr fontId="0" ada di xl/worksheets/sheet1.xml. Pelajaran yang lebih luas adalah "hilangkan saat default" hanya aman bila skema mendeklarasikan default-nya; type dan alignment punya default di elemen itu, fontId tidak
Aturan 2: satu Override per nama part di [Content_Types].xml
Stream content type boleh mendeklarasikan setiap nama part paling banyak sekali, dan Excel memperlakukan Override kedua untuk PartName yang sama sebagai korupsi walau kedua entrinya membawa ContentType yang sama. HotXLS punya dua writer yang mengisi stream itu. BuildContentTypesXml mendeklarasikan setiap part yang dihasilkan model objek: workbook, styles, shared strings, theme, worksheet, dan, ketika TXLSXWorkbook.CustomProperties.Count > 0, /docProps/custom.xml. Ketika PreserveUnsupportedParts aktif, TXLSXOpaquePackage lalu menambahkan Override untuk setiap part yang ia tangkap verbatim dari package sumber agar byte-nya tetap terdeklarasi saat keluar. Tabrakannya adalah part yang hidup di kedua sisi. Custom document property di-parse ke model, tapi docProps/custom.xml milik package sumber juga tertangkap secara opak, sehingga stream gabungannya mendeklarasikannya dua kali, dan part chart maupun pivot cache bisa mendarat di titik yang sama ketika model membangkitkan ulang part yang juga ditahan lapisan opak. Sebelum v2.382.5, ContentTypeOverridesXml tidak punya pandangan tentang apa yang sudah ditulis model, jadi ia tidak bisa tahu
<!-- Yang dilihat Excel sebelum v2.382.5 -->
<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"/>
Perbaikannya menyerahkan XML yang dihasilkan ke ContentTypeOverridesXml dan membiarkan writer opak mem-parsenya sebelum ia mengeluarkan apa pun. Dua detail menopang kebenarannya. OpcLowerPartName mengubah ke huruf kecil, membalik backslash menjadi forward slash, dan memangkas slash di depan sebelum perbandingan, karena nama part OPC dibandingkan tanpa membedakan huruf besar-kecil sementara model menulisnya dengan slash di depan sedangkan lapisan opak menyimpan nama item ZIP tanpa slash. Dan pemanggilnya di BuildContentTypesXml menyerahkan Result + '</Types>', menutup dokumen yang baru sebagian dibangun agar TXMLReader melihat input yang well-formed, bukan stream yang terpotong. Aturan yang muncul adalah yang pertama menang dengan model di depan: apa pun yang dideklarasikan model objek bersifat otoritatif, dan replay opak hanya mengisi celah
// lxOpcPackage.pas — TXLSXOpaquePackage.ContentTypeOverridesXml
function TXLSXOpaquePackage.ContentTypeOverridesXml(const ExistingXml: WideString): WideString;
begin
...
UsedNames.Sorted:= True;
UsedNames.Duplicates:= dupIgnore;
if ExistingXml<> '' then
// Parse stream hasil model dan kumpulkan setiap PartName yang dideklarasikan.
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; // sudah dideklarasikan, atau part rels
UsedNames.Add(String(OpcLowerPartName(Part.PartName)));
Result:= Result+ '<Override PartName="/'+ OpcXmlEscapeAttribute(Part.PartName)+
'" ContentType="'+ OpcXmlEscapeAttribute(Part.ContentType)+ '"/>';
end;
end;
Aturan 3: Id relationship unik di dalam satu part relationships
Setiap Relationship di part .rels butuh Id yang unik di dalam part itu, dan Excel menolak package-nya ketika dua relationship berbagi satu Id. HotXLS menulis _rels/.rels di level package dengan identifier tetap: rId1 untuk workbook, rId2 dan rId3 untuk core dan extended document property, dan rId4 untuk custom property ketika model punya. Package opak lalu menambahkan relationship root apa pun yang ia tahan dari sumbernya, menomori ulang identifier yang sudah ada di daftar UsedIds. Daftar itu tahu rId1 sampai rId3. Ia tidak tahu rId4, dan ia tidak tahu bahwa model sebentar lagi akan mengeluarkan relationship custom-property-nya sendiri, jadi package sumber yang relationship custom-property-nya juga rId4, yang justru default tulisan Excel, keluar dengan dua entri rId4 yang menunjuk target yang sama. Pemanggilnya, BuildRootRelsXml, kini menyerahkan Workbook.FCustomProps.Count > 0 sebagai argumen kedua, sehingga reservasi dan skip-nya digerakkan oleh kondisi yang sama yang memutuskan apakah model mengeluarkan rId4 sama sekali. Penomoran ulang aman di akar package karena tidak ada apa pun di dalam workbook yang mereferensikan identifier relationship root berdasarkan nama; trik yang sama akan salah satu level di bawahnya, tempat atribut r:id di workbook.xml mengikat ke identifier di part relationship workbook, itulah sebabnya MergeWorkbookRelationshipsXml menyimpan peta identifier terpisah
// lxOpcPackage.pas — TXLSXOpaquePackage.RootRelationshipsXml
UsedIds.CaseSensitive:= False;
UsedIds.Add('rId1');
if EmitCustomProps then UsedIds.Add('rId4'); // dicadangkan oleh writer model
if EmitDocProps then
begin
UsedIds.Add('rId2');
UsedIds.Add('rId3');
end;
for i:= 0 to FRootRelationships.Count- 1 do
begin
Rel:= TXLSXOpaqueRelationship(FRootRelationships[i]);
// Custom property kini milik model; jangan replay salinan sumbernya.
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); // rIdN bebas terendah
UsedIds.Add(String(Id));
...
end;
Apa kesamaan ketiga kegagalan itu?
Ketiganya adalah gejala writer dengan dua sumber dan tanpa satu pemilik tunggal atas invarian package. Model objek menghasilkan part yang ia pahami; lapisan opak me-replay part yang tidak ia pahami, agar round trip mempertahankan chart, pivot cache, custom XML, dan segala hal lain yang dibahas di catatan tentang round-trip lossless theme, extLst, dan calcChain. Masing-masing sisi konsisten sendiri. Kendala yang diletakkan OPC pada package secara keseluruhan, nama part Override yang unik dan identifier relationship yang unik per part, hanya ada di sambungan tempat keduanya digabung, dan sampai v2.382.5 tidak ada yang memeriksa sambungan itu. Bug fontId berbentuk sama satu level di bawahnya: writer-nya tahu apa yang ingin ia hilangkan tapi tidak pernah mengonsultasikan skema yang melarangnya. Perbaikan yang akhirnya dipilih HotXLS adalah presedensi tetap, bukan heuristik merge. Model menulis lebih dulu, lapisan opak melihat apa yang sudah ditulis lalu mengalah pada setiap tabrakan, dan runner korpus kini menegakkan invariannya dari luar dengan verify_opc_uniqueness, yang membaca [Content_Types].xml dan setiap item .rels di package hasil save lalu menggagalkan kasusnya pada PartName, Extension, atau Id yang duplikat. Pemeriksaan itu murah, tidak butuh Excel, dan akan menangkap dua dari tiga cacat itu di jalannya korpus yang pertama
Batch yang sama: print area yang berupa formula, bukan range
Jalan Excel itu juga menyorot _xlnm.Print_Area milik template kredit, yang dilaporkan Excel sebagai $A$1:$J$29 pada file aslinya dan harus dilaporkan identik pada salinan hasil save. Dua bug terpisah berdiri di balik satu assertion itu. Saat import, XlsxStripSheetPrefix memotong semuanya sampai tanda ! pertama yang tidak dalam tanda kutip, sehingga print area dinamis seperti OFFSET('Print Data'!$A$1,0,0,2,2) kembali sebagai $A$1,0,0,2,2), dan union berkualifikasi seperti 'Print Data'!$A$1:$B$2,'Print Data'!$D$1:$E$2 kehilangan prefix-nya hanya di segmen pertama. Saat export, writer-nya menambahkan nama sheet sekali di depan seluruh PrintArea yang tersimpan, sehingga union biasa $A$1:$B$2,$D$1:$E$2 meninggalkan library dengan segmen pertama berkualifikasi dan segmen kedua tanpa apa pun, yang tidak diterima Excel sebagai definisi _xlnm.Print_Area menurut ECMA-376 Part 1 §18.2.5
// Import: pangkas prefix hanya jika sisanya berupa sqref biasa
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(...) dikembalikan apa adanya
// Export: kualifikasi setiap segmen yang dipisah koma, atau tidak sama sekali
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; // sebuah formula: keluarkan apa adanya
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;
Aturan pasangannya sama di kedua sisi: print area adalah range telanjang hanya jika setiap segmennya berhasil di-parse sebagai range, kalau tidak ia adalah formula dan berjalan apa adanya. PrintArea_FormulaDefinitionSurvivesRoundTrip mencakup basis bernama, basis berkualifikasi sheet, dan union-nya lewat dua siklus save-dan-buka-ulang. Bagaimana print area berinteraksi dengan page setup dan sisa model pencetakannya dibahas di artikel proteksi sheet, page setup, dan pencetakan
Bagaimana menemukan aturan mana yang sedang diprotes Excel?
Mulailah dari asumsi bahwa validator Anda sendiri yang keliru, karena ia lolos. Validator Open XML SDK akan menyebut pelanggaran skema seperti fontId yang hilang beserta part dan XPath-nya, dan lapisan packaging di bawahnya menolak membuka package dengan entri content type yang duplikat sama sekali, jadi jalankan itu sebelum apa pun. Ketika ia diam sementara Excel tetap melakukan repair, belah package-nya: unzip, hapus satu part beserta relationship dan Override-nya, zip ulang, dan buka lagi, potong set kandidatnya separuh tiap kali sampai prompt-nya hilang. Tiga cacat di sini terungkap dengan urutan itu, dan tidak satu pun akan terlihat di file hasil repair yang ditawarkan Excel untuk disimpan, karena repair-nya diam-diam membuang atau menomori ulang entri yang bermasalah. Batas perbaikan v2.382.5 layak dinyatakan sama blak-blakan. Deduplikasinya adalah yang pertama menang dengan model di depan, jadi jika package sumber mendeklarasikan content type berbeda untuk part yang juga dihasilkan model, deklarasi model yang menang dan punya sumber dibuang, yang benar untuk part yang diregenerasi HotXLS dan bukan penggabungan umum. verify_opc_uniqueness hanya memeriksa keunikan; ia tidak memvalidasi skema, jadi atribut wajib baru di kemudian hari tetap butuh Excel atau validator skema untuk memunculkannya. Dan pass TXMLReader tambahan atas stream content type yang dihasilkan berjalan di setiap save dengan PreserveUnsupportedParts aktif, biaya kecil untuk stream yang jarang lebih dari beberapa kilobyte. Dengan semua itu terpasang, build Win32 maupun Win64 dari template kreditnya kini terbuka di Excel tanpa prompt, menghitung ulang seluruh 4805 formula yang terverifikasi tanpa satu pun ketidakcocokan, dan melaporkan print area yang sama dengan aslinya
Kalau Anda sendiri menulis XLSX dari Delphi, daftar periksanya pendek: keluarkan setiap atribut yang ditandai wajib oleh skema tanpa memandang nilainya, deklarasikan setiap nama part sekali, dan simpan satu daftar identifier terpakai per part relationships di seluruh writer yang menyentuhnya. Kalau Anda lebih suka daftar itu sudah ada dan teruji terhadap Excel, bukan hanya terhadap reader Anda sendiri, package writer yang diuraikan di sini sudah tersedia di komponen spreadsheet Delphi HotXLS, bersama round-trip part opak yang membuat sambungan itu layak dijaga sejak awal