diff --git a/Project.toml b/Project.toml index 645d7b0..6359037 100644 --- a/Project.toml +++ b/Project.toml @@ -8,7 +8,9 @@ DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DefaultApplication = "3f0dd361-4fe0-5fc6-8523-80b14ec94d85" EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +GridLayoutBase = "3955a311-db13-416c-9275-1d80ed98e5e9" ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" +Observables = "510215fc-4207-5dde-b226-833fc4488ee2" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" XMLDict = "228000da-037f-5747-90a9-8195ccbf91a5" ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c" @@ -18,7 +20,9 @@ DataStructures = "0.18" DefaultApplication = "1" EzXML = "1" FileIO = "1" +GridLayoutBase = "0.11" ImageIO = "0.6.1" +Observables = "0.5" Tables = "1" XMLDict = "0.4" ZipArchives = "2" diff --git a/src/PPTX.jl b/src/PPTX.jl index 2db23e6..14880cf 100644 --- a/src/PPTX.jl +++ b/src/PPTX.jl @@ -10,8 +10,16 @@ using ZipArchives: import Tables import Tables: columns, columnnames, rows +import GridLayoutBase +import GridLayoutBase: GridLayout, LayoutObservables, BBox + +import Observables +import Observables: Observable + export Presentation, Slide, TextBox, Picture, Table +export ShapeLayout + include("AbstractShape.jl") include("constants.jl") include("TextBox.jl") @@ -22,5 +30,6 @@ include("Presentation.jl") include("xml_utils.jl") include("xml_ppt_utils.jl") include("write.jl") +include("layout.jl") end diff --git a/src/TextBox.jl b/src/TextBox.jl index cf44c2e..96759fd 100644 --- a/src/TextBox.jl +++ b/src/TextBox.jl @@ -59,10 +59,10 @@ TextBox """ struct TextBox <: AbstractShape content::TextBody - offset_x::Int # EMUs - offset_y::Int # EMUs - size_x::Int # EMUs - size_y::Int # EMUs + offset_x::Observable{Int} # EMUs + offset_y::Observable{Int} # EMUs + size_x::Observable{Int} # EMUs + size_y::Observable{Int} # EMUs hlink::Union{Nothing, Any} function TextBox( content::AbstractString, @@ -76,10 +76,10 @@ struct TextBox <: AbstractShape # input is in mm return new( TextBody(content, style), - Int(round(offset_x * _EMUS_PER_MM)), - Int(round(offset_y * _EMUS_PER_MM)), - Int(round(size_x * _EMUS_PER_MM)), - Int(round(size_y * _EMUS_PER_MM)), + Observable(Int(round(offset_x * _EMUS_PER_MM))), + Observable(Int(round(offset_y * _EMUS_PER_MM))), + Observable(Int(round(size_x * _EMUS_PER_MM))), + Observable(Int(round(size_y * _EMUS_PER_MM))), hlink ) end @@ -112,10 +112,10 @@ function _show_string(p::TextBox, compact::Bool) show_string = "TextBox" if !compact show_string *= "\n content is \"$(String(p.content))\"" - 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" - show_string *= "\n size_y is $(p.size_y) EMUs" + 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" + show_string *= "\n size_y is $(p.size_y[]) EMUs" end return show_string end @@ -145,8 +145,8 @@ function make_xml(t::TextBox, id::Int=1) nvSpPr = Dict("p:nvSpPr" => [cNvPr, cNvSpPr, nvPr]) - offset = Dict("a:off" => [Dict("x" => "$(t.offset_x)"), Dict("y" => "$(t.offset_y)")]) - extend = Dict("a:ext" => [Dict("cx" => "$(t.size_x)"), Dict("cy" => "$(t.size_y)")]) + offset = Dict("a:off" => [Dict("x" => "$(t.offset_x[])"), Dict("y" => "$(t.offset_y[])")]) + extend = Dict("a:ext" => [Dict("cx" => "$(t.size_x[])"), Dict("cy" => "$(t.size_y[])")]) spPr = Dict( "p:spPr" => [ diff --git a/src/layout.jl b/src/layout.jl new file mode 100644 index 0000000..9bf6c14 --- /dev/null +++ b/src/layout.jl @@ -0,0 +1,138 @@ + +# This struct exists for these reasons: +# 1) To add all shapes in the layout as a flat vector in Slide (so id is same as index) +# 2) To ensure that the only contents of the layout can be AbstractShapes +# 3) To allow us to override setindex! with an AbstractShape without risk of ambiguity +# 4) to provide some better default values for the GridLayout (especially the bbox, see below) + +# Main drawback is that we don't get all the utility functions defined for GridLayout for free since we must forward +# the calls. There are also a couple of functions which check x isa GridLayout internally which will not work +struct ShapeLayout + slide::Slide + layout::GridLayout + function ShapeLayout( + slide, + offset_x::Real, # millimeters + offset_y::Real, # millimeters + size_x::Real, # millimeters + size_y::Real; # millimeters + kwargs... + ) + offset_x_emu = offset_x * _EMUS_PER_MM + offset_y_emu = offset_y * _EMUS_PER_MM + size_x_emu = size_x * _EMUS_PER_MM + size_y_emu = size_y * _EMUS_PER_MM + + # Maybe better for users to see this in mm, but all other shapes display in EMU + suggestedbbox = BBox( + offset_x_emu, # Left + offset_x_emu + size_x_emu, # Right + -offset_y_emu - size_y_emu, # Bottom. Negative since PPTX counts y from the top + -offset_y_emu # Top. Negative since PPTX counts y from the top + ) + + new(slide, GridLayout(;halign=:left, valign=:top, kwargs..., bbox = suggestedbbox)) + end +end + +# keyword argument constructor +function ShapeLayout(slide::Slide; + offset_x::Real=20, # millimeters + offset_y::Real=50, # millimeters + size_x::Real=100, # millimeters + size_y::Real=40, # millimeters + kwargs... # Forwarded to GridLayout + ) + # Would really like to get the actual layout of the slide here so we can initialize the bounding boxes + ShapeLayout(slide, offset_x, offset_y, size_x, size_y; kwargs...) +end + +ShapeLayout(sl::ShapeLayout; kwargs...) = ShapeLayout(sl.slide; kwargs...) + +GridLayoutBase.layoutobservables(sl::ShapeLayout) = GridLayoutBase.layoutobservables(sl.layout) + +# Make sure that the only things we can add are Shapes and ShapeLayouts +Base.setindex!(sl::ShapeLayout, content::ShapeLayout, args...) = setindex!(sl.layout, content, args...) +function Base.setindex!(sl::ShapeLayout, content::AbstractShape, args...) + # Synopsis: Since shapes are stored as a flat vector outside of the layout we need to make sure it + # stays in sync with the layout in case we overwrite. + # We do this by just storing the index inside the ShapeLayoutObservables. If there is something + # at the position (given by args...) we just overwrite it instead of adding something new + current = GridLayoutBase.contents(Base.getindex(sl.layout, args...)) + slo = if isempty(current) + push!(sl.slide, content) + ShapeLayoutObservables(content, lastindex(sl.slide.shapes)) + else + shapeindex = only(current).index + sl.slide.shapes[shapeindex] = content + ShapeLayoutObservables(content, shapeindex) + end + setindex!(sl.layout, slo, args...) +end + +Base.getindex(sl::ShapeLayout, args...) = Base.getindex(sl.layout, args...) + +# This struct exists for these reasons: +# 1) the contents of a GridLayout must have a LayoutObservable +# 2) With the current design we need to remember which index we inserted the +# shape into slide.shapes since setindex! allows for overwriting + +# Users should not need to interact with it +struct ShapeLayoutObservables{T<:AbstractShape} + shape::T + index::Int + layoutobservables::LayoutObservables{GridLayout} +end + +# TODO: Need to implement for each shape +BBox(s::AbstractShape) = BBox(Float32, + s.offset_x[], # Left + s.offset_x[]+s.size_x[], # Right + -s.offset_y[] - s.size_y[], # Bottom. Negative since PPTX counts y from the top + -s.offset_y[] # Top. Negative since PPTX counts y from the top + ) + +# Don't need to add LayoutObservables since GridLayout has it +ShapeLayoutObservables(sl::ShapeLayout, args...) = sl +function ShapeLayoutObservables(shape::AbstractShape, shapeindex) + + # Just boilerplate to create a LayoutObservable + # We only use the computedbboxobservable from all this for now. + # Maybe we need to handle a few others for the full API of GridLayoutBase to work well... + bbox = BBox(shape) + layout_width = Observable{Any}(GridLayoutBase.width(bbox)) + layout_height = Observable{Any}(GridLayoutBase.height(bbox)) + layout_tellwidth = Observable(true) + layout_tellheight = Observable(true) + layout_halign = Observable{GridLayoutBase.HorizontalAlignment}(:left) + layout_valign = Observable{GridLayoutBase.VerticalAlignment}(:top) + layout_alignmode = Observable{Any}(GridLayoutBase.Inside()) + + lobservables = LayoutObservables( + layout_width, + layout_height, + layout_tellwidth, + layout_tellheight, + layout_halign, + layout_valign, + layout_alignmode, + ) + + sl = ShapeLayoutObservables(shape, shapeindex, lobservables) + + Observables.on(GridLayoutBase.computedbboxobservable(sl)) do newbbox + updatebbox!(shape, newbbox) + end + # Would like to connect the sizes and offsets of shape to suggestedbboxobservable so users can change their size + # and have the results take effect in the layout, but doing so create an infinite loop. Maybe it can be avoided through + # with_updates_suspended + sl +end + +# TODO: Need to implement for each shape +function updatebbox!(s::AbstractShape, bbox) + s.offset_x[] = round(Int, GridLayoutBase.left(bbox)) + s.offset_y[] = -round(Int, GridLayoutBase.top(bbox)) # Negative since PPTX counts y from the top + s.size_x[] = round(Int, GridLayoutBase.width(bbox)) + s.size_y[] = round(Int, GridLayoutBase.height(bbox)) +end