Excel rodo „We found a problem with some content" XLSX failui, kurį LibreOffice ir visi savadarbiai skaitytuvai atveria be priekaištų, nes Excel reikalauja dviejų dalykų, kuriuos tie skaitytuvai ignoruoja: schemos reikalaujamų atributų ir Open Packaging Conventions unikalumo taisyklių. HotXLS — savo Excel skaičiuoklės komponentas Delphi ir C++Builder — su tuo susidūrė būtent v2.382.5, kai jo išvestis pirmą kartą praėjo per tikrą Excel COM egzempliorių, o trys priežastys buvo <phoneticPr> be fontId, pasikartojęs Override [Content_Types].xml faile ir du šakniniai ryšiai, besidalijantys rId4
Kodėl Excel atmeta paketą, kurį priima visi kiti skaitytuvai?
Nes taisymo pranešimas yra schemos ir paketo tikrintuvas, o ne analizatoriaus gedimas. HotXLS korpusas savaitėmis varė 4805 formulių paskolos šabloną pirmyn ir atgal per biblioteką, per LibreOffice ir per testų rinkinio XML tikrintuvus. Įrašytas failas OPC prasme, vartojama straipsnyje apie XLSX OPC ryšių sprendimą, buvo struktūriškai tvarkingas: kiekviena dalis pasiekiama, kiekvienas taikinys išsprendžiamas. Paskui atsirado Windows mašina su Excel 16.0 build 20326, korpuso leidėjas atidarė įrašytą šabloną per Workbooks.Open izoliuotame COM egzemplioriuje su išjungtu DisplayAlerts — ir kvietimas tiesiog nepavyko. Interaktyviai tas pats failas iššaukia įprastą dialogą su pasiūlymu taisyti, o taisymo žurnale, kai Excel apskritai jį parašo, nurodoma dalis, bet ne taisyklė. Tame viename pranešime slėpėsi trys nepriklausomi defektai, o Excel jų nereportuoja po vieną; jis atmeta darbaknygę ir palieka jums jų ieškoti ir jas analizuoti. Toliau — kiekviena taisyklė, ją pažeidusi HotXLS eilutė ir pasirodžiusi pataisa, nes kiekviena jų yra taisyklė, į kurią gali užkliūti bet kuris Delphi XLSX rašytojas
1 taisyklė: phoneticPr fontId yra privalomas, net kai lygus nuliui
<phoneticPr> elementas neša fontId atributą, paskelbtą use="required" ECMA-376 1 dalies §18.4.3, o reikšmė 0 yra teisėtas šrifto indeksas, o ne nebuvimas. Senasis HotXLS darbalapio rašytojas nulį traktavo kaip „nenustatyta" ir atributą išvesdavo tik tada, kai Sheet.PhoneticFontId > 0. Tai natūralus Delphi refleksas, nes sveikojo skaičiaus laukai pagal nutylėjimą yra nuliai, bet taip gaunamas <phoneticPr type="noConversion"/> bet kuriai darbaknygei, kurios fonetinis šriftas atsitiktinai yra pirmas styles.xml šriftas — o būtent tokį ir nešė HotXLS korpuso paskolos šablonas. Excel tada atmesdamas atmeta pačios savęs įrašytą reikšmę
// lxHandleX.pas, darbalapio rašytojas — iki v2.382.5
phoneticXml:= '<phoneticPr';
if Sheet.PhoneticFontId > 0 then
phoneticXml:= phoneticXml+ ' fontId="'+ IntToStr(Sheet.PhoneticFontId)+ '"';
// v2.382.5 — atributas privalomas, įskaitant nulį
phoneticXml:= '<phoneticPr fontId="'+ IntToStr(Sheet.PhoneticFontId)+ '"';
phoneticXml:= phoneticXml+ ' type="'+ XlsxEscapeAttr(Sheet.PhoneticType)+ '"';
if Sheet.PhoneticAlignment<> '' then
phoneticXml:= phoneticXml+ ' alignment="'+ XlsxEscapeAttr(Sheet.PhoneticAlignment)+ '"';
HotXLS elementą vis tiek išveda tik tada, kai TXLSXWorksheet.PhoneticType nėra tuščias, tad darbaknygės, kurios niekada neturėjo fonetinių nustatymų, nepatiria jokių pokyčių. Regresinis testas PhoneticSettings_DefaultFontIsExplicit naujame lape nustato PhoneticFontId į nulį, įrašo ir patvirtina, kad xl/worksheets/sheet1.xml faile yra <phoneticPr fontId="0". Platesnė pamoka tokia: „praleisti, kai numatytoji" saugu tik tada, kai schema paskelbia numatytąją reikšmę; type ir alignment tame elemente numatytąsias turi, fontId — ne
2 taisyklė: vienas Override vienam dalies pavadinimui [Content_Types].xml faile
Turinio tipų srautas kiekvieną dalies pavadinimą gali paskelbti daugiausiai vieną kartą, o Excel antrą Override tam pačiam PartName laiko sugadinimu net tada, kai abu įrašai neša tą patį ContentType. HotXLS turi du rašytojus, maitinančius tą srautą. BuildContentTypesXml paskelbia kiekvieną dalį, kurią generuoja objektinis modelis: darbaknygę, stilius, bendras eilutes, temą, darbalapius ir, kai TXLSXWorkbook.CustomProperties.Count > 0, /docProps/custom.xml. Kai PreserveUnsupportedParts įjungtas, TXLSXOpaquePackage tada prideda po Override kiekvienai daliai, kurią pažodžiui paėmė iš šaltinio paketo, kad tie baitai liktų paskelbti ir išėjime. Susidūrimas kyla dėl dalies, kuri gyvena abiejose pusėse. Pasirinktinės dokumento savybės išanalizuojamos į modelį, bet šaltinio paketo docProps/custom.xml taip pat buvo paimtas nepermatomai, tad sulietas srautas paskelbė jį du kartus, o diagramų ir pivot cache dalys gali atsidurti toje pačioje vietoje, kai modelis atkuria dalį, kurią nepermatomas sluoksnis taip pat buvo išsaugojęs. Iki v2.382.5 ContentTypeOverridesXml nematė, ką modelis jau įrašė, tad negalėjo to žinoti
<!-- Ką Excel matė iki 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"/>
Pataisa perduoda sugeneruotą XML į ContentTypeOverridesXml ir leidžia nepermatomam rašytojui jį išanalizuoti, kol dar nieko neišvedė. Teisingumą laiko dvi detalės. OpcLowerPartName prieš palyginimą paverčia raides mažosiomis, kairinius brūkšnius verčia pasvirųjų brūkšnių ženklais ir nubrauko pradinius pasviruosius brūkšnius, nes OPC dalių pavadinimai lyginami neatsižvelgiant į raidžių dydį, o modelis juos rašo su pradiniu pasviruoju brūkšniu, kai nepermatomas sluoksnis ZIP elemento pavadinimus saugo be jo. O kvietėjas BuildContentTypesXml viduje perduoda Result + '</Types>', taip uždarydamas iš dalies sukonstruotą dokumentą, kad TXMLReader matytų gerai suformuotą įvestį, o ne nukirptą srautą. Iš to išplaukianti taisyklė yra „laimi pirmasis", su modeliu priekyje: ką paskelbia objektinis modelis, tas ir galioja, o nepermatomas atkūrimas tik užpildo spragas
// lxOpcPackage.pas — TXLSXOpaquePackage.ContentTypeOverridesXml
function TXLSXOpaquePackage.ContentTypeOverridesXml(const ExistingXml: WideString): WideString;
begin
...
UsedNames.Sorted:= True;
UsedNames.Duplicates:= dupIgnore;
if ExistingXml<> '' then
// Išanalizuoti modelio sugeneruotą srautą ir surinkti kiekvieną paskelbtą 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; // jau paskelbta arba rels dalis
UsedNames.Add(String(OpcLowerPartName(Part.PartName)));
Result:= Result+ '<Override PartName="/'+ OpcXmlEscapeAttribute(Part.PartName)+
'" ContentType="'+ OpcXmlEscapeAttribute(Part.ContentType)+ '"/>';
end;
end;
3 taisyklė: ryšių Id unikalūs ryšių dalies viduje
Kiekvienas Relationship .rels dalyje turi turėti Id, unikalų tos dalies viduje, ir Excel atsisako paketo, kai du jį pasidalija. HotXLS paketo lygmens _rels/.rels rašo su fiksuotais identifikatoriais: rId1 darbaknygei, rId2 ir rId3 pagrindinėms ir išplėstinėms dokumento savybėms, ir rId4 pasirinktinėms savybėms, kai modelis jų turi. Nepermatomas paketas tada prideda visus šakninius ryšius, kuriuos buvo išsaugojęs iš šaltinio, pernumeruodamas bet kurį identifikatorių, jau esantį UsedIds sąraše. Sąrašas žinojo apie rId1–rId3. Jis nežinojo apie rId4 ir nežinojo, kad modelis tuoj pats išves savo pasirinktinių savybių ryšį, tad šaltinio paketas, kurio pasirinktinių savybių ryšys taip pat buvo rId4 — o taip Excel rašo pagal nutylėjimą — išėjo su dviem rId4 įrašais, nurodančiais į tą patį taikinį. Kvietėjas BuildRootRelsXml dabar kaip antrą argumentą perduoda Workbook.FCustomProps.Count > 0, tad ir rezervacija, ir praleidimas valdomi tos pačios sąlygos, kuri nusprendžia, ar modelis apskritai išves rId4. Numeruoti iš naujo paketo šaknyje saugu, nes niekas darbaknygės viduje nenurodo šakninių ryšių identifikatorių vardu; tas pats triukas būtų klaidingas vienu lygmeniu žemiau, kur r:id atributai workbook.xml faile susieti su identifikatoriais darbaknygės ryšių dalyje, ir todėl MergeWorkbookRelationshipsXml laiko atskirą identifikatorių žemėlapį
// lxOpcPackage.pas — TXLSXOpaquePackage.RootRelationshipsXml
UsedIds.CaseSensitive:= False;
UsedIds.Add('rId1');
if EmitCustomProps then UsedIds.Add('rId4'); // rezervuoja modelio rašytojas
if EmitDocProps then
begin
UsedIds.Add('rId2');
UsedIds.Add('rId3');
end;
for i:= 0 to FRootRelationships.Count- 1 do
begin
Rel:= TXLSXOpaqueRelationship(FRootRelationships[i]);
// Pasirinktinės savybės dabar priklauso modeliui; nekartoti šaltinio kopijos.
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); // mažiausias laisvas rIdN
UsedIds.Add(String(Id));
...
end;
Ką bendro turi visos trys nesėkmės?
Visos trys yra rašytojo su dviem šaltiniais ir be vieno paketo invariantų šeimininko simptomai. Objektinis modelis generuoja dalis, kurias supranta; nepermatomas sluoksnis atkuria dalis, kurių nesupranta, kad kelias pirmyn ir atgal išsaugotų diagramas, pivot cache, pasirinktinį XML ir visa kita, aprašyta pastabose apie nuostolių neturinčią temų, extLst ir calcChain kelionę. Kiekviena pusė atskirai buvo nuosekli. Apribojimai, kuriuos OPC uždeda visam paketui — unikalūs Override dalių pavadinimai ir unikalūs ryšių identifikatoriai kiekvienai daliai — egzistuoja tik toje siūlėje, kur abi sujungiamos, ir iki v2.382.5 tos siūlės niekas netikrino. fontId klaida yra tokios pat formos vienu lygmeniu žemiau: rašytojas žinojo, ką nori praleisti, bet nepasiteiravo schemos, kuri sako, kad negalima. Sprendimas, prie kurio HotXLS apsistojo, yra fiksuotas prioritetas, o ne suliejimo euristika. Pirmas rašo modelis, nepermatomas sluoksnis mato, kas įrašyta, ir nusileidžia prie bet kurio susidūrimo, o korpuso leidėjas dabar invariantus tikrina iš šalies su verify_opc_uniqueness, kuris perskaito [Content_Types].xml ir kiekvieną .rels elementą įrašytame pakete ir užlenkia atvejį dėl bet kurio pasikartojančio PartName, Extension ar Id. Ta patikra pigi, jai nereikia Excel ir ji būtų pagavusi du iš trijų defektų jau pirmame korpuso paleidime
Tas pats paketas: spausdinimo sritys, kurios yra formulės, o ne intervalai
Excel paleidimas taip pat pažymėjo paskolos šablono _xlnm.Print_Area, kurį Excel originalui nurodė kaip $A$1:$J$29 ir turėjo nurodyti identiškai įrašytoje kopijoje. Už to vieno teiginio slėpėsi dvi atskiros klaidos. Importuojant XlsxStripSheetPrefix nukirpdavo viską iki pirmo kabutėmis nesupančioto !, tad dinaminė spausdinimo sritis, tokia kaip OFFSET('Print Data'!$A$1,0,0,2,2), grįždavo kaip $A$1,0,0,2,2), o kvalifikuota sąjunga, tokia kaip 'Print Data'!$A$1:$B$2,'Print Data'!$D$1:$E$2, netekdavo prefikso tik pirmame segmente. Eksportuojant rašytojas vieną kartą pridėdavo lapo pavadinimą prie visos saugomos PrintArea, tad paprasta sąjunga $A$1:$B$2,$D$1:$E$2 palikdavo biblioteką su kvalifikuotu pirmu segmentu ir pliku antru, ko Excel nepriima kaip _xlnm.Print_Area apibrėžimo pagal ECMA-376 1 dalies §18.2.5
// Importuojant: prefiksą nuimti tik tada, kai lieka paprastas 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(...) grąžinamas nepaliestas
// Eksportuojant: kvalifikuoti kiekvieną kableliu atskirtą segmentą arba nė vieno
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; // formulė: išvesti pažodžiui
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;
Suporavimo taisyklė abiejose pusėse ta pati: spausdinimo sritis yra plikas intervalas tik tada, jei kiekvienas segmentas taip išsianalizuoja, kitaip tai formulė ir keliauja pažodžiui. PrintArea_FormulaDefinitionSurvivesRoundTrip apima įvardytą bazę, lapu kvalifikuotą bazę ir sąjungą per du įrašymo ir atidarymo ciklus. Kaip spausdinimo sritys sąveikauja su puslapio nustatymais ir likusiu spausdinimo modeliu, aptarta straipsnyje apie lapo apsaugą, puslapio nustatymus ir spausdinimą
Kaip rasti, kurią taisyklę Excel peikia?
Pradėkite nuo prielaidos, kad jūsų pačių tikrintuvas klysta, nes jis praėjo. Open XML SDK tikrintuvas įvardys schemos pažeidimą, tokį kaip trūkstamas fontId, kartu su dalimi ir XPath, o po juo esantis pakavimo sluoksnis apskritai atsisako atidaryti paketą su pasikartojančiais turinio tipo įrašais, tad paleiskite jį pirmiausia. Kai jis tyli, o Excel vis tiek taiso, dalinkite paketą pusiau: išarchyvuokite, ištrinkite dalį, jos ryšį ir jos Override, vėl suarchyvuokite ir atidarykite iš naujo, kiekvieną kartą perpus mažindami kandidatų rinkinį, kol pranešimas dingsta. Trys čia aprašyti defektai iškrito būtent tokia tvarka, ir nė vieno jų nebūtų matyti taisytame faile, kurį Excel siūlo įrašyti, nes taisymas tyliai išmeta arba pernumeruoja nusižengusius įrašus. v2.382.5 pataisos ribas verta išdėstyti lygiai taip pat tiesiai. Dublikatų šalinimas yra „laimi pirmasis" su modeliu priekyje, tad jei šaltinio paketas deklaravo kitą turinio tipą daliai, kurią generuoja ir modelis, modelio deklaracija laimi, o šaltinio atmetama — tai teisinga toms dalims, kurias HotXLS atkuria iš naujo, ir nėra bendras suliejimas. verify_opc_uniqueness tikrina tik unikalumą; jis nevaliduoja schemų, tad būsimas privalomas atributas vis tiek turėtų pasirodyti per Excel arba schemos tikrintuvą. O papildomas TXMLReader ėjimas per sugeneruotą turinio tipų srautą vyksta kiekvieną įrašymą su įjungtu PreserveUnsupportedParts — nedidelė kaina prieš srautą, kuris retai kada viršija kelis kilobaitus. Tam viskam atsiradus, tiek Win32, tiek Win64 paskolos šablono versijos Excel atsidaro be pranešimo, perskaičiuoja visas 4805 patikrintas formules be nė vieno nesutapimo ir nurodo tokią pačią spausdinimo sritį kaip originalas
Jei XLSX rašote patys iš Delphi, kontrolinis sąrašas trumpas: išveskite kiekvieną schemos pažymėtą privalomą atributą nepriklausomai nuo jo reikšmės, paskelbkite kiekvieną dalies pavadinimą vieną kartą ir laikykite vieną panaudotų identifikatorių sąrašą kiekvienai ryšių daliai per visus ją liečiančius rašytojus. Jei norite, kad tas sąrašas jau egzistuotų ir būtų tikrinamas su Excel, o ne vien su jūsų pačių skaitytuvu, čia aprašytas paketo rašytojas platinamas su HotXLS Delphi skaičiuoklės komponentu, kartu su nepermatomų dalių keliu pirmyn ir atgal, dėl kurio ta siūlė ir buvo verta saugoti