Excel prikazuje „Našli smo problem sa nekim sadržajem" na XLSX-u koji LibreOffice i svaki domaći čitač otvaraju bez primedbe, jer Excel sprovodi dve stvari koje ti čitači ignorišu: atribute koje šema zahteva i pravila jedinstvenosti Open Packaging Conventions. HotXLS, nativna Excel spreadsheet komponenta za Delphi i C++Builder, udario je tačno u to u v2.382.5 prvi put kada je njegov izlaz prošao kroz pravu Excel COM instancu, a tri uzroka bila su <phoneticPr> bez fontId, duplirani Override u [Content_Types].xml i dve root relacije koje dele rId4
Zašto Excel odbija paket koji svaki drugi čitač prihvata?
Zato što je prompt za popravku validacija šeme i paketa, a ne pad parsera. HotXLS corpus nedeljama je provlačio predložak kredita sa 4805 formula kroz biblioteku, kroz LibreOffice i kroz XML validatore u test suite-u. Sačuvani fajl bio je strukturno ispravan u OPC smislu opisanom u članku o razrešavanju XLSX OPC relacija: svaki deo dostupan, svaki target razrešiv. Zatim je postala dostupna Windows mašina sa Excel-om 16.0 build 20326, corpus runner je otvorio sačuvani predložak preko Workbooks.Open u izolovanoj COM instanci sa isključenim DisplayAlerts, i poziv je pao u startu. Interaktivno isti fajl daje poznati dijalog koji nudi popravku, a repair log, kada se Excel uopšte potrudi da ga napiše, imenuje deo ali ne i pravilo. Tri nezavisna defekta krila su se u tom jednom promptu, a Excel ih ne prijavljuje jedno po jedno; odbija radnu svesku i ostavlja vas da ih nađete i analizirate. Ono što sledi je svako pravilo, linija HotXLS-a koja ga je prekršila, i ispravka koja je isporučena, jer je svako od njih pravilo o koje se može saplesti svaki Delphi XLSX pisac
Pravilo 1: phoneticPr fontId je obavezan, čak i kada je nula
Element <phoneticPr> nosi atribut fontId deklarisan sa use="required" u ECMA-376 Part 1 §18.4.3, a vrednost 0 je legalan indeks fonta, ne odsustvo. Stari HotXLS pisac radnog lista tretirao je nulu kao „nije postavljeno" i emitovao atribut samo kada je Sheet.PhoneticFontId > 0. To je prirodan Delphi refleks, jer celobrojna polja podrazumevano imaju nulu, ali proizvodi <phoneticPr type="noConversion"/> za svaku radnu svesku čiji je fonetski font slučajno prvi font u styles.xml, što je upravo ono što je nosio predložak kredita u HotXLS corpus-u. Excel zatim odbija na ulazu vrednost koju je sam napisao
// lxHandleX.pas, pisac radnog lista — pre v2.382.5
phoneticXml:= '<phoneticPr';
if Sheet.PhoneticFontId > 0 then
phoneticXml:= phoneticXml+ ' fontId="'+ IntToStr(Sheet.PhoneticFontId)+ '"';
// v2.382.5 — atribut je obavezan, uključujući i nulu
phoneticXml:= '<phoneticPr fontId="'+ IntToStr(Sheet.PhoneticFontId)+ '"';
phoneticXml:= phoneticXml+ ' type="'+ XlsxEscapeAttr(Sheet.PhoneticType)+ '"';
if Sheet.PhoneticAlignment<> '' then
phoneticXml:= phoneticXml+ ' alignment="'+ XlsxEscapeAttr(Sheet.PhoneticAlignment)+ '"';
HotXLS element i dalje emituje samo kada je TXLSXWorksheet.PhoneticType neprazan, pa radne sveske koje nikad nisu nosile fonetska podešavanja ostaju nepromenjene. Regresioni test PhoneticSettings_DefaultFontIsExplicit postavlja PhoneticFontId na nulu na svežem listu, čuva, i tvrdi da je <phoneticPr fontId="0" prisutan u xl/worksheets/sheet1.xml. Šira pouka je da je „izostavi kada je podrazumevano" bezbedno samo kada šema deklariše podrazumevanu vrednost; type i alignment imaju podrazumevane vrednosti u tom elementu, fontId ih nema
Pravilo 2: jedan Override po imenu dela u [Content_Types].xml
Tok sadržajnih tipova može deklarisati svako ime dela najviše jednom, a Excel drugi Override za isti PartName tretira kao korupciju čak i kada oba unosa nose isti ContentType. HotXLS ima dva pisca koja pune taj tok. BuildContentTypesXml deklariše svaki deo koji objektni model generiše: radnu svesku, stilove, deljene stringove, temu, radne listove, i, kada je TXLSXWorkbook.CustomProperties.Count > 0, /docProps/custom.xml. Kada je PreserveUnsupportedParts uključen, TXLSXOpaquePackage zatim dodaje Override za svaki deo koji je verbatim snimio iz izvornog paketa, kako bi ti bajtovi ostali deklarisani i na izlazu. Sudar nastaje za deo koji živi na obe strane. Custom document properties se parsiraju u model, ali je i docProps/custom.xml iz izvornog paketa snimljen neprozirno, pa je spojeni tok deklarisao taj deo dvaput, a chart i pivot cache delovi mogu da slete na isto mesto kada model regeneriše deo koji je neprozirni sloj takođe zadržao. Pre v2.382.5 ContentTypeOverridesXml nije imao uvid u ono što je model već napisao, pa nije mogao da zna
<!-- Šta je Excel video pre 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"/>
Ispravka prosleđuje generisani XML u ContentTypeOverridesXml i pušta neprozirnog pisca da ga parsira pre nego što bilo šta emituje. Dva detalja nose korektnost. OpcLowerPartName prevodi u mala slova, menja backslash u forward slash i skida vodeće kose crte pre poređenja, jer se OPC imena delova porede bez obzira na veličinu slova, a model ih piše sa vodećim kosom crtom dok neprozirni sloj čuva imena ZIP stavki bez nje. I pozivalac u BuildContentTypesXml prosleđuje Result + '</Types>', zatvarajući delimično izgrađen dokument tako da TXMLReader vidi dobro formiran ulaz umesto skraćenog toka. Pravilo koje iz toga izlazi je prvi pobeđuje sa modelom napred: šta god objektni model deklariše je merodavno, a neprozirna reprodukcija samo popunjava rupe
// lxOpcPackage.pas — TXLSXOpaquePackage.ContentTypeOverridesXml
function TXLSXOpaquePackage.ContentTypeOverridesXml(const ExistingXml: WideString): WideString;
begin
...
UsedNames.Sorted:= True;
UsedNames.Duplicates:= dupIgnore;
if ExistingXml<> '' then
// Parsiraj tok koji je generisao model i sakupi svaki deklarisani PartName.
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; // već deklarisano, ili rels deo
UsedNames.Add(String(OpcLowerPartName(Part.PartName)));
Result:= Result+ '<Override PartName="/'+ OpcXmlEscapeAttribute(Part.PartName)+
'" ContentType="'+ OpcXmlEscapeAttribute(Part.ContentType)+ '"/>';
end;
end;
Pravilo 3: identifikatori relacija su jedinstveni unutar dela relacija
Svaka Relationship u .rels delu treba Id koji je jedinstven unutar tog dela, i Excel odbija paket kada dve dele isti. HotXLS piše _rels/.rels na nivou paketa sa fiksnim identifikatorima: rId1 za radnu svesku, rId2 i rId3 za core i extended document properties, i rId4 za custom properties kada ih model ima. Neprozirni paket zatim dodaje sve root relacije koje je zadržao iz izvora, prenumerišući svaki identifikator koji je već u listi UsedIds. Lista je znala za rId1 do rId3. Nije znala za rId4, i nije znala da će model upravo emitovati sopstvenu custom-properties relaciju, pa je izvorni paket čija je custom-properties relacija takođe bila rId4, što Excel podrazumevano i piše, izašao sa dva unosa rId4 koji pokazuju na isti target. Pozivalac, BuildRootRelsXml, sada prosleđuje Workbook.FCustomProps.Count > 0 kao drugi argument, pa su rezervacija i preskakanje vođeni istim uslovom koji odlučuje da li model uopšte emituje rId4. Prenumeracija je bezbedna u korenu paketa jer ništa unutar radne sveske ne referencira identifikatore root relacija po imenu; isti trik bio bi pogrešan nivo niže, gde se r:id atributi u workbook.xml vezuju za identifikatore u delu relacija radne sveske, i zato MergeWorkbookRelationshipsXml drži odvojenu mapu identifikatora
// lxOpcPackage.pas — TXLSXOpaquePackage.RootRelationshipsXml
UsedIds.CaseSensitive:= False;
UsedIds.Add('rId1');
if EmitCustomProps then UsedIds.Add('rId4'); // rezervisano od pisca modela
if EmitDocProps then
begin
UsedIds.Add('rId2');
UsedIds.Add('rId3');
end;
for i:= 0 to FRootRelationships.Count- 1 do
begin
Rel:= TXLSXOpaqueRelationship(FRootRelationships[i]);
// Model sada poseduje custom properties; ne reprodukuj kopiju iz izvora.
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); // najniži slobodan rIdN
UsedIds.Add(String(Id));
...
end;
Šta je zajedničko trima otkazima?
Sva tri su simptomi pisca sa dva izvora i bez jednog vlasnika invarijanti paketa. Objektni model generiše delove koje razume; neprozirni sloj reprodukuje delove koje ne razume, tako da round trip čuva grafikone, pivot cache, custom XML i sve ostalo opisano u beleškama o lossless round trip-u teme, extLst i calcChain. Svaka strana bila je pojedinačno dosledna. Ograničenja koja OPC postavlja na ceo paket, jedinstvena imena Override delova i jedinstveni identifikatori relacija po delu, postoje samo na šavu gde se dvoje spaja, a do v2.382.5 niko nije proveravao šav. fontId bug je istog oblika nivo niže: pisac je znao šta želi da izostavi ali nikad nije konsultovao šemu koja kaže da ne sme. Ispravka na kojoj se HotXLS zaustavio je fiksni prioritet a ne merge heuristika. Model piše prvi, neprozirni sloj vidi šta je napisano i popušta u svakom sudaru, a corpus runner sada sprovodi invarijante spolja preko verify_opc_uniqueness, koji čita [Content_Types].xml i svaku .rels stavku u sačuvanom paketu i obara slučaj na svaki duplikat PartName, Extension ili Id. Ta provera je jeftina, ne treba joj Excel, i uhvatila bi dva od tri defekta u prvom corpus pokretanju
Isti batch: print areas koje su formule, ne opsezi
Excel prolaz je takođe prijavio _xlnm.Print_Area predloška kredita, koji je Excel naveo kao $A$1:$J$29 na originalu i morao identično da ga navede na sačuvanoj kopiji. Dva odvojena buga stajala su iza te jedne tvrdnje. Pri uvozu, XlsxStripSheetPrefix sekao je sve do prvog nekvedovanog !, pa je dinamična print area kao OFFSET('Print Data'!$A$1,0,0,2,2) vraćena kao $A$1,0,0,2,2), a kvalifikovana unija kao 'Print Data'!$A$1:$B$2,'Print Data'!$D$1:$E$2 izgubila je prefiks samo na svom prvom segmentu. Pri izvozu, pisac je dodavao ime lista jednom na celu sačuvanu PrintArea, pa je obična unija $A$1:$B$2,$D$1:$E$2 ostavila biblioteku sa kvalifikovanim prvim segmentom i ogoljenim drugim, što Excel ne prihvata kao definiciju _xlnm.Print_Area po ECMA-376 Part 1 §18.2.5
// Uvoz: skini prefiks samo kada je ono što ostaje običan sqref
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(...) se vraća netaknut
// Izvoz: kvalifikuj svaki segment razdvojen zarezom, ili nijedan
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; // formula: emituj verbatim
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;
Pravilo uparivanja isto je na obe strane: print area je ogoljeni opseg samo ako se svaki segment parsira kao opseg, inače je formula i putuje verbatim. PrintArea_FormulaDefinitionSurvivesRoundTrip pokriva imenovanu bazu, bazu kvalifikovanu listom, i uniju kroz dva ciklusa čuvanja i ponovnog otvaranja. Kako print areas interaguju sa page setup-om i ostatkom modela za štampanje pokriveno je u članku o zaštiti lista, page setup-u i štampanju
Kako naći na koje pravilo Excel prigovara?
Pođite od pretpostavke da je vaš sopstveni validator pogrešan, jer je prošao. Open XML SDK validator imenovaće kršenje šeme kao što je nedostajući fontId uz deo i XPath, a sloj pakovanja ispod njega odbija da uopšte otvori paket sa duplim unosima sadržajnih tipova, pa ga pokrenite pre svega ostalog. Kada ćuti a Excel se i dalje popravlja, bisektirajte paket: raspakujte, obrišite deo i njegovu relaciju i njegov Override, zapakujte ponovo i otvorite, prepolovljavajući skup kandidata svaki put dok prompt ne nestane. Tri defekta odavde ispala su tim redom, i nijedan od njih ne bi bio vidljiv u popravljenom fajlu koji Excel nudi da sačuva, jer popravka tiho odbacuje ili prenumeriše sporne unose. Granice ispravke v2.382.5 vredi reći jednako jasno. Deduplikacija je prvi pobeđuje sa modelom napred, pa ako je izvorni paket deklarisao drugačiji content type za deo koji model takođe generiše, deklaracija modela pobeđuje a izvorna se odbacuje, što je tačno za delove koje HotXLS regeneriše i nije opšti merge. verify_opc_uniqueness proverava samo jedinstvenost; ne validira šeme, pa bi budući obavezni atribut i dalje zahtevao Excel ili validator šeme da ispliva. A dodatni TXMLReader prolaz nad generisanim tokom sadržajnih tipova izvršava se pri svakom čuvanju sa uključenim PreserveUnsupportedParts, mala cena prema toku koji retko prelazi nekoliko kilobajta. Uz to na mestu, i Win32 i Win64 buildovi predloška kredita sada se otvaraju u Excel-u bez prompta, preračunavaju svih 4805 verifikovanih formula sa nula nepodudaranja, i prijavljuju istu print area kao original
Ako sami pišete XLSX iz Delphi-ja, checklista je kratka: emitujte svaki atribut koji šema označava kao obavezan bez obzira na njegovu vrednost, deklarišite svako ime dela jednom, i držite jednu listu iskorišćenih identifikatora po delu relacija kroz svakog pisca koji ga dira. Ako biste radije da ta lista već postoji i da je testirana prema Excel-u, a ne samo prema vašem čitaču, pisac paketa opisan ovde isporučuje se u HotXLS-u Delphi spreadsheet komponenti, zajedno sa round trip-om neprozirnih delova zbog kojeg je šav uopšte vredelo čuvati