From 11e74e297ccb6ba01aab9177d195585ad956326f Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 26 May 2026 07:23:09 +0100 Subject: [PATCH 1/6] Small update to TagBot.yml --- .github/workflows/TagBot.yml | 3 ++- src/write.jl | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index 108b1dc1..5c9e3042 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -1,4 +1,3 @@ - name: TagBot on: issue_comment: @@ -15,3 +14,5 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} changelog: true + dispatch: docs + diff --git a/src/write.jl b/src/write.jl index aff5346c..96a6f8c6 100644 --- a/src/write.jl +++ b/src/write.jl @@ -20,7 +20,7 @@ function savexlsx(f::XLSXFile) if f.source == "blank.xlsx" throw(XLSXError("Can't save to a blank `XLSXFile` instance. Use `writexlsx` instead to specify a file name.")) elseif f.is_xltx - throw(XLSXError("Can't save to a back to an Excel template file. Use `writexlsx` instead to specify a file name.")) + throw(XLSXError("Can't save back to an Excel template file. Use `writexlsx` instead to specify a file name.")) end end return writexlsx(f.source, f; overwrite=true) From 3fa3258e1835668dff82a773563b033137ca8a64 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 26 May 2026 14:10:44 +0100 Subject: [PATCH 2/6] Simple initial fix for #401. --- src/read.jl | 31 ++++++++++++++++++++++++++++--- src/types.jl | 10 ++++++++-- src/write.jl | 2 +- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/read.jl b/src/read.jl index af7b9ad9..8ef89476 100644 --- a/src/read.jl +++ b/src/read.jl @@ -476,9 +476,10 @@ function openxlsx(f::F, source::Union{AbstractString,IO}; finally if _write - if xf.is_xltx + if xf.template_type != NotATemplate if isa(xf.source, AbstractString) - xf.source = splitext(xf.source)[1] * ".xlsx" + ext = xf.template_type == XLTMTemplate ? ".xlsm" : ".xlsx" + xf.source = splitext(xf.source)[1] * ext end end writexlsx(xf.source, xf, overwrite=true) @@ -684,6 +685,10 @@ function ensure_workbook_is_xlsx!(xf::XLSXFile) if ctype == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" return nothing end + # XLSM — treat as normal workbook, just ignore macros + if ctype == "application/vnd.ms-excel.sheet.macroEnabled.main+xml" + return nothing + end # Template .xltx → convert to .xlsx if ctype == "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml" @@ -703,7 +708,27 @@ function ensure_workbook_is_xlsx!(xf::XLSXFile) end end - xf.is_xltx = true # mark the file as originally being a template, so that we can rename it to .xlsx on save + xf.template_type = XLTXTemplate # mark the file as originally being a an .xltx template. + return nothing + end + + # Template .xltm → convert to .xlsm (macro-enabled workbook) + if ctype == "application/vnd.ms-excel.template.macroEnabled.main+xml" + + if !isnothing(workbook_override) + workbook_override["ContentType"] = + "application/vnd.ms-excel.sheet.macroEnabled.main+xml" + else + for child in XML.children(root) + if localname(child) == "Default" && + child["Extension"] == "xml" + child["ContentType"] = + "application/vnd.ms-excel.sheet.macroEnabled.main+xml" + end + end + end + + xf.template_type = XLTMTemplate # mark the file as originally being a an .xltx template return nothing end diff --git a/src/types.jl b/src/types.jl index 9a17f735..d2a7a0ca 100644 --- a/src/types.jl +++ b/src/types.jl @@ -558,6 +558,12 @@ mutable struct Workbook styles_xroot::Union{XML.Node, Nothing} end +@enum TemplateType begin + NotATemplate + XLTXTemplate # .xltx → save as .xlsx + XLTMTemplate # .xltm → save as .xlsm +end + """ `XLSXFile` represents a reference to an Excel file. @@ -584,12 +590,12 @@ mutable struct XLSXFile <: MSOfficePackage workbook::Workbook relationships::Vector{Relationship} # contains package level relationships is_writable::Bool # indicates whether this XLSX file can be edited - is_xltx::Bool # indicates whether this XLSX file was read from template (xltx) file + template_type::TemplateType # indicates whether this XLSX file was read from template file uuid_rng::Random.Xoshiro # rng used to generate uuids for this file function XLSXFile(source::Union{AbstractString, IO}, use_cache::Bool, is_writable::Bool) check_for_xlsx_file_format(source) - xl = new(source, use_cache, Dict{String, Bool}(), Dict{String, XML.Node}(), Dict{String, String}(), Dict{String, Vector{UInt8}}(), EmptyWorkbook(), Vector{Relationship}(), is_writable, false, Random.Xoshiro(2468)) + xl = new(source, use_cache, Dict{String, Bool}(), Dict{String, XML.Node}(), Dict{String, String}(), Dict{String, Vector{UInt8}}(), EmptyWorkbook(), Vector{Relationship}(), is_writable, NotATemplate, Random.Xoshiro(2468)) xl.workbook.package = xl return xl end diff --git a/src/write.jl b/src/write.jl index 96a6f8c6..bf1a03b8 100644 --- a/src/write.jl +++ b/src/write.jl @@ -19,7 +19,7 @@ function savexlsx(f::XLSXFile) if isa(f.source, AbstractString) if f.source == "blank.xlsx" throw(XLSXError("Can't save to a blank `XLSXFile` instance. Use `writexlsx` instead to specify a file name.")) - elseif f.is_xltx + elseif f.template_type != NotATemplate throw(XLSXError("Can't save back to an Excel template file. Use `writexlsx` instead to specify a file name.")) end end From 33ff375b4758a8470088c736a2a94042da335aae Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Thu, 28 May 2026 16:04:43 +0100 Subject: [PATCH 3/6] Now with tests for #401 and fix for #403 --- src/read.jl | 99 ++++++++++++---------------------- test/data/UTF-16.xlsx | Bin 0 -> 27853 bytes test/data/macro-enabled.xlsm | Bin 0 -> 8832 bytes test/data/macro-enabled2.xltm | Bin 0 -> 8860 bytes test/runtests.jl | 68 ++++++++++++++++++----- 5 files changed, 90 insertions(+), 77 deletions(-) create mode 100644 test/data/UTF-16.xlsx create mode 100644 test/data/macro-enabled.xlsm create mode 100644 test/data/macro-enabled2.xltm diff --git a/src/read.jl b/src/read.jl index 8ef89476..dce158d2 100644 --- a/src/read.jl +++ b/src/read.jl @@ -658,85 +658,52 @@ function ensure_workbook_is_xlsx!(xf::XLSXFile) workbook_override = nothing default_xml_type = nothing - # Scan once, collecting both possible sources of the workbook content type for child in XML.children(root) name = localname(child) - - if name == "Override" && - lowercase(child["PartName"]) == "/xl/workbook.xml" + if name == "Override" && lowercase(child["PartName"]) == "/xl/workbook.xml" workbook_override = child - - elseif name == "Default" && - child["Extension"] == "xml" + elseif name == "Default" && child["Extension"] == "xml" default_xml_type = child["ContentType"] end end - # Excel-compatible fallback: - # 1. Prefer the Override - # 2. Fall back to the Default - # 3. Only error if both missing ctype = !isnothing(workbook_override) ? workbook_override["ContentType"] : !isnothing(default_xml_type) ? default_xml_type : throw(XLSXError("Malformed XLSX: workbook.xml content type not found.")) - # Normal .xlsx - if ctype == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" - return nothing - end - # XLSM — treat as normal workbook, just ignore macros - if ctype == "application/vnd.ms-excel.sheet.macroEnabled.main+xml" - return nothing - end + # Passthrough types — no conversion needed + ctype == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" && return nothing + ctype == "application/vnd.ms-excel.sheet.macroEnabled.main+xml" && return nothing - # Template .xltx → convert to .xlsx - if ctype == "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml" + # Template types — convert to their workbook equivalent + template_conversions = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml" => + ("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", XLTXTemplate), + "application/vnd.ms-excel.template.macroEnabled.main+xml" => + ("application/vnd.ms-excel.sheet.macroEnabled.main+xml", XLTMTemplate), + ) - if !isnothing(workbook_override) - # Update the Override entry - workbook_override["ContentType"] = - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" - else - # Update the Default entry instead - for child in XML.children(root) - if localname(child) == "Default" && - child["Extension"] == "xml" - child["ContentType"] = - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" - end - end - end - - xf.template_type = XLTXTemplate # mark the file as originally being a an .xltx template. - return nothing - end - - # Template .xltm → convert to .xlsm (macro-enabled workbook) - if ctype == "application/vnd.ms-excel.template.macroEnabled.main+xml" + for (template_ctype, (target_ctype, template_type)) in template_conversions + ctype == template_ctype || continue if !isnothing(workbook_override) - workbook_override["ContentType"] = - "application/vnd.ms-excel.sheet.macroEnabled.main+xml" + workbook_override["ContentType"] = target_ctype else for child in XML.children(root) - if localname(child) == "Default" && - child["Extension"] == "xml" - child["ContentType"] = - "application/vnd.ms-excel.sheet.macroEnabled.main+xml" + if localname(child) == "Default" && child["Extension"] == "xml" + child["ContentType"] = target_ctype end end end - xf.template_type = XLTMTemplate # mark the file as originally being a an .xltx template + xf.template_type = template_type return nothing end - # Unknown workbook type throw(XLSXError("Unknown workbook content type: $ctype")) end - # See section 12.2 - Package Structure function check_minimum_requirements(xf::XLSXFile) mandatory_files = ["_rels/.rels", @@ -973,7 +940,7 @@ function stream_files(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int, ch # ignore xl/calcChain.xml in any case (#31) if f != "xl/calcChain.xml" - if pass==1 && (endswith(f, ".xml") || endswith(f, ".rels")) + if pass==1 && (!startswith(f, "customXml") && (endswith(f, ".xml") || endswith(f, ".rels"))) # Identify usable xml files in XLSXFile internal_xml_file_add!(xf, f) end @@ -998,14 +965,15 @@ function load_files!(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int) # Filter files based on pass BEFORE parallel processing filtered_files = Channel{String}(1 << 8) do out for file in all_files + is_sst = occursin(r"^xl/sharedStrings\.xml$", file) + is_worksheet = occursin(r"^xl/worksheets/[^/]+\.xml$", file) should_process = if pass == 1 - !occursin(r"^xl/worksheets/[^/]+\.xml$|^xl/sharedStrings\.xml$", file) + !is_sst && !is_worksheet elseif pass == 2 - occursin(r"xl/sharedStrings\.xml", file) + is_sst else # pass == 3 - occursin(r"^xl/worksheets/[^/]+\.xml$", file) - end - + is_worksheet + end if should_process put!(out, file) end @@ -1020,11 +988,11 @@ function load_files!(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int) end if !isnothing(file.raw) if xf.is_writable || pass==2 - if occursin("xl/sharedStrings.xml", file.name) + if occursin(r"^xl/sharedStrings\.xml$", file.name) if has_sst(wb) sst_load!(wb) end - elseif xf.use_cache_for_sheet_data && !occursin("xl/sharedStrings.xml", file.name) + elseif xf.use_cache_for_sheet_data && !occursin(r"^xl/sharedStrings\.xml$", file.name) rid = get_relationship_id_by_target(wb, file.name) for sheet in wb.sheets if sheet.relationship_id == rid @@ -1062,10 +1030,12 @@ function process_file(zip_io::ZipArchives.ZipReader, filename::String) try bytes = ZipArchives.zip_readentry(zip_io, filename) - if (endswith(filename, ".xml") || endswith(filename, ".rels")) - if occursin(r"^xl/worksheets/[^/]+\.xml$|^xl/sharedStrings\.xml$", filename) + if !startswith(filename, "customXml") && (endswith(filename, ".xml") || endswith(filename, ".rels")) +# if (endswith(filename, ".xml") || endswith(filename, ".rels")) + is_sst = occursin(r"^xl/sharedStrings\.xml$", filename) + if is_sst || occursin(r"^xl/worksheets/[^/]+\.xml$", filename) strip_bom_and_lf!(bytes) - skipnode = filename == "xl/sharedStrings.xml" ? "sst" : "sheetData" + skipnode = is_sst ? "sst" : "sheetData" f, s = skipNode(XML.Raw(bytes), skipnode) # and elements can be very numerous in large files, so split out and keep as Raw XML data for speed node = XML.Node(XML.Raw(f)) raw = XML.Raw(s) @@ -1098,8 +1068,9 @@ function internal_xml_file_read(xf::XLSXFile, zip_io::Union{Nothing,ZipArchives. try bytes = ZipArchives.zip_readentry(zip_io, filename) strip_bom_and_lf!(bytes) - if occursin(r"^xl/worksheets/[^/]+\.xml$|^xl/sharedStrings\.xml$", filename) - skipnode = filename == "xl/sharedStrings.xml" ? "sst" : "sheetData" + is_sst = occursin(r"^xl/sharedStrings\.xml$", filename) + if is_sst || occursin(r"^xl/worksheets/[^/]+\.xml$", filename) + skipnode = is_sst ? "sst" : "sheetData" f, _ = skipNode(XML.Raw(bytes), skipnode) # and elements can be very numerous in large files, so split out and keep as Raw XML data for speed xf.data[filename] = XML.Node(XML.Raw(f)) else diff --git a/test/data/UTF-16.xlsx b/test/data/UTF-16.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f9c04492b73213be70153cf78d2542b1b0ae3f1e GIT binary patch literal 27853 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 literal 0 HcmV?d00001 diff --git a/test/data/macro-enabled.xlsm b/test/data/macro-enabled.xlsm new file mode 100644 index 0000000000000000000000000000000000000000..42997a6d25064f3302626066cfbed98a088fc5d5 GIT binary patch literal 8832 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 literal 0 HcmV?d00001 diff --git a/test/data/macro-enabled2.xltm b/test/data/macro-enabled2.xltm new file mode 100644 index 0000000000000000000000000000000000000000..eac603d4f0f53c9ccdd42febce370974525002f3 GIT binary patch literal 8860 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 T?JfX-2KxlUlpE>E&tLxs^KxtH literal 0 HcmV?d00001 diff --git a/test/runtests.jl b/test/runtests.jl index 196453a3..c2efcb9d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -67,6 +67,61 @@ src_data_directory = joinpath(dirname(pathof(XLSX)), "data") end + # Issue #293 + @testset "read .xltx file" begin + xf = XLSX.openxlsx(joinpath(data_directory, "Template File.xltx"); mode="rw") + s=xf[1] + @test s["P5"] == 5 + @test XLSX.getFormula(s, "B5") == "=RANDBETWEEN(0,100)" + @test_throws XLSX.XLSXError XLSX.savexlsx(xf) # can't use save on a template file. + + XLSX.openxlsx(joinpath(data_directory, "Template File.xltx"); mode="rw") do xf + s=xf[1] + @test s["P5"] == 5 + @test XLSX.getFormula(s, "B5") == "=RANDBETWEEN(0,100)" + end + @test isfile(joinpath(data_directory, "Template File.xlsx")) + isfile(joinpath(data_directory, "Template File.xlsx")) && rm(joinpath(data_directory, "Template File.xlsx")) + end + + # Issue #402 + @testset "macro enabled files" begin + mf = XLSX.openxlsx(joinpath(data_directory, "macro-enabled.xlsm"); mode="rw") + @test mf[1]["A1"] == "hello" + XLSX.writexlsx("mytest.xlsm", mf; overwrite=true) + mf = XLSX.openxlsx("mytest.xlsm"; mode="rw") + @test mf[1]["A1"] == "hello" + isfile("mytest.xlsm") && rm("mytest.xlsm") + + mf = XLSX.openxlsx(joinpath(data_directory, "macro-enabled2.xltm"); mode="rw") + @test mf[1]["A1"] == "hello" + @test_throws XLSX.XLSXError XLSX.savexlsx(mf) # can't save a template file + + XLSX.openxlsx(joinpath(data_directory, "macro-enabled2.xltm"); mode="rw") do mf + @test mf[1]["A1"] == "hello" + end + @test isfile(joinpath(data_directory, "macro-enabled2.xlsm")) + mf = XLSX.openxlsx(joinpath(data_directory, "macro-enabled2.xlsm"); mode="rw") + @test mf[1]["A1"] == "hello" + isfile(joinpath(data_directory, "macro-enabled2.xlsm")) && rm(joinpath(data_directory, "macro-enabled2.xlsm")) + + end + + # Issue #403 + @testset "UTF-16 customXml" begin + try + xf1 = XLSX.openxlsx(joinpath(data_directory, "UTF-16.xlsx"); mode="rw") + XLSX.writexlsx("UTF-16_test.xlsx", xf1; overwrite=true) + xf2 = XLSX.openxlsx("UTF-16_test.xlsx"; mode="rw") + @test xf1[1]["E3"] == xf2[1]["E3"] + @test xf1[1]["R99"] == xf2[1]["R99"] + catch e + @test false + end + + isfile("UTF-16_test.xlsx") && rm("UTF-16_test.xlsx") + + end @testset "Read password protected file error" begin @test_throws XLSX.XLSXError XLSX.readxlsx(joinpath(data_directory, "password.xlsx")) # password for this file is simply "password" try @@ -86,21 +141,8 @@ src_data_directory = joinpath(dirname(pathof(XLSX)), "data") catch e @test occursin("is not a valid XLSX file", "$e") end -# @test_throws XLSX.XLSXError XLSX.readxlsx(joinpath(data_directory, "Template File.xltx")) -# try -# XLSX.readxlsx(joinpath(data_directory, "Template File.xltx")) -# @test false # didn't throw exception -# catch e -# @test occursin("does not support Excel template files", "$e") -# end end - @testset "read .xltx file" begin - xf = XLSX.readxlsx(joinpath(data_directory, "Template File.xltx")) - s=xf[1] - @test s["P5"] == 5 - @test XLSX.getFormula(s, "B5") == "=RANDBETWEEN(0,100)" - end @testset "missing file or bad `mode`" begin @test_throws XLSX.XLSXError XLSX.openxlsx("noSuchFile.xlsx") @test_throws XLSX.XLSXError XLSX.openxlsx(joinpath(data_directory, "Book1.xlsx"); mode="tg") From 4e901d00f3818b6e9da0eb022ad729736bfdf433 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Thu, 28 May 2026 16:10:22 +0100 Subject: [PATCH 4/6] Update changelog and Project.toml for v0.11.10 --- CHANGELOG.md | 4 ++++ Project.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7af00b6..77204dfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +## [v0.11.10](https://github.com/JuliaData/XLSX.jl/tree/v0.11.10) - 2026-05-28 +- support macro-enabled files ([#401](https://github.com/JuliaData/XLSX.jl/issues/401)) +- support pass-through of customXml files (again). ([#403](https://github.com/JuliaData/XLSX.jl/issues/403)) + ## [v0.11.9](https://github.com/JuliaData/XLSX.jl/tree/v0.11.9) - 2026-05-26 - fix bug in `setFormula` when a function name occured inside a quoted string - fix issue [#395](https://github.com/JuliaData/XLSX.jl/issues/395)(@mathieu17g) diff --git a/Project.toml b/Project.toml index 0bb62074..d7bf04ca 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "XLSX" uuid = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" license = "MIT" -version = "0.11.9" +version = "0.11.10" authors = ["Felipe Noronha "] repo = "https://github.com/juliadata/XLSX.jl.git" From 109ef2ae4a62950518c49fe6d8be8a10746c57a2 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Thu, 28 May 2026 16:29:54 +0100 Subject: [PATCH 5/6] Add CONST list of files to pass through as binary. --- src/read.jl | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/read.jl b/src/read.jl index dce158d2..5736f78b 100644 --- a/src/read.jl +++ b/src/read.jl @@ -950,11 +950,16 @@ function stream_files(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int, ch end end +# list of filename prefixes to pass through as binary files. +const BINARY_PREFIXES = ["customXml"] + + # Read xml files in three passes # pass 1 - read all but worksheets and sharedStrings # pass 2 - only read sharedStrings (needed before worksheets) # pass 3 - only read worksheets -function load_files!(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int) +function load_files!(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int, + binary_prefixes::Vector{String}=BINARY_PREFIXES) (pass < 1 || pass > 3) && throw(XLSXError("Unknown pass to read files.")) wb = get_workbook(xf) @@ -1012,7 +1017,7 @@ function load_files!(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int) @sync for _ in 1:Threads.nthreads() Threads.@spawn begin for file in filtered_files - readfile = process_file(zip_io, file) + readfile = process_file(zip_io, file; binary_prefixes) put!(read_files, readfile) end end @@ -1022,29 +1027,31 @@ function load_files!(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int) wait(consumer) end -function process_file(zip_io::ZipArchives.ZipReader, filename::String) +function process_file(zip_io::ZipArchives.ZipReader, filename::String; + binary_prefixes::Vector{String}=BINARY_PREFIXES) - node=nothing - raw=nothing - bin=nothing + node = nothing + raw = nothing + bin = nothing + + is_binary_path = any(p -> startswith(filename, p), binary_prefixes) try bytes = ZipArchives.zip_readentry(zip_io, filename) - if !startswith(filename, "customXml") && (endswith(filename, ".xml") || endswith(filename, ".rels")) -# if (endswith(filename, ".xml") || endswith(filename, ".rels")) + if !is_binary_path && (endswith(filename, ".xml") || endswith(filename, ".rels")) is_sst = occursin(r"^xl/sharedStrings\.xml$", filename) if is_sst || occursin(r"^xl/worksheets/[^/]+\.xml$", filename) strip_bom_and_lf!(bytes) - skipnode = is_sst ? "sst" : "sheetData" - f, s = skipNode(XML.Raw(bytes), skipnode) # and elements can be very numerous in large files, so split out and keep as Raw XML data for speed + skipnode = is_sst ? "sst" : "sheetData" + f, s = skipNode(XML.Raw(bytes), skipnode) node = XML.Node(XML.Raw(f)) - raw = XML.Raw(s) + raw = XML.Raw(s) else strip_bom_and_lf!(bytes) node = XML.Node(XML.Raw(bytes)) end else - bin = bytes + bin = bytes end catch err throw(XLSXError("Failed to parse internal XML file `$filename`")) From 37187cd698b3eef752ff3db1d15f13dc507e23b3 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Thu, 28 May 2026 17:04:44 +0100 Subject: [PATCH 6/6] Add missing `binary_prefixes` kw. --- src/read.jl | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/read.jl b/src/read.jl index 5736f78b..bd43739e 100644 --- a/src/read.jl +++ b/src/read.jl @@ -933,14 +933,20 @@ function skipNode(r::XML.Raw, skipnode::String) # separate rows or ssts to speed return take!(new), take!(skipped) end -function stream_files(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int, channel_size::Int=1 << 8) - Channel{String}(channel_size) do out +# list of filename prefixes to pass through as binary files. +const BINARY_PREFIXES = ["customXml"] + +function stream_files(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int, + channel_size::Int=1 << 8, + binary_prefixes::Vector{String}=BINARY_PREFIXES) + + Channel{String}(channel_size) do out for f in ZipArchives.zip_names(zip_io) # ignore xl/calcChain.xml in any case (#31) if f != "xl/calcChain.xml" - if pass==1 && (!startswith(f, "customXml") && (endswith(f, ".xml") || endswith(f, ".rels"))) + if pass==1 && (!any(p -> startswith(f, p), binary_prefixes) && (endswith(f, ".xml") || endswith(f, ".rels"))) # Identify usable xml files in XLSXFile internal_xml_file_add!(xf, f) end @@ -950,10 +956,6 @@ function stream_files(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int, ch end end -# list of filename prefixes to pass through as binary files. -const BINARY_PREFIXES = ["customXml"] - - # Read xml files in three passes # pass 1 - read all but worksheets and sharedStrings # pass 2 - only read sharedStrings (needed before worksheets) @@ -965,7 +967,7 @@ function load_files!(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int, wb = get_workbook(xf) read_files = Channel{ReadFile}(1 << 8) - all_files = stream_files(xf, zip_io; pass) + all_files = stream_files(xf, zip_io; pass, binary_prefixes) # Filter files based on pass BEFORE parallel processing filtered_files = Channel{String}(1 << 8) do out