Skip to content

Commit

Permalink
Merge pull request #31 from nhz2/main
Browse files Browse the repository at this point in the history
Remove `unzip` and `zip` and use ZipArchives.jl instead
  • Loading branch information
matthijscox-asml authored May 8, 2024
2 parents e4d3f90 + 2a195f3 commit 64435fe
Show file tree
Hide file tree
Showing 84 changed files with 126 additions and 308 deletions.
5 changes: 3 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
name = "PPTX"
uuid = "14a86994-10a4-4a7d-b9ad-ef6f3b1fac6a"
authors = ["Xander de Vries", "Matthijs Cox"]
version = "0.6.7"
version = "0.7.0"

[deps]
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
DefaultApplication = "3f0dd361-4fe0-5fc6-8523-80b14ec94d85"
EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
XMLDict = "228000da-037f-5747-90a9-8195ccbf91a5"
ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c"

[compat]
DataStructures = "0.18"
Expand All @@ -21,6 +21,7 @@ FileIO = "1"
ImageIO = "0.6.1"
Tables = "1"
XMLDict = "0.4"
ZipArchives = "2"
julia = "1.6"

[extras]
Expand Down
5 changes: 3 additions & 2 deletions src/PPTX.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ module PPTX
using XMLDict
using EzXML
using DataStructures
using ZipArchives:
ZipBufferReader, ZipWriter, zip_commitfile, zip_newfile, zip_nentries,
zip_name, zip_name_collision, zip_isdir, zip_readentry, zip_iscompressed

import Tables
import Tables: columns, columnnames, rows

import Pkg.PlatformEngines: exe7z

export Presentation, Slide, TextBox, Picture, Table

include("AbstractShape.jl")
Expand Down
15 changes: 11 additions & 4 deletions src/Picture.jl
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ has_rid(s::Picture) = true
function _show_string(p::Picture, compact::Bool)
show_string = "Picture"
if !compact
show_string *= "\n source is \"$(p.source)\""
show_string *= "\n source is $(repr(p.source))"
show_string *= "\n offset_x is $(p.offset_x) EMUs"
show_string *= "\n offset_y is $(p.offset_y) EMUs"
show_string *= "\n size_x is $(p.size_x) EMUs"
Expand Down Expand Up @@ -139,9 +139,16 @@ function relationship_xml(p::Picture)
)
end

function copy_picture(p::Picture)
if !isfile("./media/$(filename(p))")
return cp(p.source, "./media/$(filename(p))")
function copy_picture(w::ZipWriter, p::Picture)
dest_path = "ppt/media/$(filename(p))"
# save any file being written so zip_name_collision is correct.
zip_commitfile(w)
if !zip_name_collision(w, dest_path)
open(p.source) do io
zip_newfile(w, dest_path)
write(w, io)
zip_commitfile(w)
end
end
end

Expand Down
4 changes: 2 additions & 2 deletions src/Presentation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ function make_presentation(p::Presentation)
return xml_pres
end

function update_presentation_state!(p::Presentation, ppt_dir=".")
doc = readxml(joinpath(ppt_dir, "presentation.xml"))
function update_presentation_state!(p::Presentation, template::ZipBufferReader)
doc = EzXML.parsexml(zip_readentry(template, "ppt/presentation.xml"))
r = root(doc)
n = findfirst("//p:sldSz", r)
cx, cy = n["cx"], n["cy"]
Expand Down
2 changes: 1 addition & 1 deletion src/Tables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ julia> df = DataFrame(a = [1,2], b = [3,4], c = [5,6])
julia> t = Table(content=df, size_x=30)
Table
content isa DataFrame
content isa DataFrames.DataFrame
offset_x is 1800000 EMUs
offset_y is 1800000 EMUs
size_x is 1080000 EMUs
Expand Down
172 changes: 74 additions & 98 deletions src/write.jl
Original file line number Diff line number Diff line change
@@ -1,76 +1,85 @@
import DefaultApplication

function write_presentation!(p::Presentation)
rm("./presentation.xml")
function write_presentation!(w::ZipWriter, p::Presentation)
xml = make_presentation(p)
doc = xml_document(xml)
return write("./presentation.xml", doc)
zip_newfile(w, "ppt/presentation.xml"; compress=true)
print(w, doc)
zip_commitfile(w)
end

function write_relationships!(p::Presentation)
rm("./_rels/presentation.xml.rels")
function write_relationships!(w::ZipWriter, p::Presentation)
xml = make_relationships(p)
doc = xml_document(xml)
return write("./_rels/presentation.xml.rels", doc)
zip_newfile(w, "ppt/_rels/presentation.xml.rels"; compress=true)
print(w, doc)
zip_commitfile(w)
end

function write_slides!(p::Presentation)
if isdir("slides")
function write_slides!(w::ZipWriter, p::Presentation, template::ZipBufferReader)
if zip_isdir(template, "ppt/slides")
error("input template pptx already contains slides, please use an empty template")
end
mkdir("slides")
mkdir("slides/_rels")
for (idx, slide) in enumerate(slides(p))
xml = make_slide(slide)
doc::EzXML.Document = xml_document(xml)
add_title_shape!(doc, slide)
write("./slides/slide$idx.xml", doc)
add_title_shape!(doc, slide, template)
zip_newfile(w, "ppt/slides/slide$idx.xml"; compress=true)
print(w, doc)
zip_commitfile(w)
xml = make_slide_relationships(slide)
doc = xml_document(xml)
write("./slides/_rels/slide$idx.xml.rels", doc)
zip_newfile(w, "ppt/slides/_rels/slide$idx.xml.rels"; compress=true)
print(w, doc)
zip_commitfile(w)
end
end

function add_title_shape!(doc::EzXML.Document, slide::Slide, unzipped_ppt_dir::String=".")
function add_title_shape!(doc::EzXML.Document, slide::Slide, template::ZipBufferReader)
# xpath to find something with an unregistered namespace
spTree = findfirst("//*[local-name()='p:spTree']", root(doc))
title_shape_node = PPTX.get_title_shape_node(slide, unzipped_ppt_dir)
layout_path = "ppt/slideLayouts/slideLayout$(slide.layout).xml"
layout_doc = EzXML.parsexml(zip_readentry(template, layout_path))
title_shape_node = PPTX.get_title_shape_node(layout_doc)
if !isnothing(title_shape_node)
PPTX.update_xml_title!(title_shape_node, slide.title)
new_id = maximum(get_shape_ids(doc))+1
update_shape_id!(title_shape_node, new_id)
unlink!(title_shape_node)
link!(spTree, title_shape_node)
end
return nothing
nothing
end

function write_shapes!(pres::Presentation)
if !isdir("media")
mkdir("media")
end
function write_shapes!(w::ZipWriter, pres::Presentation)
for slide in slides(pres)
for shape in shapes(slide)
if shape isa Picture
copy_picture(shape)
copy_picture(w::ZipWriter, shape)
end
end
end
end

function update_table_style!(unzipped_ppt_dir::String=".")
function update_table_style!(w::ZipWriter, template::ZipBufferReader)
# minimally we want at least one table style
if has_empty_table_list(unzipped_ppt_dir)
table_style_path = "ppt/tableStyles.xml"
table_style_doc = EzXML.parsexml(zip_readentry(template, table_style_path))
if has_empty_table_list(table_style_doc)
table_style_filename = "tableStyles.xml"
default_table_style_file = joinpath(TEMPLATE_DIR, table_style_filename)
destination_table_style_file = joinpath(unzipped_ppt_dir, table_style_filename)
cp(default_table_style_file, destination_table_style_file; force=true)
open(default_table_style_file) do io
zip_newfile(w, table_style_path; compress=true)
write(w, io)
zip_commitfile(w)
end
end
nothing
end

function add_contenttypes!()
path = joinpath("..", "[Content_Types].xml")
doc = readxml(path)
function add_contenttypes!(w::ZipWriter, template::ZipBufferReader)
path = "[Content_Types].xml"
doc = EzXML.parsexml(zip_readentry(template, path))
r = root(doc)
extension_contenttypes = (
("emf", "image/x-emf"),
Expand All @@ -88,10 +97,9 @@ function add_contenttypes!()
isnothing(findfirst(x -> (x.name == "Default" && x["Extension"] == ext), elements(r))) || continue
addelement!(r, "Default Extension=\"$ext\" ContentType=\"$ct\"")
end
chmod(path, 0o644)
open(path, "w") do io
prettyprint(io, doc)
end
zip_newfile(w, path; compress=true)
prettyprint(w, doc)
zip_commitfile(w)
end

"""
Expand Down Expand Up @@ -133,19 +141,18 @@ function Base.write(
p::Presentation;
overwrite::Bool=false,
open_ppt::Bool=true,
template_path::String=joinpath(TEMPLATE_DIR, "no-slides"),
template_path::String=joinpath(TEMPLATE_DIR, "no-slides.pptx"),
)

template_path = abspath(template_path)
template_name = splitpath(template_path)[end]
template_isdir = isdir(template_path)
template_isfile = isfile(template_path)

if !template_isdir && !template_isfile
if !template_isfile
error(
"No file found at template path: $template_path",
"No file found at template path: $(repr(template_path))",
)
end
template_reader = ZipBufferReader(read(template_path))

if !endswith(filepath, ".pptx")
filepath *= ".pptx"
Expand All @@ -154,80 +161,49 @@ function Base.write(
filepath = abspath(filepath)
filedir, filename = splitdir(filepath)

if !isdir(filedir)
mkdir(filedir)
end
mkpath(filedir)

if isfile(filepath)
if overwrite
rm(filepath)
else
error(
"File \"$filepath\" already exists use \"overwrite = true\" or a different name to proceed",
)
end
if !overwrite && isfile(filepath)
error(
"File $(repr(filepath)) already exists use \"overwrite = true\" or a different name to proceed",
)
end

mktempdir() do tmpdir
cd(tmpdir) do
cp(template_path, template_name)
unzipped_dir = template_name
if template_isfile
unzip(template_name)
unzipped_dir = first(splitext(template_name)) # remove .pptx
mktemp(filedir) do temp_path, temp_out
ZipWriter(temp_out; own_io=true) do w
update_presentation_state!(p, template_reader)
write_relationships!(w, p)
write_presentation!(w, p)
write_slides!(w, p, template_reader)
write_shapes!(w, p)
update_table_style!(w, template_reader)
add_contenttypes!(w, template_reader)
# copy over any files from the template
# but don't overwrite any files in w
for i in zip_nentries(template_reader):-1:1
local name = zip_name(template_reader, i)
if !endswith(name,"/")
if !zip_name_collision(w, name)
local compress = zip_iscompressed(template_reader, i)
zip_data = zip_readentry(template_reader, i)
zip_newfile(w, name; compress)
write(w, zip_data)
zip_commitfile(w)
end
end
end
ppt_dir = joinpath(unzipped_dir, "ppt")
cd(ppt_dir) do
update_presentation_state!(p)
write_relationships!(p)
write_presentation!(p)
write_slides!(p)
write_shapes!(p)
update_table_style!()
add_contenttypes!()
end
zip(unzipped_dir, filename)
cp(filename, filepath)
end
mv(temp_path, filepath; force=overwrite)
end

if open_ppt
try
DefaultApplication.open(filepath)
catch err
@warn "Could not open file $filepath"
@warn "Could not open file $(repr(filepath))"
bt = backtrace()
print(sprint(showerror, err, bt))
end
end
return nothing
end

# unzips file as folder into current folder
function unzip(path::String)
output = split(path, ".pptx")[begin]
run_silent_pipeline(`$(exe7z()) x $path -o$output`)
end

# Turns folder into zipped file
function zip(folder::String, filename::String)
zip_ext_filename = split(filename, ".")[begin] * ".zip"
origin = pwd()
cd(folder) do
for f in readdir(".")
run_silent_pipeline(`$(exe7z()) a $zip_ext_filename $f`)
end
mv(zip_ext_filename, joinpath(origin, filename))
end
return nothing
end

# silent, unless we error
function run_silent_pipeline(command)
standard_output = Pipe() # capture output, so it doesn't pollute the REPL
try
run(pipeline(command, stdout=standard_output))
catch e
println(standard_output)
rethrow(e)
end
end
19 changes: 2 additions & 17 deletions src/xml_ppt_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,11 @@ function main_attributes()::Vector{OrderedDict}
]
end

function get_slide_layout_path(layout::Int, unzipped_ppt_dir::String=".")
filename = "slideLayout$layout.xml"
path = joinpath(unzipped_ppt_dir, "slideLayouts", filename)
return abspath(path)
end

function xpath_to_find_sp_type(type_name::String)
return "//p:spTree/p:sp/p:nvSpPr[1]/p:nvPr[1]/p:ph[1][@type=\"$type_name\"][1]/ancestor::p:sp[1]"
end

function get_title_shape_node(s::Slide, unzipped_ppt_dir::String=".")
get_title_shape_node(s.layout, unzipped_ppt_dir)
end

function get_title_shape_node(layout::Int, unzipped_ppt_dir::String=".")
layout_path = get_slide_layout_path(layout, unzipped_ppt_dir)
layout_doc = EzXML.readxml(layout_path)

function get_title_shape_node(layout_doc::EzXML.Document)
# the xpath way to find things
# note: on layout2 and forth it's type="title", layout1 uses type="ctrTitle"
xpath = xpath_to_find_sp_type("title")
Expand Down Expand Up @@ -87,9 +74,7 @@ function update_shape_id!(sp_node::EzXML.Node, id::Int)
return nothing
end

function has_empty_table_list(unzipped_ppt_dir::String=".")
table_style_path = abspath(joinpath(unzipped_ppt_dir, "tableStyles.xml"))
table_style_doc = EzXML.readxml(table_style_path)
function has_empty_table_list(table_style_doc::EzXML.Document)
tblStyles = findall("//a:tblStyleLst/a:tblStyle", root(table_style_doc))
return isnothing(tblStyles) || isempty(tblStyles)
end
2 changes: 0 additions & 2 deletions templates/no-slides-dark/[Content_Types].xml

This file was deleted.

Loading

2 comments on commit 64435fe

@matthijscox-asml
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register

Release notes:

Backend change: switched to ZipArchives.jl

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/106383

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.7.0 -m "<description of version>" 64435fe29a9e50c366be5f8067f8e6b4199a8f30
git push origin v0.7.0

Please sign in to comment.