Excel näyttää "We found a problem with some content" XLSX-tiedostolle, jonka LibreOffice ja jokainen itse tehty lukija avaavat valittamatta, koska Excel valvoo kahta asiaa, jotka nuo lukijat sivuuttavat: skeeman vaatimia attribuutteja ja Open Packaging Conventions -määrityksen yksikäsitteisyyssääntöjä. HotXLS, natiivi taulukkokomponentti Delphille ja C++Builderille, törmäsi täsmälleen tähän versiossa 2.382.5 ensimmäisellä kerralla, kun sen tuloste ajettiin oikean Excel COM -instanssin läpi, ja kolme syytä olivat <phoneticPr> ilman fontId-attribuuttia, tuplattu Override tiedostossa [Content_Types].xml ja kaksi juurisuhdetta, jotka jakoivat tunnuksen rId4
Miksi Excel hylkää paketin, jonka kaikki muut lukijat hyväksyvät?
Koska korjauskehoitus on skeema- ja pakettivalidaattori, ei jäsentimen virhe. HotXLS:n korpus oli kierrättänyt 4805 kaavan lainamallia kirjaston, LibreOfficen ja testisarjan XML-validaattorien läpi viikkojen ajan. Tallennettu tiedosto oli rakenteellisesti kunnossa siinä OPC-mielessä, jota käytetään artikkelissa XLSX:n OPC-suhderesoluutiosta: jokainen osa tavoitettavissa, jokainen kohde ratkaistavissa. Sitten käyttöön tuli Windows-kone, jossa oli Excel 16.0 build 20326, korpuksen ajaja avasi tallennetun mallin Workbooks.Open-kutsulla eristetyssä COM-instanssissa DisplayAlerts pois päältä, ja kutsu epäonnistui suoraan. Interaktiivisesti sama tiedosto tuottaa tutun valintaikkunan, joka tarjoaa korjausta, ja korjausloki, kun Excel vaivautuu sellaisen kirjoittamaan, nimeää osan mutta ei sääntöä. Yhden kehoituksen takana piili kolme erillistä vikaa, eikä Excel raportoi niitä yksi kerrallaan; se hylkää työkirjan ja jättää sinut etsimään ja analysoimaan ne. Seuraavassa käydään läpi jokainen sääntö, se HotXLS:n rivi joka sitä rikkoi, ja korjaus joka toimitettiin, koska jokainen niistä on sääntö, johon mikä tahansa Delphi-XLSX-kirjoittaja voi kompastua
Sääntö 1: phoneticPr:n fontId on pakollinen, myös nollana
<phoneticPr>-elementti kantaa fontId-attribuuttia, joka on julistettu use="required" ECMA-376 Part 1 §18.4.3:ssa, ja arvo 0 on laillinen fontti-indeksi, ei puuttuminen. Vanha HotXLS:n laskentataulukon kirjoittaja kohteli nollaa asettamattomana ja tuotti attribuutin vain kun Sheet.PhoneticFontId > 0. Se on luonnollinen Delphi-refleksi, koska kokonaislukukentät oletusarvoistuvat nollaan, mutta se tuottaa muodon <phoneticPr type="noConversion"/> jokaiselle työkirjalle, jonka foneettinen fontti sattuu olemaan ensimmäinen fontti tiedostossa styles.xml, ja juuri sellaisen HotXLS:n korpuksen lainamalli kantoi. Excel hylkää silloin paluumatkalla arvon, jonka se oli itse kirjoittanut
// lxHandleX.pas, laskentataulukon kirjoittaja — ennen versiota 2.382.5
phoneticXml:= '<phoneticPr';
if Sheet.PhoneticFontId > 0 then
phoneticXml:= phoneticXml+ ' fontId="'+ IntToStr(Sheet.PhoneticFontId)+ '"';
// v2.382.5 — attribuutti on pakollinen, nolla mukaan lukien
phoneticXml:= '<phoneticPr fontId="'+ IntToStr(Sheet.PhoneticFontId)+ '"';
phoneticXml:= phoneticXml+ ' type="'+ XlsxEscapeAttr(Sheet.PhoneticType)+ '"';
if Sheet.PhoneticAlignment<> '' then
phoneticXml:= phoneticXml+ ' alignment="'+ XlsxEscapeAttr(Sheet.PhoneticAlignment)+ '"';
HotXLS tuottaa elementin yhä vain kun TXLSXWorksheet.PhoneticType ei ole tyhjä, joten työkirjat, joissa ei koskaan ollut foneettisia asetuksia, eivät muutu mihinkään. Regressiotesti PhoneticSettings_DefaultFontIsExplicit asettaa PhoneticFontId-arvoksi nollan tuoreella taulukkosivulla, tallentaa ja väittää, että <phoneticPr fontId="0" on läsnä tiedostossa xl/worksheets/sheet1.xml. Laajempi oppi on, että attribuutin jättäminen pois oletusarvon kohdalla on turvallista vain kun skeema julistaa oletuksen; type- ja alignment-attribuuteilla on oletukset tuossa elementissä, fontId:llä ei ole
Sääntö 2: yksi Override per osan nimi tiedostossa [Content_Types].xml
Sisältötyyppivirta saa julistaa jokaisen osan nimen korkeintaan kerran, ja Excel pitää toista saman PartName-arvon Override-merkintää korruptiona, vaikka molemmat merkinnät kantaisivat samaa ContentType-arvoa. HotXLS:ssä kaksi kirjoittajaa ruokkii tuota virtaa. BuildContentTypesXml julistaa jokaisen osan, jonka objektimalli tuottaa: työkirjan, tyylit, jaetut merkkijonot, teeman, laskentataulukot ja, kun TXLSXWorkbook.CustomProperties.Count > 0, tiedoston /docProps/custom.xml. Kun PreserveUnsupportedParts on päällä, TXLSXOpaquePackage lisää sitten Override-merkinnän jokaiselle osalle, jonka se otti sanatarkasti lähdepaketista, jotta nuo tavut pysyvät julistettuina ulospäin mentäessä. Törmäys syntyy osasta, joka elää molemmilla puolilla. Mukautetut asiakirjaominaisuudet jäsennetään malliin, mutta lähdepaketin docProps/custom.xml otettiin myös läpinäkymättömästi talteen, joten yhdistetty virta julisti sen kahdesti, ja kaavio- ja pivot-välimuistiosat voivat päätyä samaan kohtaan, kun malli luo uudelleen osan, jonka läpinäkymätön kerros myös säilytti. Ennen versiota 2.382.5 ContentTypeOverridesXml-funktiolla ei ollut näkyvyyttä siihen, mitä malli oli jo kirjoittanut, joten se ei voinut tietää
<!-- Mitä Excel näki ennen versiota 2.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"/>
Korjaus välittää generoidun XML:n ContentTypeOverridesXml-funktiolle ja antaa läpinäkymättömän kirjoittajan jäsentää sen ennen kuin se tuottaa mitään. Kaksi yksityiskohtaa kantavat oikeellisuuden. OpcLowerPartName muuttaa pieniksi kirjaimiksi, kääntää kenoviivat vinoviivoiksi ja riisuu johtavat vinoviivat ennen vertailua, koska OPC:n osanimiä verrataan kirjainkoosta riippumatta ja malli kirjoittaa ne johtavalla vinoviivalla, kun taas läpinäkymätön kerros tallentaa ZIP-merkintöjen nimet ilman sitä. Ja kutsuja BuildContentTypesXml-funktiossa välittää Result + '</Types>', sulkien osittain rakennetun asiakirjan, jotta TXMLReader näkee hyvin muodostetun syötteen eikä katkaistua virtaa. Syntyvä sääntö on ensimmäinen voittaa mallin ollessa edessä: se, mitä objektimalli julistaa, on auktoritatiivista, ja läpinäkymätön toisto täyttää vain aukot
// lxOpcPackage.pas — TXLSXOpaquePackage.ContentTypeOverridesXml
function TXLSXOpaquePackage.ContentTypeOverridesXml(const ExistingXml: WideString): WideString;
begin
...
UsedNames.Sorted:= True;
UsedNames.Duplicates:= dupIgnore;
if ExistingXml<> '' then
// Jäsennä mallin tuottama virta ja kerää jokainen julistettu 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; // jo julistettu, tai rels-osa
UsedNames.Add(String(OpcLowerPartName(Part.PartName)));
Result:= Result+ '<Override PartName="/'+ OpcXmlEscapeAttribute(Part.PartName)+
'" ContentType="'+ OpcXmlEscapeAttribute(Part.ContentType)+ '"/>';
end;
end;
Sääntö 3: suhdetunnukset ovat yksikäsitteisiä suhdeosassa
Jokainen .rels-osan Relationship tarvitsee Id-tunnuksen, joka on yksikäsitteinen kyseisessä osassa, ja Excel kieltäytyy paketista kun kaksi jakaa yhden. HotXLS kirjoittaa pakettitason _rels/.rels-tiedoston kiinteillä tunnuksilla: rId1 työkirjalle, rId2 ja rId3 ydin- ja laajennetuille asiakirjaominaisuuksille sekä rId4 mukautetuille ominaisuuksille, kun mallissa niitä on. Läpinäkymätön paketti lisää sitten kaikki juurisuhteet, jotka se säilytti lähteestä, numeroimalla uudelleen jokaisen tunnuksen, joka on jo UsedIds-listassa. Lista tunsi tunnukset rId1–rId3. Se ei tuntenut tunnusta rId4, eikä se tiennyt, että malli oli juuri tuottamassa oman mukautettujen ominaisuuksien suhteensa, joten lähdepaketti, jonka mukautettujen ominaisuuksien suhde oli myös rId4, mikä on Excelin oletusarvoisesti kirjoittama muoto, tuli ulos kahdella rId4-merkinnällä, jotka osoittivat samaan kohteeseen. Kutsuja BuildRootRelsXml välittää nyt Workbook.FCustomProps.Count > 0 toisena argumenttina, joten varaus ja ohitus ajetaan samalla ehdolla, joka ratkaisee, tuottaako malli rId4-tunnusta lainkaan. Uudelleennumerointi on turvallista paketin juuressa, koska mikään työkirjan sisällä ei viittaa juurisuhdetunnuksiin nimeltä; sama temppu olisi väärä yhden tason alempana, missä workbook.xml:n r:id-attribuutit sitoutuvat työkirjan suhdeosan tunnuksiin, minkä takia MergeWorkbookRelationshipsXml pitää erillistä tunnuskarttaa
// lxOpcPackage.pas — TXLSXOpaquePackage.RootRelationshipsXml
UsedIds.CaseSensitive:= False;
UsedIds.Add('rId1');
if EmitCustomProps then UsedIds.Add('rId4'); // mallin kirjoittaja on varannut tämän
if EmitDocProps then
begin
UsedIds.Add('rId2');
UsedIds.Add('rId3');
end;
for i:= 0 to FRootRelationships.Count- 1 do
begin
Rel:= TXLSXOpaqueRelationship(FRootRelationships[i]);
// Malli omistaa mukautetut ominaisuudet nyt; älä toista lähdekopiota.
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); // alin vapaa rIdN
UsedIds.Add(String(Id));
...
end;
Mikä on kolmen vian yhteinen piirre?
Kaikki kolme ovat oireita kirjoittajasta, jolla on kaksi lähdettä eikä yhtään omistajaa paketin invariantille. Objektimalli tuottaa osat, jotka se ymmärtää; läpinäkymätön kerros toistaa osat, joita se ei ymmärrä, jotta edestakainen kierros säilyttää kaaviot, pivot-välimuistit, mukautetun XML:n ja kaiken muun, jota kuvataan muistiinpanoissa teeman, extLst:n ja calcChainin häviöttömästä edestakaisesta kierroksesta. Kumpikin puoli oli itsessään johdonmukainen. Ne rajoitteet, jotka OPC asettaa koko paketille — yksikäsitteiset Override-osanimet ja yksikäsitteiset suhdetunnukset osaa kohden — ovat olemassa vain siinä saumassa, jossa nämä kaksi yhdistetään, eikä kukaan tarkistanut saumaa ennen versiota 2.382.5. fontId-vika on samanmuotoinen yhden tason alempana: kirjoittaja tiesi, mitä halusi jättää pois, muttei koskaan kysynyt skeemalta, joka sanoo ettei niin saa tehdä. Ratkaisu, johon HotXLS päätyi, on kiinteä etusijajärjestys eikä yhdistämisheuristiikka. Malli kirjoittaa ensin, läpinäkymätön kerros näkee mitä kirjoitettiin ja väistyy jokaisessa törmäyksessä, ja korpuksen ajaja valvoo nyt invariantteja ulkopuolelta komennolla verify_opc_uniqueness, joka lukee tiedoston [Content_Types].xml ja jokaisen tallennetun paketin .rels-merkinnän ja hylkää tapauksen jokaisesta päällekkäisestä PartName-, Extension- tai Id-arvosta. Tarkistus on halpa, ei tarvitse Exceliä, ja se olisi napannut kaksi kolmesta viasta jo ensimmäisellä korpusajolla
Samassa erässä: tulostusalueet, jotka ovat kaavoja eivät alueita
Excel-kierros nosti esiin myös lainamallin _xlnm.Print_Area-määritteen, jonka Excel raportoi alkuperäisessä muodossa $A$1:$J$29 ja joutui raportoimaan samanlaisena tallennetussa kopiossa. Yhden väittämän takana oli kaksi erillistä bugia. Tuonnissa XlsxStripSheetPrefix leikkasi kaiken ensimmäiseen lainaamattomaan !-merkkiin asti, joten dynaaminen tulostusalue kuten OFFSET('Print Data'!$A$1,0,0,2,2) palasi muodossa $A$1,0,0,2,2), ja kvalifioitu unioni kuten 'Print Data'!$A$1:$B$2,'Print Data'!$D$1:$E$2 menetti etuliitteen vain ensimmäisestä segmentistään. Viennissä kirjoittaja liitti taulukkosivun nimen kerran koko tallennettuun PrintArea-arvoon, joten pelkkä unioni $A$1:$B$2,$D$1:$E$2 jätti kirjastoon kvalifioidun ensimmäisen segmentin ja paljaan toisen, mitä Excel ei hyväksy _xlnm.Print_Area-määritelmäksi ECMA-376 Part 1 §18.2.5:n mukaan
// Tuonti: riisu etuliite vain kun jäljelle jää pelkkä 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(...) palautetaan koskemattomana
// Vienti: kvalifioi jokainen pilkuin eroteltu segmentti tai ei yhtään niistä
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; // kaava: tulostetaan sellaisenaan
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;
Pariosääntö on sama molemmilla puolilla: tulostusalue on paljas alue vain jos jokainen segmentti jäsentyy sellaiseksi, muuten se on kaava ja kulkee sellaisenaan. PrintArea_FormulaDefinitionSurvivesRoundTrip kattaa nimetyn kannan, taulukkosivukvalifioidun kannan ja unionin kahden tallenna-ja-avaa-kierroksen läpi. Siitä, miten tulostusalueet vaikuttavat sivun asetteluun ja muuhun tulostusmalliin, kerrotaan artikkelissa taulukkosivun suojauksesta, sivun asettelusta ja tulostuksesta
Miten löydät, mihin sääntöön Excel oikein takertuu?
Lähde siitä olettamuksesta, että oma validaattorisi on väärässä, koska se meni läpi. Open XML SDK:n validaattori nimeää skeemarikkomuksen, kuten puuttuvan fontId-attribuutin, osan ja XPath-polun kanssa, ja sen alla oleva pakkauskerros kieltäytyy avaamasta pakettia, jossa on päällekkäisiä sisältötyyppimerkintöjä, joten aja se ennen muuta. Kun se on vaiti ja Excel korjaa silti, puolita paketti: pura zip, poista yksi osa sekä sen suhde ja Override-merkintä, pakkaa uudelleen ja avaa uudelleen, puolittaen ehdokasjoukko joka kerta kunnes kehoitus katoaa. Tässä kuvatut kolme vikaa putosivat esiin juuri tuossa järjestyksessä, eikä yksikään niistä olisi ollut näkyvissä siinä korjatussa tiedostossa, jota Excel tarjoaa tallennettavaksi, koska korjaus pudottaa tai numeroi uudelleen ongelmalliset merkinnät hiljaisesti. Myös version 2.382.5 korjauksen rajat kannattaa sanoa yhtä selvästi. Deduplikointi on ensimmäinen voittaa mallin ollessa edessä, joten jos lähdepaketti julisti osalle eri sisältötyypin kuin malli myös tuottaa, mallin julistus voittaa ja lähteen julistus hylätään, mikä on oikein niille osille jotka HotXLS generoi uudelleen eikä ole yleinen yhdistäminen. verify_opc_uniqueness tarkistaa vain yksikäsitteisyyden; se ei validoi skeemoja, joten tuleva pakollinen attribuutti tarvitsisi yhä Excelin tai skeemavalidaattorin noustakseen esiin. Ja ylimääräinen TXMLReader-kierros generoidun sisältötyyppivirran yli ajetaan jokaisella tallennuksella, kun PreserveUnsupportedParts on päällä — pieni kustannus virrasta, joka harvoin on muutamaa kilotavua suurempi. Nämä kun ovat paikallaan, sekä Win32- että Win64-buildit lainamallista avautuvat Excelissä ilman kehoitusta, laskevat uudelleen kaikki 4805 vahvistettua kaavaa ilman yhtään poikkeamaa ja raportoivat saman tulostusalueen kuin alkuperäinen
Jos kirjoitat XLSX:ää Delphistä itse, tarkistuslista on lyhyt: tuota jokainen attribuutti, jonka skeema merkitsee pakolliseksi, arvosta riippumatta, julista jokainen osan nimi kerran ja pidä yhtä käytettyjen tunnusten listaa per suhdeosa kaikkien sitä koskettavien kirjoittajien yli. Jos haluat mieluummin, että tuo lista on jo olemassa ja testattu Exceliä eikä vain omaa lukijaasi vasten, tässä kuvattu pakettikirjoittaja toimitetaan HotXLS:n Delphi-taulukkokomponentissa yhdessä läpinäkymättömien osien edestakaisen kierroksen kanssa, jonka takia sauma ylipäänsä oli vartioimisen arvoinen