From c6e0cb059df7bd64827f76c61e47157b3a13eda6 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Wed, 8 Apr 2026 13:54:14 +0200 Subject: [PATCH 01/19] start of GridLayout type --- src/GridLayout.jl | 101 ++++++++++++++++++++++++++++++++++ src/PPTX.jl | 3 +- src/TextBox.jl | 9 +++ test/runtests.jl | 3 +- test/testGridLayout.jl | 85 ++++++++++++++++++++++++++++ test/testPresentationState.jl | 5 +- 6 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 src/GridLayout.jl create mode 100644 test/testGridLayout.jl diff --git a/src/GridLayout.jl b/src/GridLayout.jl new file mode 100644 index 0000000..7ebc3d4 --- /dev/null +++ b/src/GridLayout.jl @@ -0,0 +1,101 @@ +""" + GridLayout( + nrows::Int, + ncols::Int; + padding::Real=5.0, + margins=(left = padding, right = padding, top = padding, bottom = padding) + ) + +A declarative grid layout that distributes shapes evenly across a slide. + +Shapes are placed in cells via `setindex!` using integer indices, ranges or colons: + +```julia +layout = GridLayout(2, 2) +layout[1, 1] = TextBox("Title") +layout[2, :] = Picture("plot.png") # spans both columns in row 2 +push!(slide, layout) +``` + +Cell positions and sizes are resolved at write time from the actual slide dimensions. +`padding` (in mm) is applied between cells and at the slide edges. +Shapes are stretched to fill their assigned cell area (including any column/row span). + +Rules: +- Empty cells are silently skipped. +- Assigning to overlapping cell regions raises an `ArgumentError`. +""" +mutable struct GridLayout <: AbstractShape + nrows::Int + ncols::Int + padding::Float64 # mm to EMUs + margins::Margins # mm to EMUs + _entries::Vector{Tuple{AbstractShape,UnitRange{Int},UnitRange{Int}}} +end + +function GridLayout( + nrows::Int, + ncols::Int; + padding::Real=5.0, + margins=(left = padding, right = padding, top = padding, bottom = padding), +) + nrows > 0 || throw(ArgumentError("nrows must be positive")) + ncols > 0 || throw(ArgumentError("ncols must be positive")) + padding >= 0 || throw(ArgumentError("padding must be non-negative")) + return GridLayout( + nrows, + ncols, + mm_to_emu(padding), + Margins(margins), + Tuple{AbstractShape,UnitRange{Int},UnitRange{Int}}[], + ) +end + +# ----- index normalization ----- + +function _to_range(idx, n::Int, dim::String)::UnitRange{Int} + if idx isa Colon + return 1:n + elseif idx isa Integer + i = Int(idx) + 1 <= i <= n || throw(BoundsError("$dim index $i is out of range 1:$n")) + return i:i + elseif idx isa UnitRange + r = UnitRange{Int}(idx) + (1 <= first(r) && last(r) <= n) || + throw(BoundsError("$dim range $r is out of bounds 1:$n")) + return r + else + throw(ArgumentError("unsupported index type $(typeof(idx)) for $dim")) + end +end + +# ----- setindex! ----- + +function Base.setindex!(layout::GridLayout, shape::AbstractShape, row_idx, col_idx) + row_range = _to_range(row_idx, layout.nrows, "row") + col_range = _to_range(col_idx, layout.ncols, "col") + + # conflict detection: two rectangular spans conflict when they overlap in both dimensions + for (_, er, ec) in layout._entries + if !isempty(intersect(row_range, er)) && !isempty(intersect(col_range, ec)) + throw(ArgumentError( + "Cell span ($row_range, $col_range) conflicts with existing entry at ($er, $ec)" + )) + end + end + + push!(layout._entries, (shape, row_range, col_range)) + return shape +end + +# ----- show ----- + +function _show_string(layout::GridLayout, compact::Bool) + s = "GridLayout($(layout.nrows)×$(layout.ncols))" + if !compact + n = length(layout._entries) + s *= " with $n assigned cell$(n == 1 ? "" : "s")" + end + return s +end diff --git a/src/PPTX.jl b/src/PPTX.jl index 0cbad9f..05433af 100644 --- a/src/PPTX.jl +++ b/src/PPTX.jl @@ -13,7 +13,7 @@ import Tables: columns, columnnames, rows import Colors: Colorant, hex, @colorant_str -export Presentation, Slide, TextBox, TextStyle, Picture, Table, TableCell, list_layoutnames, Video +export Presentation, Slide, TextBox, TextStyle, Picture, Table, TableCell, list_layoutnames, Video, GridLayout include("AbstractShape.jl") include("constants.jl") @@ -23,6 +23,7 @@ include("Tables.jl") include("Slide.jl") include("Presentation.jl") include("Video.jl") +include("GridLayout.jl") include("xml_utils.jl") include("xml_ppt_utils.jl") include("write.jl") diff --git a/src/TextBox.jl b/src/TextBox.jl index 5a7bec0..43ba973 100644 --- a/src/TextBox.jl +++ b/src/TextBox.jl @@ -148,6 +148,15 @@ struct Margins bottom::Union{Nothing, Int} end +function Base.:(==)(m1::Margins, m2::Margins) + return m1.left == m2.left && m1.right == m2.right && m1.top == m2.top && m1.bottom == m2.bottom +end + +function Margins(m::Real) + mar = margin(m) + return Margins(mar, mar, mar, mar) +end + function Margins(; left = nothing, right = nothing, diff --git a/test/runtests.jl b/test/runtests.jl index 837159e..a488016 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,11 +5,12 @@ using Test import PPTX: slides, shapes, rid include("testAbstractShape.jl") +include("testGridLayout.jl") include("testHyperlinks.jl") include("testLayout.jl") include("testPicture.jl") include("testPresentation.jl") -# include("testPresentationState.jl") # This testset seems outdated and or obsolete +include("testPresentationState.jl") include("testSlide.jl") include("testSlideXML.jl") include("testTables.jl") diff --git a/test/testGridLayout.jl b/test/testGridLayout.jl new file mode 100644 index 0000000..d9f68f4 --- /dev/null +++ b/test/testGridLayout.jl @@ -0,0 +1,85 @@ +@testset "GridLayout" begin + @testset "constructor" begin + layout = GridLayout(2, 3) + @test layout.nrows == 2 + @test layout.ncols == 3 + @test layout.padding == PPTX.mm_to_emu(5) + @test layout.margins == PPTX.Margins(5) + @test isempty(layout._entries) + + layout_p = GridLayout(3, 3; padding=10) + @test layout_p.padding == PPTX.mm_to_emu(10) + @test layout_p.margins == PPTX.Margins(10) + + @test_throws ArgumentError GridLayout(0, 2) + @test_throws ArgumentError GridLayout(2, 0) + @test_throws ArgumentError GridLayout(2, 2; padding=-1) + end + + @testset "setindex! integer indices" begin + layout = GridLayout(2, 2) + box = TextBox("Hello") + layout[1, 1] = box + @test length(layout._entries) == 1 + shape, rows, cols = layout._entries[1] + @test shape === box + @test rows == 1:1 + @test cols == 1:1 + end + + @testset "setindex! colon spans" begin + layout = GridLayout(2, 3) + box = TextBox("Wide") + layout[2, :] = box + shape, rows, cols = layout._entries[1] + @test rows == 2:2 + @test cols == 1:3 + + layout2 = GridLayout(3, 2) + layout2[:, 1] = TextBox("Tall") + _, rows2, cols2 = layout2._entries[1] + @test rows2 == 1:3 + @test cols2 == 1:1 + end + + @testset "setindex! range spans" begin + layout = GridLayout(3, 4) + layout[1:2, 2:3] = TextBox("Block") + _, rows, cols = layout._entries[1] + @test rows == 1:2 + @test cols == 2:3 + end + + @testset "setindex! bounds checking" begin + layout = GridLayout(2, 2) + @test_throws BoundsError (layout[3, 1] = TextBox("")) + @test_throws BoundsError (layout[1, 0] = TextBox("")) + @test_throws BoundsError (layout[1:3, 1] = TextBox("")) + end + + @testset "setindex! conflict detection" begin + layout = GridLayout(3, 3) + layout[1, 1] = TextBox("A") + # exact overlap + @test_throws ArgumentError (layout[1, 1] = TextBox("B")) + # partial overlap + layout2 = GridLayout(3, 3) + layout2[1:2, 1:2] = TextBox("Block") + @test_throws ArgumentError (layout2[2, 2] = TextBox("Corner")) + # non-overlapping should succeed + layout3 = GridLayout(2, 2) + layout3[1, 1] = TextBox("A") + layout3[1, 2] = TextBox("B") + layout3[2, :] = TextBox("C") + @test length(layout3._entries) == 3 + end + + @testset "show" begin + layout = GridLayout(2, 2) + @test sprint(show, layout) == "GridLayout(2×2)" + layout[1, 1] = TextBox("x") + io = IOBuffer() + Base.show(io, MIME"text/plain"(), layout) + @test contains(String(take!(io)), "1 assigned cell") + end +end diff --git a/test/testPresentationState.jl b/test/testPresentationState.jl index 1128eab..5af4502 100644 --- a/test/testPresentationState.jl +++ b/test/testPresentationState.jl @@ -2,10 +2,9 @@ using Test using PPTX @testset "Presentation Size" begin - template_folder = abspath(joinpath(PPTX.TEMPLATE_DIR,"no-slides")) p = Presentation() - ppt_dir = joinpath(template_folder, "ppt") - PPTX.update_presentation_state!(p, ppt_dir) + template_reader = PPTX.ZipBufferReader(PPTX.read_template(PPTX.DEFAULT_TEMPLATE_DATA)) + PPTX.update_presentation_state!(p, template_reader) @test p._state.size.x == 12192000 @test p._state.size.y == 6858000 end \ No newline at end of file From d2b3247024da2985aee03c486d9fe054239d3e6d Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Wed, 8 Apr 2026 14:15:22 +0200 Subject: [PATCH 02/19] push!(::Slide,::GridLayout) with rid updates --- src/GridLayout.jl | 24 ++++++++++++++++-------- src/PPTX.jl | 2 +- src/Slide.jl | 14 ++++++++++++++ src/write.jl | 8 +++++--- test/testGridLayout.jl | 22 ++++++++++++++++++++++ 5 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/GridLayout.jl b/src/GridLayout.jl index 7ebc3d4..10e3b2d 100644 --- a/src/GridLayout.jl +++ b/src/GridLayout.jl @@ -28,11 +28,19 @@ Rules: mutable struct GridLayout <: AbstractShape nrows::Int ncols::Int - padding::Float64 # mm to EMUs - margins::Margins # mm to EMUs + padding::Float64 + margins::Margins _entries::Vector{Tuple{AbstractShape,UnitRange{Int},UnitRange{Int}}} end +function shapes(layout::GridLayout) + return getfield.(layout._entries, 1) +end + +function rid(layout::GridLayout) + return maximum(rid.(shapes(layout))) +end + function GridLayout( nrows::Int, ncols::Int; @@ -51,8 +59,6 @@ function GridLayout( ) end -# ----- index normalization ----- - function _to_range(idx, n::Int, dim::String)::UnitRange{Int} if idx isa Colon return 1:n @@ -70,8 +76,6 @@ function _to_range(idx, n::Int, dim::String)::UnitRange{Int} end end -# ----- setindex! ----- - function Base.setindex!(layout::GridLayout, shape::AbstractShape, row_idx, col_idx) row_range = _to_range(row_idx, layout.nrows, "row") col_range = _to_range(col_idx, layout.ncols, "col") @@ -89,8 +93,6 @@ function Base.setindex!(layout::GridLayout, shape::AbstractShape, row_idx, col_i return shape end -# ----- show ----- - function _show_string(layout::GridLayout, compact::Bool) s = "GridLayout($(layout.nrows)×$(layout.ncols))" if !compact @@ -99,3 +101,9 @@ function _show_string(layout::GridLayout, compact::Bool) end return s end + +function copy_shape(w::ZipWriter, p::GridLayout) + for (nested_shape, _, _) in p._entries + copy_shape(w, nested_shape) + end +end diff --git a/src/PPTX.jl b/src/PPTX.jl index 05433af..f4f14c1 100644 --- a/src/PPTX.jl +++ b/src/PPTX.jl @@ -20,10 +20,10 @@ include("constants.jl") include("TextBox.jl") include("Picture.jl") include("Tables.jl") +include("GridLayout.jl") include("Slide.jl") include("Presentation.jl") include("Video.jl") -include("GridLayout.jl") include("xml_utils.jl") include("xml_ppt_utils.jl") include("write.jl") diff --git a/src/Slide.jl b/src/Slide.jl index 7eaec52..ad27f69 100644 --- a/src/Slide.jl +++ b/src/Slide.jl @@ -91,6 +91,20 @@ function Base.push!(slide::Slide, shape::AbstractShape) end end +function Base.push!(slide::Slide, layout::GridLayout) + next_rid = new_rid(slide) + updated_entries = Tuple{AbstractShape,UnitRange{Int},UnitRange{Int}}[] + for (shape, row_range, col_range) in layout._entries + if has_rid(shape) + shape = set_rid(shape, next_rid) + next_rid += 1 + end + push!(updated_entries, (shape, row_range, col_range)) + end + layout._entries = updated_entries + return push!(shapes(slide), layout) +end + function make_slide(s::Slide, relationship_map::Dict = slide_relationship_map(s))::AbstractDict xml_slide = OrderedDict("p:sld" => main_attributes()) diff --git a/src/write.jl b/src/write.jl index 682399b..65f6869 100644 --- a/src/write.jl +++ b/src/write.jl @@ -87,12 +87,14 @@ function add_title_shape!(doc::EzXML.Document, slide::Slide, template::ZipBuffer nothing end +# default do nothing +copy_shape(w::ZipWriter, shape::AbstractShape) = nothing + +# some shapes, like Pictures, are media files that need to be copied/written into the pptx function write_shapes!(w::ZipWriter, pres::Presentation) for slide in slides(pres) for shape in shapes(slide) - if typeof(shape) ∈ [Picture, Video] - copy_shape(w::ZipWriter, shape) - end + copy_shape(w, shape) end end end diff --git a/test/testGridLayout.jl b/test/testGridLayout.jl index d9f68f4..19434a4 100644 --- a/test/testGridLayout.jl +++ b/test/testGridLayout.jl @@ -82,4 +82,26 @@ Base.show(io, MIME"text/plain"(), layout) @test contains(String(take!(io)), "1 assigned cell") end + + @testset "push! assigns nested rids" begin + slide = Slide() + layout = GridLayout(1, 2) + img1 = Picture(joinpath(PPTX.ASSETS_DIR, "julia_logo.emf"); size_x=10, size_y=10) + img2 = Picture(joinpath(PPTX.ASSETS_DIR, "julia_dots.wmf"); size_x=10, size_y=10) + layout[1, 1] = img1 + layout[1, 2] = img2 + + push!(slide, layout) + + @test length(PPTX.shapes(slide)) == 1 + pushed_layout = only(PPTX.shapes(slide)) + @test pushed_layout isa GridLayout + + pushed_entries = pushed_layout._entries + @test PPTX.rid(pushed_entries[1][1]) == 2 + @test PPTX.rid(pushed_entries[2][1]) == 3 + @test PPTX.rid(pushed_layout) == 3 + + @test PPTX.new_rid(slide) == 4 + end end From 94961d058615e7e46a96079d104f8bc3f06e1cfd Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Wed, 8 Apr 2026 14:19:14 +0200 Subject: [PATCH 03/19] handle layout slide relationships --- src/Slide.jl | 68 +++++++++++++++++++++++++----------------- test/testGridLayout.jl | 22 ++++++++++++++ 2 files changed, 63 insertions(+), 27 deletions(-) diff --git a/src/Slide.jl b/src/Slide.jl index ad27f69..4cb6117 100644 --- a/src/Slide.jl +++ b/src/Slide.jl @@ -188,16 +188,28 @@ function relationship_xml(url::AbstractString, r_id::Integer) ) end +_nested_shapes(shape::AbstractShape) = (shape,) + +function _nested_shapes(layout::GridLayout) + nested = AbstractShape[] + for (shape, _, _) in layout._entries + append!(nested, _nested_shapes(shape)) + end + return nested +end + function slide_relationship_map(s::Slide) d = Dict{Union{Slide, AbstractShape, AbstractString}, Int}() r_id = 1 # first rid is reserved by slideLayout for shape in shapes(s) - if has_rid(shape) && !haskey(d, shape) - r_id += 1 - d[shape] = r_id - elseif has_hyperlink(shape) && !haskey(d, shape.hlink) - r_id += 1 - d[shape.hlink] = r_id + for nested_shape in _nested_shapes(shape) + if has_rid(nested_shape) && !haskey(d, nested_shape) + r_id += 1 + d[nested_shape] = r_id + elseif has_hyperlink(nested_shape) && !haskey(d, nested_shape.hlink) + r_id += 1 + d[nested_shape.hlink] = r_id + end end end return d @@ -223,27 +235,29 @@ function make_slide_relationships(s::Slide, relationship_map::Dict = slide_relat ) used_r_ids = [1] for shape in shapes(s) - r_id = 1 - if has_rid(shape) - r_id = relationship_map[shape] - r_shape = shape - end - if has_hyperlink(shape) - r_id = relationship_map[shape.hlink] - r_shape = shape.hlink - end - if r_id ∉ used_r_ids - push!(xml_slide_rels["Relationships"], relationship_xml(r_shape, r_id)) - push!(used_r_ids, r_id) - end - # For a video 2 extra relationships are defined: an extra video link and a thumbnail - if shape isa Video - r_id += 1 - push!(xml_slide_rels["Relationships"], relationship_xml(shape, r_id; it = 1)) - push!(used_r_ids, r_id) - r_id += 1 - push!(xml_slide_rels["Relationships"], relationship_xml(picture_thumbnail(thumbnail_name(shape)), r_id)) - push!(used_r_ids, r_id) + for nested_shape in _nested_shapes(shape) + r_id = 1 + if has_rid(nested_shape) + r_id = relationship_map[nested_shape] + r_shape = nested_shape + end + if has_hyperlink(nested_shape) + r_id = relationship_map[nested_shape.hlink] + r_shape = nested_shape.hlink + end + if r_id ∉ used_r_ids + push!(xml_slide_rels["Relationships"], relationship_xml(r_shape, r_id)) + push!(used_r_ids, r_id) + end + # For a video 2 extra relationships are defined: an extra video link and a thumbnail + if nested_shape isa Video + r_id += 1 + push!(xml_slide_rels["Relationships"], relationship_xml(nested_shape, r_id; it = 1)) + push!(used_r_ids, r_id) + r_id += 1 + push!(xml_slide_rels["Relationships"], relationship_xml(picture_thumbnail(thumbnail_name(nested_shape)), r_id)) + push!(used_r_ids, r_id) + end end end return xml_slide_rels diff --git a/test/testGridLayout.jl b/test/testGridLayout.jl index 19434a4..dfec20e 100644 --- a/test/testGridLayout.jl +++ b/test/testGridLayout.jl @@ -104,4 +104,26 @@ @test PPTX.new_rid(slide) == 4 end + + @testset "nested relationships" begin + target_slide = Slide() + slide = Slide() + layout = GridLayout(1, 2) + img = Picture(joinpath(PPTX.ASSETS_DIR, "julia_logo.emf"); size_x=10, size_y=10) + link_box = TextBox(content="jump", hlink=target_slide) + layout[1, 1] = img + layout[1, 2] = link_box + push!(slide, layout) + + rel_map = PPTX.slide_relationship_map(slide) + nested_img = only(filter(x -> x isa Picture, PPTX.shapes(layout))) + @test haskey(rel_map, nested_img) + @test haskey(rel_map, target_slide) + + rel_xml = PPTX.make_slide_relationships(slide) + relationships = rel_xml["Relationships"] + @test length(relationships) == 4 + @test relationships[3]["Relationship"][1]["Id"] == "rId2" + @test relationships[4]["Relationship"][1]["Id"] == "rId3" + end end From 7f5f8f726d4f3c8eb048236351ce964642ca5590 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Wed, 8 Apr 2026 14:57:02 +0200 Subject: [PATCH 04/19] make slide xml with layout --- src/AbstractShape.jl | 1 + src/GridLayout.jl | 67 ++++++++++++++++++++++++++++++++++++++++-- src/Picture.jl | 5 ++++ src/Slide.jl | 53 +++++++++++++++++++++++++++++---- src/Tables.jl | 41 ++++++++++++++++++++++++++ src/TextBox.jl | 41 ++++++++++++++++++++++++++ src/Video.jl | 17 +++++++++++ src/write.jl | 5 +++- test/testGridLayout.jl | 40 +++++++++++++++++++++++-- 9 files changed, 260 insertions(+), 10 deletions(-) diff --git a/src/AbstractShape.jl b/src/AbstractShape.jl index d884f40..d5b0efe 100644 --- a/src/AbstractShape.jl +++ b/src/AbstractShape.jl @@ -2,6 +2,7 @@ abstract type AbstractShape end # the 'relative identifier' is used to link shapes in the PowerPoint XML set_rid!(s::AbstractShape, i::Int) = nothing +set_geometry(s::AbstractShape, offset_x::Int, offset_y::Int, size_x::Int, size_y::Int) = s has_rid(s::AbstractShape) = false ## If AbstractShape does not have rId return 0 diff --git a/src/GridLayout.jl b/src/GridLayout.jl index 10e3b2d..a9ed946 100644 --- a/src/GridLayout.jl +++ b/src/GridLayout.jl @@ -1,3 +1,29 @@ +struct GridMargins # in EMUs + left::Int + right::Int + top::Int + bottom::Int +end + +function Base.:(==)(m1::GridMargins, m2::GridMargins) + return m1.left == m2.left && m1.right == m2.right && m1.top == m2.top && m1.bottom == m2.bottom +end + +_grid_margin_mm(x::Real) = mm_to_emu(x) +_grid_margin_mm(::Nothing) = 0 + +function GridMargins(; left=0, right=0, top=0, bottom=0) + return GridMargins( + _grid_margin_mm(left), + _grid_margin_mm(right), + _grid_margin_mm(top), + _grid_margin_mm(bottom), + ) +end + +GridMargins(x::Real) = GridMargins(; left=x, right=x, top=x, bottom=x) +GridMargins(nt::NamedTuple) = GridMargins(; nt...) + """ GridLayout( nrows::Int, @@ -29,7 +55,7 @@ mutable struct GridLayout <: AbstractShape nrows::Int ncols::Int padding::Float64 - margins::Margins + margins::GridMargins _entries::Vector{Tuple{AbstractShape,UnitRange{Int},UnitRange{Int}}} end @@ -50,11 +76,14 @@ function GridLayout( nrows > 0 || throw(ArgumentError("nrows must be positive")) ncols > 0 || throw(ArgumentError("ncols must be positive")) padding >= 0 || throw(ArgumentError("padding must be non-negative")) + grid_margins = GridMargins(margins) + min_margin = min(grid_margins.left, grid_margins.right, grid_margins.top, grid_margins.bottom) + min_margin >= 0 || throw(ArgumentError("margins must be non-negative")) return GridLayout( nrows, ncols, mm_to_emu(padding), - Margins(margins), + grid_margins, Tuple{AbstractShape,UnitRange{Int},UnitRange{Int}}[], ) end @@ -93,6 +122,40 @@ function Base.setindex!(layout::GridLayout, shape::AbstractShape, row_idx, col_i return shape end +function layout_bounds( + layout::GridLayout, + row_range::UnitRange{Int}, + col_range::UnitRange{Int}, + slide_size_x::Int, + slide_size_y::Int, +) + left_margin = layout.margins.left + right_margin = layout.margins.right + top_margin = layout.margins.top + bottom_margin = layout.margins.bottom + gap = Int(round(layout.padding)) + + usable_width = slide_size_x - left_margin - right_margin - (layout.ncols - 1) * gap + usable_height = slide_size_y - top_margin - bottom_margin - (layout.nrows - 1) * gap + usable_width > 0 || throw(ArgumentError("GridLayout has non-positive usable width")) + usable_height > 0 || throw(ArgumentError("GridLayout has non-positive usable height")) + + cell_width = usable_width / layout.ncols + cell_height = usable_height / layout.nrows + + first_col = first(col_range) + last_col = last(col_range) + first_row = first(row_range) + last_row = last(row_range) + + offset_x = Int(round(left_margin + (first_col - 1) * (cell_width + gap))) + offset_y = Int(round(top_margin + (first_row - 1) * (cell_height + gap))) + size_x = Int(round((last_col - first_col + 1) * cell_width + (length(col_range) - 1) * gap)) + size_y = Int(round((last_row - first_row + 1) * cell_height + (length(row_range) - 1) * gap)) + + return offset_x, offset_y, size_x, size_y +end + function _show_string(layout::GridLayout, compact::Bool) s = "GridLayout($(layout.nrows)×$(layout.ncols))" if !compact diff --git a/src/Picture.jl b/src/Picture.jl index f7ea146..887f147 100644 --- a/src/Picture.jl +++ b/src/Picture.jl @@ -97,6 +97,11 @@ end function set_rid(s::Picture, i::Int) return Picture(s.source, s.offset_x, s.offset_y, s.size_x, s.size_y, i) end + +function set_geometry(s::Picture, offset_x::Int, offset_y::Int, size_x::Int, size_y::Int) + return Picture(s.source, offset_x, offset_y, size_x, size_y, s.rid, s._uuid) +end + rid(s::Picture) = s.rid has_rid(s::Picture) = true diff --git a/src/Slide.jl b/src/Slide.jl index 4cb6117..9c83d38 100644 --- a/src/Slide.jl +++ b/src/Slide.jl @@ -105,14 +105,57 @@ function Base.push!(slide::Slide, layout::GridLayout) return push!(shapes(slide), layout) end -function make_slide(s::Slide, relationship_map::Dict = slide_relationship_map(s))::AbstractDict +function _bind_relationships!(relationship_map::Dict, original_shape::AbstractShape, updated_shape::AbstractShape) + if has_rid(original_shape) && haskey(relationship_map, original_shape) + relationship_map[updated_shape] = relationship_map[original_shape] + end + return nothing +end + +function _make_xml_nodes( + shape::AbstractShape, + start_id::Int, + relationship_map::Dict, + slide_size_x::Int, + slide_size_y::Int, +) + return Any[make_xml(shape, start_id, relationship_map)] +end + +function _make_xml_nodes( + layout::GridLayout, + start_id::Int, + relationship_map::Dict, + slide_size_x::Int, + slide_size_y::Int, +) + xml_nodes = Any[] + current_id = start_id + for (shape, row_range, col_range) in layout._entries + offset_x, offset_y, size_x, size_y = + layout_bounds(layout, row_range, col_range, slide_size_x, slide_size_y) + shape_updated = set_geometry(shape, offset_x, offset_y, size_x, size_y) + _bind_relationships!(relationship_map, shape, shape_updated) + push!(xml_nodes, make_xml(shape_updated, current_id, relationship_map)) + current_id += 1 + end + return xml_nodes +end + +function make_slide( + s::Slide, + relationship_map::Dict = slide_relationship_map(s); + slide_size_x::Int = Int(13.333 * _EMUS_PER_INCH), + slide_size_y::Int = Int(7.5 * _EMUS_PER_INCH), +)::AbstractDict xml_slide = OrderedDict("p:sld" => main_attributes()) spTree = init_sptree() - initial_max_id = 1 - for (index, shape) in enumerate(shapes(s)) - id = index + initial_max_id - push!(spTree["p:spTree"], make_xml(shape, id, relationship_map)) + next_id = 2 + for shape in shapes(s) + xml_nodes = _make_xml_nodes(shape, next_id, relationship_map, slide_size_x, slide_size_y) + append!(spTree["p:spTree"], xml_nodes) + next_id += length(xml_nodes) end push!(xml_slide["p:sld"], OrderedDict("p:cSld" => [spTree])) diff --git a/src/Tables.jl b/src/Tables.jl index a489277..9161058 100644 --- a/src/Tables.jl +++ b/src/Tables.jl @@ -54,6 +54,32 @@ struct Table <: AbstractShape header::Bool bandrow::Bool style_id::String + function Table( + content, + offset_x::Int, + offset_y::Int, + size_x::Int, + size_y::Int, + column_widths::Union{Nothing, Vector{Int}} = nothing, + row_heights::Union{Nothing, Vector{Int}} = nothing, + header::Bool = true, + bandrow::Bool = true, + style_id::String = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}", + ) + return new( + content, + offset_x, + offset_y, + size_x, + size_y, + column_widths, + row_heights, + header, + bandrow, + style_id, + ) + end + function Table( content, offset_x::Real, # millimeters @@ -82,6 +108,21 @@ struct Table <: AbstractShape end end +function set_geometry(t::Table, offset_x::Int, offset_y::Int, size_x::Int, size_y::Int) + return Table( + t.content, + offset_x, + offset_y, + size_x, + size_y, + t.column_widths, + t.row_heights, + t.header, + t.bandrow, + t.style_id, + ) +end + # keyword argument constructor function Table(; content, diff --git a/src/TextBox.jl b/src/TextBox.jl index 43ba973..500f48a 100644 --- a/src/TextBox.jl +++ b/src/TextBox.jl @@ -304,6 +304,32 @@ struct TextBox<: AbstractShape linecolor::Union{Nothing, String} linewidth::Union{Nothing, Int} rotation::Union{Nothing, Float64} + function TextBox( + content::TextBody, + offset_x::Int, + offset_y::Int, + size_x::Int, + size_y::Int, + hlink::Union{Nothing, Any} = nothing, + color::Union{Nothing, String} = nothing, + linecolor::Union{Nothing, String} = nothing, + linewidth::Union{Nothing, Int} = nothing, + rotation::Union{Nothing, Float64} = nothing, + ) + return new( + content, + offset_x, + offset_y, + size_x, + size_y, + hlink, + color, + linecolor, + linewidth, + rotation, + ) + end + function TextBox( content::AbstractString, offset_x::Real, # millimeters @@ -341,6 +367,21 @@ struct TextBox<: AbstractShape end end +function set_geometry(t::TextBox, offset_x::Int, offset_y::Int, size_x::Int, size_y::Int) + return TextBox( + t.content, + offset_x, + offset_y, + size_x, + size_y, + t.hlink, + t.color, + t.linecolor, + t.linewidth, + t.rotation, + ) +end + mm_to_emu(::Nothing) = nothing mm_to_emu(x) = Int(round(x * _EMUS_PER_MM)) mm_to_emu(x::AbstractArray{<:Real}) = mm_to_emu.(x) diff --git a/src/Video.jl b/src/Video.jl index 0a48948..7f86ddd 100644 --- a/src/Video.jl +++ b/src/Video.jl @@ -8,6 +8,18 @@ struct Video <: AbstractShape size_y::Int rid::Int _uuid::String + function Video( + source::String, + offset_x::Int, + offset_y::Int, + size_x::Int, + size_y::Int, + rid::Int, + uuid::String, + ) + new(source, offset_x, offset_y, size_x, size_y, rid, uuid) + end + function Video(source::String, offset_x::Int, offset_y::Int, size_x::Int, size_y::Int, rid::Int) new(source, offset_x, offset_y, size_x, size_y, rid, string(UUIDs.uuid4())) end @@ -40,6 +52,11 @@ end function set_rid(v::Video, i::Int) return Video(v.source, v.offset_x, v.offset_y, v.size_x, v.size_y, i) end + +function set_geometry(v::Video, offset_x::Int, offset_y::Int, size_x::Int, size_y::Int) + return Video(v.source, offset_x, offset_y, size_x, size_y, v.rid, v._uuid) +end + rid(v::Video) = v.rid has_rid(v::Video) = true diff --git a/src/write.jl b/src/write.jl index 65f6869..8f34bb9 100644 --- a/src/write.jl +++ b/src/write.jl @@ -55,9 +55,12 @@ function write_slides!(w::ZipWriter, p::Presentation, template::ZipBufferReader) error("input template pptx already contains slides, please use an empty template") end layoutmap = get_layoutnamemap(template) + sz = p._state.size + sz_x = isnothing(sz) ? Int(13.333 * _EMUS_PER_INCH) : sz.x + sz_y = isnothing(sz) ? Int(7.5 * _EMUS_PER_INCH) : sz.y for (idx, slide) in enumerate(slides(p)) layoutnametoint!(slide, layoutmap) - xml = make_slide(slide) + xml = make_slide(slide; slide_size_x=sz_x, slide_size_y=sz_y) doc::EzXML.Document = xml_document(xml) add_title_shape!(doc, slide, template) zip_newfile(w, "ppt/slides/slide$idx.xml"; compress=true) diff --git a/test/testGridLayout.jl b/test/testGridLayout.jl index dfec20e..e546187 100644 --- a/test/testGridLayout.jl +++ b/test/testGridLayout.jl @@ -4,16 +4,17 @@ @test layout.nrows == 2 @test layout.ncols == 3 @test layout.padding == PPTX.mm_to_emu(5) - @test layout.margins == PPTX.Margins(5) + @test layout.margins == PPTX.GridMargins(5) @test isempty(layout._entries) layout_p = GridLayout(3, 3; padding=10) @test layout_p.padding == PPTX.mm_to_emu(10) - @test layout_p.margins == PPTX.Margins(10) + @test layout_p.margins == PPTX.GridMargins(10) @test_throws ArgumentError GridLayout(0, 2) @test_throws ArgumentError GridLayout(2, 0) @test_throws ArgumentError GridLayout(2, 2; padding=-1) + @test_throws ArgumentError GridLayout(2, 2; margins=(left=-1, right=0, top=0, bottom=0)) end @testset "setindex! integer indices" begin @@ -126,4 +127,39 @@ @test relationships[3]["Relationship"][1]["Id"] == "rId2" @test relationships[4]["Relationship"][1]["Id"] == "rId3" end + + @testset "make_slide places grid shapes" begin + slide = Slide() + layout = GridLayout(2, 2; padding=10) + layout[1, 1] = TextBox("Title") + layout[2, :] = Picture(joinpath(PPTX.ASSETS_DIR, "julia_logo.emf"); size_x=10, size_y=10) + push!(slide, layout) + + xml = PPTX.make_slide( + slide; + slide_size_x=PPTX.mm_to_emu(100), + slide_size_y=PPTX.mm_to_emu(50), + ) + + sp_tree = xml["p:sld"][end]["p:cSld"][1]["p:spTree"] + # spTree contains 2 group entries first, then our 2 layout shapes + text_sp = sp_tree[3]["p:sp"] + pic_sp = sp_tree[4]["p:pic"] + + text_xfrm = text_sp[2]["p:spPr"][1]["a:xfrm"] + text_off = only(filter(x -> haskey(x, "a:off"), text_xfrm))["a:off"] + text_ext = only(filter(x -> haskey(x, "a:ext"), text_xfrm))["a:ext"] + @test text_off[1]["x"] == string(PPTX.mm_to_emu(10)) + @test text_off[2]["y"] == string(PPTX.mm_to_emu(10)) + @test text_ext[1]["cx"] == string(PPTX.mm_to_emu(35)) + @test text_ext[2]["cy"] == string(PPTX.mm_to_emu(10)) + + pic_xfrm = pic_sp[3]["p:spPr"][1]["a:xfrm"] + pic_off = pic_xfrm[1]["a:off"] + pic_ext = pic_xfrm[2]["a:ext"] + @test pic_off[1]["x"] == string(PPTX.mm_to_emu(10)) + @test pic_off[2]["y"] == string(PPTX.mm_to_emu(30)) + @test pic_ext[1]["cx"] == string(PPTX.mm_to_emu(80)) + @test pic_ext[2]["cy"] == string(PPTX.mm_to_emu(10)) + end end From bbcd4a88e3c7de20fb7c02a8f84ae6411ee045c4 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Wed, 8 Apr 2026 15:49:50 +0200 Subject: [PATCH 05/19] PPTX.write -> write --- docs/src/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/index.md b/docs/src/index.md index ecd5d31..adb0fd1 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -74,5 +74,5 @@ Presentation with 4 slides Finally you can write the PPTX file with `PPTX.write`: ```julia -PPTX.write("example.pptx", pres, overwrite = true, open_ppt=true) +write("example.pptx", pres, overwrite = true, open_ppt=true) ``` From 966d4f7daa31a4e2ce80df0924be97bd75adb276 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Thu, 9 Apr 2026 16:40:26 +0200 Subject: [PATCH 06/19] optional rescaling of the shape ratio --- src/AbstractShape.jl | 35 ++++++++++++++++++++++++++++++++++- src/GridLayout.jl | 16 +++++++++++----- src/Picture.jl | 12 ++++++++---- src/Slide.jl | 6 +++--- src/Tables.jl | 10 +++++----- src/TextBox.jl | 10 +++++----- src/Video.jl | 12 ++++++++---- test/testGridLayout.jl | 39 ++++++++++++++++++++++++++++++++++++--- 8 files changed, 110 insertions(+), 30 deletions(-) diff --git a/src/AbstractShape.jl b/src/AbstractShape.jl index d5b0efe..32a73ad 100644 --- a/src/AbstractShape.jl +++ b/src/AbstractShape.jl @@ -2,12 +2,45 @@ abstract type AbstractShape end # the 'relative identifier' is used to link shapes in the PowerPoint XML set_rid!(s::AbstractShape, i::Int) = nothing -set_geometry(s::AbstractShape, offset_x::Int, offset_y::Int, size_x::Int, size_y::Int) = s has_rid(s::AbstractShape) = false ## If AbstractShape does not have rId return 0 rid(s::AbstractShape) = 0 +# struct to hold common shape geometry properties +# (could be re-used in the future inside the shape) +struct Geometry + offset_x::Int + offset_y::Int + size_x::Int + size_y::Int +end + +# default do not change geometries (this is used for GridLayout, where the geometry is determined by the cell span, not the shape) +set_geometry(s::AbstractShape, geom::Geometry) = s +function get_geometry(s::AbstractShape) + return Geometry(s.offset_x, s.offset_y, s.size_x, s.size_y) +end + +# default no geometry rescaling (e.g. for TextBox, where the text should fill the cell span without changing the aspect ratio) +geometry_in_span(s::AbstractShape, geom::Geometry, rescale::Bool) = geom + +# geometry rescaling re-used by Picture and Video, keeps the ratio constant +function geometry_in_span(shape_geom::Geometry, span::Geometry, rescale::Bool) + if !rescale || shape_geom.size_x <= 0 || shape_geom.size_y <= 0 || span.size_x <= 0 || span.size_y <= 0 + return span + end + + # look at the scaling factor for each dimension and use the smaller one to ensure the shape fits within the span + scale = min(span.size_x / shape_geom.size_x, span.size_y / shape_geom.size_y) + # rescale the shape dimensions by the calculated factor + scaled_x = Int(round(shape_geom.size_x * scale)) + scaled_y = Int(round(shape_geom.size_y * scale)) + centered_x = span.offset_x + Int(round((span.size_x - scaled_x) / 2)) + centered_y = span.offset_y + Int(round((span.size_y - scaled_y) / 2)) + return Geometry(centered_x, centered_y, scaled_x, scaled_y) +end + # default show used by Array show function Base.show(io::IO, shape::AbstractShape) compact = get(io, :compact, true) diff --git a/src/GridLayout.jl b/src/GridLayout.jl index a9ed946..9b4c03d 100644 --- a/src/GridLayout.jl +++ b/src/GridLayout.jl @@ -28,8 +28,9 @@ GridMargins(nt::NamedTuple) = GridMargins(; nt...) GridLayout( nrows::Int, ncols::Int; - padding::Real=5.0, - margins=(left = padding, right = padding, top = padding, bottom = padding) + padding::Real=5.0, + margins=(left = padding, right = padding, top = padding, bottom = padding), + rescale::Bool=true, ) A declarative grid layout that distributes shapes evenly across a slide. @@ -45,7 +46,9 @@ push!(slide, layout) Cell positions and sizes are resolved at write time from the actual slide dimensions. `padding` (in mm) is applied between cells and at the slide edges. -Shapes are stretched to fill their assigned cell area (including any column/row span). +When `rescale=true` (default), pictures/videos preserve their aspect ratio and are +centered within the assigned cell span. With `rescale=false`, they are stretched +to fill the assigned span. Rules: - Empty cells are silently skipped. @@ -56,6 +59,7 @@ mutable struct GridLayout <: AbstractShape ncols::Int padding::Float64 margins::GridMargins + rescale::Bool _entries::Vector{Tuple{AbstractShape,UnitRange{Int},UnitRange{Int}}} end @@ -70,8 +74,9 @@ end function GridLayout( nrows::Int, ncols::Int; - padding::Real=5.0, + padding::Real=5.0, margins=(left = padding, right = padding, top = padding, bottom = padding), + rescale::Bool=true, ) nrows > 0 || throw(ArgumentError("nrows must be positive")) ncols > 0 || throw(ArgumentError("ncols must be positive")) @@ -84,6 +89,7 @@ function GridLayout( ncols, mm_to_emu(padding), grid_margins, + rescale, Tuple{AbstractShape,UnitRange{Int},UnitRange{Int}}[], ) end @@ -153,7 +159,7 @@ function layout_bounds( size_x = Int(round((last_col - first_col + 1) * cell_width + (length(col_range) - 1) * gap)) size_y = Int(round((last_row - first_row + 1) * cell_height + (length(row_range) - 1) * gap)) - return offset_x, offset_y, size_x, size_y + return Geometry(offset_x, offset_y, size_x, size_y) end function _show_string(layout::GridLayout, compact::Bool) diff --git a/src/Picture.jl b/src/Picture.jl index 887f147..c763658 100644 --- a/src/Picture.jl +++ b/src/Picture.jl @@ -98,13 +98,17 @@ function set_rid(s::Picture, i::Int) return Picture(s.source, s.offset_x, s.offset_y, s.size_x, s.size_y, i) end -function set_geometry(s::Picture, offset_x::Int, offset_y::Int, size_x::Int, size_y::Int) - return Picture(s.source, offset_x, offset_y, size_x, size_y, s.rid, s._uuid) -end - rid(s::Picture) = s.rid has_rid(s::Picture) = true +function set_geometry(s::Picture, geom::Geometry) + return Picture(s.source, geom.offset_x, geom.offset_y, geom.size_x, geom.size_y, s.rid, s._uuid) +end + +function geometry_in_span(s::Picture, geom::Geometry, rescale::Bool) + geometry_in_span(get_geometry(s), geom, rescale) +end + function _show_string(p::Picture, compact::Bool) show_string = "Picture" if !compact diff --git a/src/Slide.jl b/src/Slide.jl index 9c83d38..44dd352 100644 --- a/src/Slide.jl +++ b/src/Slide.jl @@ -132,9 +132,9 @@ function _make_xml_nodes( xml_nodes = Any[] current_id = start_id for (shape, row_range, col_range) in layout._entries - offset_x, offset_y, size_x, size_y = - layout_bounds(layout, row_range, col_range, slide_size_x, slide_size_y) - shape_updated = set_geometry(shape, offset_x, offset_y, size_x, size_y) + geom = layout_bounds(layout, row_range, col_range, slide_size_x, slide_size_y) + geom = geometry_in_span(shape, geom, layout.rescale) + shape_updated = set_geometry(shape, geom) _bind_relationships!(relationship_map, shape, shape_updated) push!(xml_nodes, make_xml(shape_updated, current_id, relationship_map)) current_id += 1 diff --git a/src/Tables.jl b/src/Tables.jl index 9161058..74d4c30 100644 --- a/src/Tables.jl +++ b/src/Tables.jl @@ -108,13 +108,13 @@ struct Table <: AbstractShape end end -function set_geometry(t::Table, offset_x::Int, offset_y::Int, size_x::Int, size_y::Int) +function set_geometry(t::Table, geom::Geometry) return Table( t.content, - offset_x, - offset_y, - size_x, - size_y, + geom.offset_x, + geom.offset_y, + geom.size_x, + geom.size_y, t.column_widths, t.row_heights, t.header, diff --git a/src/TextBox.jl b/src/TextBox.jl index 500f48a..bd1ad48 100644 --- a/src/TextBox.jl +++ b/src/TextBox.jl @@ -367,13 +367,13 @@ struct TextBox<: AbstractShape end end -function set_geometry(t::TextBox, offset_x::Int, offset_y::Int, size_x::Int, size_y::Int) +function set_geometry(t::TextBox, geom::Geometry) return TextBox( t.content, - offset_x, - offset_y, - size_x, - size_y, + geom.offset_x, + geom.offset_y, + geom.size_x, + geom.size_y, t.hlink, t.color, t.linecolor, diff --git a/src/Video.jl b/src/Video.jl index 7f86ddd..fd1195a 100644 --- a/src/Video.jl +++ b/src/Video.jl @@ -53,13 +53,17 @@ function set_rid(v::Video, i::Int) return Video(v.source, v.offset_x, v.offset_y, v.size_x, v.size_y, i) end -function set_geometry(v::Video, offset_x::Int, offset_y::Int, size_x::Int, size_y::Int) - return Video(v.source, offset_x, offset_y, size_x, size_y, v.rid, v._uuid) -end - rid(v::Video) = v.rid has_rid(v::Video) = true +function set_geometry(v::Video, geom::Geometry) + return Video(v.source, geom.offset_x, geom.offset_y, geom.size_x, geom.size_y, v.rid, v._uuid) +end + +function geometry_in_span(s::Video, geom::Geometry, rescale::Bool) + geometry_in_span(get_geometry(s), geom, rescale) +end + function _show_string(v::Video, compact::Bool) show_string = "Video" if !compact diff --git a/test/testGridLayout.jl b/test/testGridLayout.jl index e546187..2edc7c6 100644 --- a/test/testGridLayout.jl +++ b/test/testGridLayout.jl @@ -5,6 +5,7 @@ @test layout.ncols == 3 @test layout.padding == PPTX.mm_to_emu(5) @test layout.margins == PPTX.GridMargins(5) + @test layout.rescale == true @test isempty(layout._entries) layout_p = GridLayout(3, 3; padding=10) @@ -132,7 +133,8 @@ slide = Slide() layout = GridLayout(2, 2; padding=10) layout[1, 1] = TextBox("Title") - layout[2, :] = Picture(joinpath(PPTX.ASSETS_DIR, "julia_logo.emf"); size_x=10, size_y=10) + pic = Picture(joinpath(PPTX.ASSETS_DIR, "julia_logo.emf")) + layout[2, :] = pic push!(slide, layout) xml = PPTX.make_slide( @@ -146,14 +148,45 @@ text_sp = sp_tree[3]["p:sp"] pic_sp = sp_tree[4]["p:pic"] + # size of a grid cell for this slide size and layout dimensions, after accounting for padding and margins + cell_x_size = PPTX.mm_to_emu(35) + cell_y_size = PPTX.mm_to_emu(10) + text_xfrm = text_sp[2]["p:spPr"][1]["a:xfrm"] text_off = only(filter(x -> haskey(x, "a:off"), text_xfrm))["a:off"] text_ext = only(filter(x -> haskey(x, "a:ext"), text_xfrm))["a:ext"] + @test text_ext[1]["cx"] == string(cell_x_size) + @test text_ext[2]["cy"] == string(cell_y_size) @test text_off[1]["x"] == string(PPTX.mm_to_emu(10)) @test text_off[2]["y"] == string(PPTX.mm_to_emu(10)) - @test text_ext[1]["cx"] == string(PPTX.mm_to_emu(35)) - @test text_ext[2]["cy"] == string(PPTX.mm_to_emu(10)) + # default rescale=true keeps ratio for Picture and centers it in span. + pic_xfrm = pic_sp[3]["p:spPr"][1]["a:xfrm"] + pic_off = pic_xfrm[1]["a:off"] + pic_ext = pic_xfrm[2]["a:ext"] + ratio = pic.size_x / pic.size_y + scaled_y = Int(round(cell_y_size * ratio)) + @test pic_ext[1]["cx"] == string(scaled_y) + @test pic_ext[2]["cy"] == string(cell_y_size) + centered_x = PPTX.mm_to_emu(45) + Int(round((cell_y_size - scaled_y) / 2)) + @test pic_off[1]["x"] == string(centered_x) + @test pic_off[2]["y"] == string(PPTX.mm_to_emu(30)) + end + + @testset "make_slide picture rescale=false stretches" begin + slide = Slide() + layout = GridLayout(2, 2; padding=10, rescale=false) + layout[2, :] = Picture(joinpath(PPTX.ASSETS_DIR, "julia_logo.emf"); size_x=10, size_y=10) + push!(slide, layout) + + xml = PPTX.make_slide( + slide; + slide_size_x=PPTX.mm_to_emu(100), + slide_size_y=PPTX.mm_to_emu(50), + ) + + sp_tree = xml["p:sld"][end]["p:cSld"][1]["p:spTree"] + pic_sp = sp_tree[3]["p:pic"] pic_xfrm = pic_sp[3]["p:spPr"][1]["a:xfrm"] pic_off = pic_xfrm[1]["a:off"] pic_ext = pic_xfrm[2]["a:ext"] From 59a9514b6625749996deb2dd5cca1bf62a637b73 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 10 Apr 2026 10:36:56 +0200 Subject: [PATCH 07/19] GridLayout refactoring --- src/AbstractShape.jl | 6 +++--- src/GridLayout.jl | 35 ++++++++++++++++++++--------------- src/Picture.jl | 4 ++-- src/Slide.jl | 4 ++-- src/Tables.jl | 4 ++-- src/Video.jl | 4 ++-- test/testGridLayout.jl | 14 +++++++------- 7 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/AbstractShape.jl b/src/AbstractShape.jl index 32a73ad..6da9730 100644 --- a/src/AbstractShape.jl +++ b/src/AbstractShape.jl @@ -23,11 +23,11 @@ function get_geometry(s::AbstractShape) end # default no geometry rescaling (e.g. for TextBox, where the text should fill the cell span without changing the aspect ratio) -geometry_in_span(s::AbstractShape, geom::Geometry, rescale::Bool) = geom +geometry_in_span(s::AbstractShape, geom::Geometry, keepratio::Bool) = geom # geometry rescaling re-used by Picture and Video, keeps the ratio constant -function geometry_in_span(shape_geom::Geometry, span::Geometry, rescale::Bool) - if !rescale || shape_geom.size_x <= 0 || shape_geom.size_y <= 0 || span.size_x <= 0 || span.size_y <= 0 +function geometry_in_span(shape_geom::Geometry, span::Geometry, keepratio::Bool) + if !keepratio || shape_geom.size_x <= 0 || shape_geom.size_y <= 0 || span.size_x <= 0 || span.size_y <= 0 return span end diff --git a/src/GridLayout.jl b/src/GridLayout.jl index 9b4c03d..efea3ff 100644 --- a/src/GridLayout.jl +++ b/src/GridLayout.jl @@ -29,8 +29,8 @@ GridMargins(nt::NamedTuple) = GridMargins(; nt...) nrows::Int, ncols::Int; padding::Real=5.0, - margins=(left = padding, right = padding, top = padding, bottom = padding), - rescale::Bool=true, + margins=(left = 5, right = 5, top = 40, bottom = 5), + keepratio::Bool=true, ) A declarative grid layout that distributes shapes evenly across a slide. @@ -38,16 +38,16 @@ A declarative grid layout that distributes shapes evenly across a slide. Shapes are placed in cells via `setindex!` using integer indices, ranges or colons: ```julia -layout = GridLayout(2, 2) +layout = GridLayout(2, 2; margins=10) layout[1, 1] = TextBox("Title") layout[2, :] = Picture("plot.png") # spans both columns in row 2 push!(slide, layout) ``` Cell positions and sizes are resolved at write time from the actual slide dimensions. -`padding` (in mm) is applied between cells and at the slide edges. -When `rescale=true` (default), pictures/videos preserve their aspect ratio and are -centered within the assigned cell span. With `rescale=false`, they are stretched +`padding` (in mm) is applied between cells and `margins` at the slide edges. +When `keepratio=true` (default), pictures/videos preserve their aspect ratio and are +centered within the assigned cell span. With `keepratio=false`, they are stretched to fill the assigned span. Rules: @@ -59,7 +59,7 @@ mutable struct GridLayout <: AbstractShape ncols::Int padding::Float64 margins::GridMargins - rescale::Bool + keepratio::Bool _entries::Vector{Tuple{AbstractShape,UnitRange{Int},UnitRange{Int}}} end @@ -75,8 +75,9 @@ function GridLayout( nrows::Int, ncols::Int; padding::Real=5.0, - margins=(left = padding, right = padding, top = padding, bottom = padding), - rescale::Bool=true, + # default bigger top margin to handle titles + margins=(left = 5, right = 5, top = 40, bottom = 5), + keepratio::Bool=true, ) nrows > 0 || throw(ArgumentError("nrows must be positive")) ncols > 0 || throw(ArgumentError("ncols must be positive")) @@ -89,7 +90,7 @@ function GridLayout( ncols, mm_to_emu(padding), grid_margins, - rescale, + keepratio, Tuple{AbstractShape,UnitRange{Int},UnitRange{Int}}[], ) end @@ -128,12 +129,16 @@ function Base.setindex!(layout::GridLayout, shape::AbstractShape, row_idx, col_i return shape end -function layout_bounds( +function Base.setindex!(layout::GridLayout, shape::GridLayout, row_idx, col_idx) + error("we do not yet supported nested GridLayouts") +end + +function gridlayout_geometry( layout::GridLayout, row_range::UnitRange{Int}, col_range::UnitRange{Int}, - slide_size_x::Int, - slide_size_y::Int, + grid_size_x::Int, + grid_size_y::Int, ) left_margin = layout.margins.left right_margin = layout.margins.right @@ -141,8 +146,8 @@ function layout_bounds( bottom_margin = layout.margins.bottom gap = Int(round(layout.padding)) - usable_width = slide_size_x - left_margin - right_margin - (layout.ncols - 1) * gap - usable_height = slide_size_y - top_margin - bottom_margin - (layout.nrows - 1) * gap + usable_width = grid_size_x - left_margin - right_margin - (layout.ncols - 1) * gap + usable_height = grid_size_y - top_margin - bottom_margin - (layout.nrows - 1) * gap usable_width > 0 || throw(ArgumentError("GridLayout has non-positive usable width")) usable_height > 0 || throw(ArgumentError("GridLayout has non-positive usable height")) diff --git a/src/Picture.jl b/src/Picture.jl index c763658..c48146a 100644 --- a/src/Picture.jl +++ b/src/Picture.jl @@ -105,8 +105,8 @@ function set_geometry(s::Picture, geom::Geometry) return Picture(s.source, geom.offset_x, geom.offset_y, geom.size_x, geom.size_y, s.rid, s._uuid) end -function geometry_in_span(s::Picture, geom::Geometry, rescale::Bool) - geometry_in_span(get_geometry(s), geom, rescale) +function geometry_in_span(s::Picture, geom::Geometry, keepratio::Bool) + geometry_in_span(get_geometry(s), geom, keepratio) end function _show_string(p::Picture, compact::Bool) diff --git a/src/Slide.jl b/src/Slide.jl index 44dd352..933503e 100644 --- a/src/Slide.jl +++ b/src/Slide.jl @@ -132,8 +132,8 @@ function _make_xml_nodes( xml_nodes = Any[] current_id = start_id for (shape, row_range, col_range) in layout._entries - geom = layout_bounds(layout, row_range, col_range, slide_size_x, slide_size_y) - geom = geometry_in_span(shape, geom, layout.rescale) + geom = gridlayout_geometry(layout, row_range, col_range, slide_size_x, slide_size_y) + geom = geometry_in_span(shape, geom, layout.keepratio) shape_updated = set_geometry(shape, geom) _bind_relationships!(relationship_map, shape, shape_updated) push!(xml_nodes, make_xml(shape_updated, current_id, relationship_map)) diff --git a/src/Tables.jl b/src/Tables.jl index 74d4c30..ad7f728 100644 --- a/src/Tables.jl +++ b/src/Tables.jl @@ -287,8 +287,8 @@ TableCell( textstyle = TextStyle(), color = nothing, # background color of the table element anchor = nothing, # anchoring of text in the cell, can be "top", "bottom" or "center" - lines, - margins, + lines, # for example (bottom = (width=1, color=:white, dash = "solid"),) + margins, # for example (left=0.1, right=0.1, top=0.1, bottom=0.1) in mm ) ``` diff --git a/src/Video.jl b/src/Video.jl index fd1195a..9276471 100644 --- a/src/Video.jl +++ b/src/Video.jl @@ -60,8 +60,8 @@ function set_geometry(v::Video, geom::Geometry) return Video(v.source, geom.offset_x, geom.offset_y, geom.size_x, geom.size_y, v.rid, v._uuid) end -function geometry_in_span(s::Video, geom::Geometry, rescale::Bool) - geometry_in_span(get_geometry(s), geom, rescale) +function geometry_in_span(s::Video, geom::Geometry, keepratio::Bool) + geometry_in_span(get_geometry(s), geom, keepratio) end function _show_string(v::Video, compact::Bool) diff --git a/test/testGridLayout.jl b/test/testGridLayout.jl index 2edc7c6..ad75d37 100644 --- a/test/testGridLayout.jl +++ b/test/testGridLayout.jl @@ -4,11 +4,11 @@ @test layout.nrows == 2 @test layout.ncols == 3 @test layout.padding == PPTX.mm_to_emu(5) - @test layout.margins == PPTX.GridMargins(5) - @test layout.rescale == true + @test layout.margins == PPTX.GridMargins(; left=5, right=5, top=40, bottom=5) + @test layout.keepratio == true @test isempty(layout._entries) - layout_p = GridLayout(3, 3; padding=10) + layout_p = GridLayout(3, 3; padding=10, margins = 10) @test layout_p.padding == PPTX.mm_to_emu(10) @test layout_p.margins == PPTX.GridMargins(10) @@ -131,7 +131,7 @@ @testset "make_slide places grid shapes" begin slide = Slide() - layout = GridLayout(2, 2; padding=10) + layout = GridLayout(2, 2; padding=10, margins=10) layout[1, 1] = TextBox("Title") pic = Picture(joinpath(PPTX.ASSETS_DIR, "julia_logo.emf")) layout[2, :] = pic @@ -160,7 +160,7 @@ @test text_off[1]["x"] == string(PPTX.mm_to_emu(10)) @test text_off[2]["y"] == string(PPTX.mm_to_emu(10)) - # default rescale=true keeps ratio for Picture and centers it in span. + # default keepratio=true keeps ratio for Picture and centers it in span. pic_xfrm = pic_sp[3]["p:spPr"][1]["a:xfrm"] pic_off = pic_xfrm[1]["a:off"] pic_ext = pic_xfrm[2]["a:ext"] @@ -173,9 +173,9 @@ @test pic_off[2]["y"] == string(PPTX.mm_to_emu(30)) end - @testset "make_slide picture rescale=false stretches" begin + @testset "make_slide picture keepratio=false stretches" begin slide = Slide() - layout = GridLayout(2, 2; padding=10, rescale=false) + layout = GridLayout(2, 2; padding=10, margins=10, keepratio=false) layout[2, :] = Picture(joinpath(PPTX.ASSETS_DIR, "julia_logo.emf"); size_x=10, size_y=10) push!(slide, layout) From 5d57550bdfdb66344eb444ceaca4fd0c9cea9a2c Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 10 Apr 2026 10:37:05 +0200 Subject: [PATCH 08/19] GridLayout docs example --- docs/make.jl | 1 + docs/src/assets/images/gridlayout_example.png | Bin 0 -> 42275 bytes docs/src/layout.md | 42 ++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 docs/src/assets/images/gridlayout_example.png create mode 100644 docs/src/layout.md diff --git a/docs/make.jl b/docs/make.jl index 44b041f..12f3ec7 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -19,6 +19,7 @@ makedocs(; pages=[ "Home" => "index.md", "Table Styling" => "tablestyle.md", + "GridLayout" => "layout.md", "Plots" => "plots.md", "API Reference" => "api.md", ], diff --git a/docs/src/assets/images/gridlayout_example.png b/docs/src/assets/images/gridlayout_example.png new file mode 100644 index 0000000000000000000000000000000000000000..16c654f5f45e4e8e56ccb4bbf86047ec968510bc GIT binary patch literal 42275 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 literal 0 HcmV?d00001 diff --git a/docs/src/layout.md b/docs/src/layout.md new file mode 100644 index 0000000..8a24e0a --- /dev/null +++ b/docs/src/layout.md @@ -0,0 +1,42 @@ +# GridLayout + +`GridLayout` lets you declare where shapes go on a slide without specifying absolute millimeter slide positions. +The layout computes cell sizes from the actual slide dimensions at write time. + +Note that by default TextBoxes and Tables are fully rescaled to fit the grid cells. Pictures and Videos are resized while keeping their ratio fixed. + +We do not yet supported nested layouts. + +```julia +using PPTX + +p = Presentation(title="GridLayout demo", author="PPTX.jl") +s = Slide(; title="Layout demo slide") + +text = TextBox( + content="welcome to layouting in PPTX.jl\n here we use a 2x2 grid", + textstyle=(align=:center, fontsize=30), + margins=(top=2,), + linecolor=:black, + wrap=true + ) +table_cells = TableCell.( + reshape(1:9, 3,3); + textstyle=(fontsize=20, align=:center), +) +table = Table(table_cells, bandrow=false) + +layout = GridLayout(2, 2) +layout[:, 1] = Picture(joinpath(PPTX.ASSETS_DIR, "julia_logo.emf")) +layout[1, 2] = text +layout[2, 2] = table + +push!(s, layout) +push!(p, s) + +write("gridlayout_example.pptx", p; overwrite=true) +``` + +```@raw html + +``` \ No newline at end of file From 57333f22daaf01e3f5a3e30e4f90512724d459cc Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 10 Apr 2026 10:56:37 +0200 Subject: [PATCH 09/19] fix Tables units --- src/Tables.jl | 26 -------------------------- test/testTables.jl | 4 ++++ 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/src/Tables.jl b/src/Tables.jl index ad7f728..fa85f8a 100644 --- a/src/Tables.jl +++ b/src/Tables.jl @@ -54,32 +54,6 @@ struct Table <: AbstractShape header::Bool bandrow::Bool style_id::String - function Table( - content, - offset_x::Int, - offset_y::Int, - size_x::Int, - size_y::Int, - column_widths::Union{Nothing, Vector{Int}} = nothing, - row_heights::Union{Nothing, Vector{Int}} = nothing, - header::Bool = true, - bandrow::Bool = true, - style_id::String = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}", - ) - return new( - content, - offset_x, - offset_y, - size_x, - size_y, - column_widths, - row_heights, - header, - bandrow, - style_id, - ) - end - function Table( content, offset_x::Real, # millimeters diff --git a/test/testTables.jl b/test/testTables.jl index 450e33e..bd01251 100644 --- a/test/testTables.jl +++ b/test/testTables.jl @@ -7,6 +7,10 @@ using Colors @testset "PPTX Tables from a Matrix" begin t = Table(rand(3,2)) + @test t.size_x = PPTX.mm_to_emu(150) + @test t.size_y = PPTX.mm_to_emu(100) + @test t.offset_x = PPTX.mm_to_emu(50) + @test t.offset_y = PPTX.mm_to_emu(50) @test t.header == false @test PPTX.ncols(t) == 2 From 040c2cbf9f02ab4486c8a58489958e9981dd70d0 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 10 Apr 2026 11:01:40 +0200 Subject: [PATCH 10/19] readme link updates --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 716b379..36dd21f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ [![Build Status](https://github.com/ASML-Labs/PPTX.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/ASML-Labs/PPTX.jl/actions/workflows/CI.yml?query=branch%3Amain) [![Coverage](https://codecov.io/gh/ASML-Labs/PPTX.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/ASML-Labs/PPTX.jl) -[![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle) +[![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/JuliaDiff/BlueStyle) +[![](https://img.shields.io/badge/docs-stable-blue.svg)](https://asml-labs.github.io/PPTX.jl/stable/) [![](https://img.shields.io/badge/docs-dev-blue.svg)](https://asml-labs.github.io/PPTX.jl/dev/) Package to generate PowerPoint© .pptx files. @@ -22,7 +23,7 @@ julia> import Pkg; Pkg.add("PPTX") ## Documentation -Documentation and examples can be found on our [github page](https://asml-labs.github.io/PPTX.jl/dev/). +Documentation and examples can be found on our [github page](https://asml-labs.github.io/PPTX.jl/stable/). ## Contributing From ebec0e4a8c3f7cd6d06821c42cc26c30b575ea35 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 10 Apr 2026 11:01:51 +0200 Subject: [PATCH 11/19] version bump --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 68be0ff..20ca214 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "PPTX" uuid = "14a86994-10a4-4a7d-b9ad-ef6f3b1fac6a" authors = ["Xander de Vries", "Matthijs Cox"] -version = "0.10.5" +version = "0.10.6" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" From a0a16f865633f19e66a9dafd9aa599e260558951 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 10 Apr 2026 11:02:13 +0200 Subject: [PATCH 12/19] test unsupported layout nesting --- test/testGridLayout.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/testGridLayout.jl b/test/testGridLayout.jl index ad75d37..714bf5d 100644 --- a/test/testGridLayout.jl +++ b/test/testGridLayout.jl @@ -195,4 +195,10 @@ @test pic_ext[1]["cx"] == string(PPTX.mm_to_emu(80)) @test pic_ext[2]["cy"] == string(PPTX.mm_to_emu(10)) end + + @testset "nested layouts not supported" begin + layout = GridLayout(2, 2) + nested_layout = GridLayout(1, 1) + @test_throws ErrorException (layout[1, 1] = nested_layout) + end end From f6c3604ef3300da8d08a2c1f0bf9532b3f8e8d7b Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 10 Apr 2026 11:12:26 +0200 Subject: [PATCH 13/19] move constant conversions --- src/Slide.jl | 4 ++-- src/TextBox.jl | 7 ------- src/constants.jl | 10 ++++++++++ test/testLayout.jl | 4 ++-- test/testTables.jl | 8 ++++---- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Slide.jl b/src/Slide.jl index 933503e..3db216d 100644 --- a/src/Slide.jl +++ b/src/Slide.jl @@ -145,8 +145,8 @@ end function make_slide( s::Slide, relationship_map::Dict = slide_relationship_map(s); - slide_size_x::Int = Int(13.333 * _EMUS_PER_INCH), - slide_size_y::Int = Int(7.5 * _EMUS_PER_INCH), + slide_size_x::Int = inch_to_emu(13.333), # default 16:9 aspect ratio + slide_size_y::Int = inch_to_emu(7.5), )::AbstractDict xml_slide = OrderedDict("p:sld" => main_attributes()) diff --git a/src/TextBox.jl b/src/TextBox.jl index bd1ad48..e42fa04 100644 --- a/src/TextBox.jl +++ b/src/TextBox.jl @@ -382,13 +382,6 @@ function set_geometry(t::TextBox, geom::Geometry) ) end -mm_to_emu(::Nothing) = nothing -mm_to_emu(x) = Int(round(x * _EMUS_PER_MM)) -mm_to_emu(x::AbstractArray{<:Real}) = mm_to_emu.(x) - -points_to_emu(x::Nothing) = nothing -points_to_emu(x::Real) = Int(round(x * 12700)) - rotation_value(::Nothing) = nothing function rotation_value(x::Real) return mod(Float64(x), 360.0) diff --git a/src/constants.jl b/src/constants.jl index 84fc2ed..3eec15e 100644 --- a/src/constants.jl +++ b/src/constants.jl @@ -4,6 +4,16 @@ const _EMUS_PER_CM = 360000 const _EMUS_PER_MM = 36000 const _EMUS_PER_PT = 12700 +mm_to_emu(::Nothing) = nothing +mm_to_emu(x) = Int(round(x * _EMUS_PER_MM)) +mm_to_emu(x::AbstractArray{<:Real}) = mm_to_emu.(x) + +inch_to_emu(::Nothing) = nothing +inch_to_emu(x::Real) = Int(round(x * _EMUS_PER_INCH)) + +points_to_emu(::Nothing) = nothing +points_to_emu(x::Real) = Int(round(x * _EMUS_PER_PT)) + const TEMPLATE_DIR = abspath(joinpath(@__DIR__, "..", "templates")) const ASSETS_DIR = abspath(joinpath(@__DIR__, "..", "assets")) const TESTDATA_DIR = abspath(joinpath(@__DIR__, "..", "test/testdata")) diff --git a/test/testLayout.jl b/test/testLayout.jl index d40aec7..1f372fa 100644 --- a/test/testLayout.jl +++ b/test/testLayout.jl @@ -20,7 +20,7 @@ end mktempdir() do tmpdir filename = "testfile-layout" output_pptx = abspath(joinpath(tmpdir, "$filename.pptx")) - PPTX.write(output_pptx, pres) + PPTX.write(output_pptx, pres; open_ppt=false) end @test pres.slides[2].layout == 5 @@ -38,7 +38,7 @@ end filename = "testfile-layout_number_not_defined" output_pptx = abspath(joinpath(tmpdir, "$filename.pptx")) err_msg = "Slide layout number 12 not defined in the template" - @test_throws ErrorException(err_msg) PPTX.write(output_pptx, pres) + @test_throws ErrorException(err_msg) PPTX.write(output_pptx, pres; open_ppt=false) end end diff --git a/test/testTables.jl b/test/testTables.jl index bd01251..ba42ee0 100644 --- a/test/testTables.jl +++ b/test/testTables.jl @@ -7,10 +7,10 @@ using Colors @testset "PPTX Tables from a Matrix" begin t = Table(rand(3,2)) - @test t.size_x = PPTX.mm_to_emu(150) - @test t.size_y = PPTX.mm_to_emu(100) - @test t.offset_x = PPTX.mm_to_emu(50) - @test t.offset_y = PPTX.mm_to_emu(50) + @test t.size_x == PPTX.mm_to_emu(150) + @test t.size_y == PPTX.mm_to_emu(100) + @test t.offset_x == PPTX.mm_to_emu(50) + @test t.offset_y == PPTX.mm_to_emu(50) @test t.header == false @test PPTX.ncols(t) == 2 From dfadb53530c233ea8ef47ff5904445144f1eca00 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 10 Apr 2026 11:43:21 +0200 Subject: [PATCH 14/19] add TextBox anchor --- docs/src/layout.md | 2 +- src/Tables.jl | 21 ++------------------- src/TextBox.jl | 35 +++++++++++++++++++++++++++++++---- test/testTextBox.jl | 8 ++++++++ 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/docs/src/layout.md b/docs/src/layout.md index 8a24e0a..1052354 100644 --- a/docs/src/layout.md +++ b/docs/src/layout.md @@ -16,7 +16,7 @@ s = Slide(; title="Layout demo slide") text = TextBox( content="welcome to layouting in PPTX.jl\n here we use a 2x2 grid", textstyle=(align=:center, fontsize=30), - margins=(top=2,), + anchor=:center, linecolor=:black, wrap=true ) diff --git a/src/Tables.jl b/src/Tables.jl index fa85f8a..affb436 100644 --- a/src/Tables.jl +++ b/src/Tables.jl @@ -321,13 +321,6 @@ end has_margins(c::TableCell) = has_margins(c.margins) -anchor_string(::Nothing) = nothing -function anchor_string(x) - s = string(x) - @assert s in ("top", "bottom", "center") "unknown table cell anchor $s, must be top, bottom or center" - return s -end - text_direction(::Nothing) = nothing function text_direction(x) s = string(x) @@ -632,18 +625,8 @@ function solid_fill_color(color::Missing) end function make_anchor(t::TableCell) - if isnothing(t.anchor) - return nothing - elseif t.anchor == "center" - anchor = "ctr" - elseif t.anchor == "top" - anchor = "t" - elseif t.anchor == "bottom" - anchor = "b" - else - error("unknown table cell anchor \"$(t.anchor)\"") - end - return Dict("anchor" => anchor) + isnothing(t.anchor) && return nothing + return make_anchor_dict(t.anchor) end function make_single_val_extLst(uri::String, type::String, val::Integer = rand(UInt32)) diff --git a/src/TextBox.jl b/src/TextBox.jl index e42fa04..08a9be1 100644 --- a/src/TextBox.jl +++ b/src/TextBox.jl @@ -92,6 +92,25 @@ function align_string(x) return s end +anchor_string(::Nothing) = nothing +function anchor_string(x) + s = string(x) + @assert s in ("top", "center", "bottom") "unknown anchor $s, must be top, center or bottom" + return s +end + +function make_anchor_dict(anchor::String) + if anchor == "center" + return Dict("anchor" => "ctr") + elseif anchor == "bottom" + return Dict("anchor" => "b") + elseif anchor == "top" + return Dict("anchor" => "t") + else + error("unknown anchor \"$anchor\"") + end +end + TextStyle(style::TextStyle) = style function TextStyle(style::AbstractDict{String}) @@ -181,6 +200,7 @@ Base.@kwdef struct TextBody style::TextStyle = TextStyle() margins::Margins = Margins() wrap::Bool = false + anchor::Union{Nothing, String} = nothing body_properties::Union{Nothing, AbstractVector} = default_body_properties() end @@ -254,6 +274,7 @@ function TextBox(; textstyle = (italic = false, bold = false, fontsize = nothing), margins = nothing, # e.g. (left=0.1, right=0.1, bottom=0.1, top=0.1) in millimeters wrap = false, # wrap text in shape or not + anchor = nothing, # "top", "center" or "bottom" ) ``` @@ -344,6 +365,7 @@ struct TextBox<: AbstractShape rotation::Union{Nothing, Real} = nothing, margins = Margins(), wrap = false, + anchor = nothing, ) # input is in mm return new( @@ -352,6 +374,7 @@ struct TextBox<: AbstractShape TextStyle(style), Margins(margins), wrap, + anchor_string(anchor), default_body_properties() ), mm_to_emu(offset_x), @@ -388,8 +411,9 @@ function rotation_value(x::Real) end # keyword argument constructor -function TextBox(; - content::AbstractString="", +function TextBox( + text::AbstractString=""; + content::AbstractString=text, offset=(50,50), offset_x::Real=offset[1], # millimeters offset_y::Real=offset[2], # millimeters @@ -406,6 +430,7 @@ function TextBox(; rotation::Union{Nothing, Real}=nothing, margins=Margins(), wrap=false, + anchor=nothing, ) return TextBox( content, @@ -421,11 +446,10 @@ function TextBox(; rotation, margins, wrap, + anchor, ) end -TextBox(content::String; kwargs...) = TextBox(;content=content, kwargs...) - function _show_string(p::TextBox, compact::Bool) show_string = "TextBox" if !compact @@ -580,6 +604,9 @@ function make_textbody_xml(t::TextBody, txBodyNameSpace="p") if has_margins(t) append!(bodyPr, make_body_properties_xml(t.margins)) end + if !isnothing(t.anchor) + push!(bodyPr, make_anchor_dict(t.anchor)) + end txBody = Dict( "$txBodyNameSpace:txBody" => [ diff --git a/test/testTextBox.jl b/test/testTextBox.jl index 48bf3c4..0592547 100644 --- a/test/testTextBox.jl +++ b/test/testTextBox.jl @@ -50,4 +50,12 @@ @test b.content.margins.right == 36000 @test b.content.margins.top === nothing @test b.content.margins.bottom === nothing + + b = TextBox("bla", anchor = :center) + @test b.content.anchor == "center" + b = TextBox("bla", anchor = "bottom") + @test b.content.anchor == "bottom" + b = TextBox("bla") + @test b.content.anchor === nothing + @test_throws AssertionError TextBox("bla", anchor = "middle") end \ No newline at end of file From df2fb9deeb0364f0fedec5f4987e48f88f5162dc Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 10 Apr 2026 13:06:51 +0200 Subject: [PATCH 15/19] improve Table and TextBox anchor --- docs/src/assets/images/gridlayout_example.png | Bin 42275 -> 42052 bytes docs/src/layout.md | 13 +++---- src/Tables.jl | 34 ++++++++++++------ src/TextBox.jl | 5 ++- test/testTables.jl | 22 ++++++++++++ test/testTextBox.jl | 4 ++- 6 files changed, 60 insertions(+), 18 deletions(-) diff --git a/docs/src/assets/images/gridlayout_example.png b/docs/src/assets/images/gridlayout_example.png index 16c654f5f45e4e8e56ccb4bbf86047ec968510bc..edc04ac0ad63f2314a105adad6507bac0314fcde 100644 GIT binary patch literal 42052 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 z z87sBjEQXT=a6AIU!Z6MYGi$>5jf=VS=hyJIsczlAV5z`=9tp(&U>00+bO*La{qIV{7pZZak}M#GM?Lq}7@Pw4m;T&Oku3Jr#%$I;?$ z8n(DrQ0!vi{&BeG#`cbN{UEQgjt~8@PSfwCVIewrsbc;qseJ-1IBDhG14jrX?9~h9}Z@0pXUluCa?AVr1fog{)GsCaAet|jSW3%a^gKFbKcO$ zN5|>nokKjU4e%tJg0;_o=w_Eb@pPr9`l8^${p||h`g=YT-^@!}L|H2Rr+#ZXpVRPF z-T(8=`K?2e>b5D*vHlP2RCRJO8f z)K%O4y&rgo)zX1&Xtb9QaoFa)9xH z6Xk3T&_5GDKg&rOQf7;Nsx|AWx9eSyCk7!9AxW{j$l*4&Q@Wt?XkIKxU$=<&2SI3a zN{YN8c(8C6JM^b{cP@QnX*#XSzVq49NE98G?&h`u`!g02svkLF)cTnSdZ>*D^Q?W; zqXFIi#Q&-{IF4vHJeq1#gA-`Jo_+`*cc7B!n&t0Yd%dzz;k>K0lryD;S4T+QV6S2s z&H!rAar*2+L6{C(avfU*^qnFY>R1MI2sV2<{`%S$73Z^V`aGNF4QSAUw0t|K{Ck!u zC`o1gyBmF+u@IRjcC+LSGTcw4l0FTrt)E2LJI7W{skiAL|8KS_`qXf5QeQblvU&4@ z!c(s01Do-}dNqk)i-?GEuhLdIy6G=N<6~#fwl5ZD zXTcXA+B`5Yo93|tV;0UOzU(%*7pF(Ufy}Oz{xrZ1zH5o79PX+Y6g!mH6PCn+7Zs1& zMuBfVID6+Nx7#qJVPqr^8g^6Wxb4z#5n8J*3>tCnluV zZ0K{$QU|TvoojUdE=o*CwQ|aTa?iXVEkLR+J89l9$uznSUtB2cnqPmnH7?0uO{*~u zeF2xRUgg2Nl|g7D_=5@XbPoy#mdw57*7+sIp=>3UIDdUI$tnpjK2ixmCe!nzaly)_ zr4oia*J@nVGmHF`~aho^@>{NeC3~H*ryoRpvY^6c!AQ z=GnW~=l{{nbE}Bm%?r*a&U{!sZ)?<3T?&l__+!aZx8#6i=w4(iN?e2fL+dGttdc^K=D68@73UuL-!$pns6b{%)H< z{s1oD8be)iZ+<&WRfexH5G@cS?@JU8buM*0Q&o1mA8FAms7Y?Uit$Rlrq|6N+-Mi+ zQ{}8k)?ISITnk=bG^uW6X?bTAK)f*?pRJOK*E#sIzx)R)*bS=-^lGMU z&#z~n%<-3=q>$WZ5)_^y_G$j9)1imE->UKO)tu#5l>#_-+6HAhQVS1{%+vAM3;-NdpNR z?fpmlZmk@psIIem;CHFWuvNXGGmziiLw~%i6*C z7{7m)F%QZkt-Bi$dNp{nVt3pg9U>O|yLWfIw6Z0w?S@P#+Bw-R!o93^1@CZM!|Nxi7}Ll7A5#?eJo!o|50uhcv6} zM$E-~7WW#NB#<~CFd}eqF#1I9jwQ>@-~Xz#f@`e}GRP*ax|cJdbmtdeIm$UBXMIPs=(>dt$* zB77=q}&A{5Fi%Dp@%V4|@pt@wUEh9j`=`xk2VJLrn>_p>(BvB+7GQF*#=D*tOhdcv zkC2{{bOU_q_fPA@!w+kd6MCKt7o-=q)d0r`)pwlCMQJ}oY*w*|C<%6`S5 zmm^=7rc-?WyIn#IzX*GAE57t;x!G{B=;VWw{tSO@k=PDQXUT1UUP^D8m7AYC;9>I^ zDb3PAl~s<$D_#@&U9r-90PojNTK5vzJ6%jNqUGdPj^H=8J%3UkeeV3|zb7^K7S3`3gn^2Gl*sdJs@Zo&ciJ&HpS1=w(A$L(_wwG_R*&CX7ewfQ3v%ynsjtI{2LO<=}0-;uUcL@48FS! zW3;UMT@{*)0vv_M!AT|k>&8R4#Z9&=?3xKhPf5~z$On^YOoB_!*V(Sv77I(nF8#18 zVk>-4*-~cX<_B6gXR&V04yM#oLRrC8t+#cL!L(uc|BM+on~q{05VLyqcz�A0)u) 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 a4%IUjdT!skt6VY)H%l+Gm%&3 zXElx=_O&-o;e=hvJJ_769eg_dpfsnI_(SWc&&7{nc5Jbn*7ZUE9P}4Ax&F@XmHB7h z+bmHmH+JcR+t&uo{>;6df4f``$A>7bm1MkKUbq(8o4Y%R(UY^5J_PQGCGUa7ZO8%U zS18UY1?H>Hk$({T7o=}*9%o)KzTN_1zCJs4YWE=X3$vGh`I)bIFCy{GR~Fv0+sp@K zL7o0TZ}ETD;{OYA@jU5@b4Q9-Kgxvez54r`yL@#{pOH3f1lKU)lIh&7v37sUFwD0U zoC8H7>fF5#mvv@zIn95cduhS@e7x>I$%<&kU8rHkQ1`>-^}%=uLIM0dva_E8xuZxJTl>7viLHzKVtX zex;b|oDPA92p#D8wn0Sfc}=xM(N%X1vtSz|bOCS#e@OoClh#u}t{wBSwte;Gq`GVu z^8n`Avmu^~O~7aq?)?K!LT(&aU4MLUOA^gYR+)I(xAgZ5M|V5;v8Q6-n3{b(6C?7k zCS$2;!`vh zMsg*&#&}e9Dcc4T(T33ywI*boWA;XEICE2WcSgGheRPK$!pQza1)`z$4wc#`DOu}W zS};tE?-ZYYGlcT)%_V6)7>5&_<7S#>(rkO7?1oF7HM4?{uuz}N3SuQq6>U#NULUfK z9=+@kZb%3aW_0<+&W`j2%4xyF@g@~qiN<3`9Y;DCIus?xxT5%X;9x|$RRVSmM5hZ<&J(vi?b_!O&EKabI(LpU-Quu2xfC*+6&-&S%F<0AX2FfF6$H4jMGfTgxfK^Ono&EQo{G9^%++Au> zAIjD5u3y&O@Q#Z`@73%{^{r3hXD!0ZDdSEKrAV|>dtvVKDH>OiI3^-s&{`^NW5l&l z6H~`asJ)25hpR833VMw=l2uc~_I7ASruL3wc29rmjz}hJRC=lX>B_{6YX#FLf?Qvp zHxTk2OU`Kx)TH*@1UI6p=cqAGTNz?gU4E@sA*o{#c+dAJzTB3|8_oD1U zZ#72^u*=&FCCVCF%fp+Ew~!&hBy7P&pl#ZQLdH4co4-EQ&#l=qk?WCFp$m*_vk#kC}rV=HBq-CzLcOC5Gw-+ zbT|7g@U`b&v1JG%*;aEPVJ4u*^vIMB^(n$6#5OtjOBCRoJR=iVW zv0-v3YPgWl9!IRu523SG<`SHE5SU4#g0@S@SgZGG)<%&t8_!p@CYFXSflTYdwOjjf zRd{+T)Jgb^aZ_A{%3(Q~7=Bok7*|fpp@?QyHLkC`D@Ay*7R@94T!tsi=x~w+^dWaV zUZvmL#v$#wtp0^vpQ?!0{x69TRcoFxbkXVJz!FRP8{zL-y?vGajmflX=ztGb+o0s2 zEjFmY;sj@vduQ>?Yu~;|hZ8~YmTIp>w zWwq$OCoo6Mt4sE0(HO=%?H}8*qgziwX8$dBfaI|r;kP*zQ)H69Sl%d~n8r^&+L3&9 zc#uPz{>`Wt)p_8k$V}elg1d&sc7pWjJFU)qiBG2h|UX95SCw^2P^l?shmCrD4F36+U??qaG?OTz3sI=6v8#jPY+{Ccgh@O zh{V6M|DFt|7AOv!v*{oA@04>dro79(?l&^6B;(LnoM)a?vjV3zP7G4MzrA{NTjfIS zHv&Io+}pZ?Aqyf@igWi$>BjhzZXYPl^SUXt&@ekg&z|+v1zz1)6Yga3&YziV#Q5ts zBi+|oA8OJ!F!%|w8b^?TdBDtPmrG}+jBkD%h7)i@=wx=q&9XR{-`I`4p}Du}!|?Zq z{(*bqs2>zQm;@a|yhFfVg@ z;i9q7TvUX_rd&)t>>?{+3JKYE6gpvO#QUA!A^4HdWll7?Gc}fLj$KbKMJNFZTeNlW zcSbR8(@7CQ<9_-=0}2O!H%C6P^{z1&+8F=|SP^w84AkX&1@atPn-7K7m$w!xj0)ua z!cPWf4lXsW;N1Jqz1VuyfEoo5AYG4K23A>htYC9Xv;Xv-lQ_dlD1biF4NubpF_wLO zvom&4;q{^Z!hDJL8)bSujL$P)-zer|${ zA?E1W7aC-y=|x<9^XzKt{In8R!^r^gi$_1vhzNwxz)FGmj+@Yl`$p#E2=B!Q<)N@& z&yV(^f{64VK_FTmmRv(RhTXEVehRQcp^20Cr)E}g*Bgn=7CxANL2;1BZ?7sYRDxJ9 z{P`;-eL5#>`HmzLGM(Q3Fq2ZU*^ycgi{o@TecitPm{;%nJMM~#GtcCt7xaoxq3?DK zM9fI9zB2Y1F)0reO_0NkUrB5j4_m>y_d85dl7ilwr0*6#oZghJg~tuDM4b4iGjPN? zU2(x56rB$yPK3of^rgs(@EIBRVpvTJnlGMgPFio|+DSorTSPD31UT}U#kv3POGoQu~a$OD0HzeZtG5-j|?FlGSn!a$3@h zTZ<)v=0Sw^c;7;N2il+4szAD{MS0BYwVr*k|09%PU%8y1jR{@A?G|r!M59xyZY} zwscXGeV4Bek5zSRWs3TLJu^QEV&_`K-*mXHZ47EvF8=g1KyO{;O{*Om)VcY@K(g}i z?klcX!gRk~IE8?<=+;f{A{{|)fNY|8Pyo(#P+ulCMlAF~!BB>?Bju-OWU!;_C< zL`N&7K%rwH-`TvEn*@T%$6M5+s7tSg^i3)^%W^0!%v-s)`KWl7o_(n2j_EI9; z!4^#2`!$a9$=)uI-w(i_hWjVsU23YffXAS2r-z*9a>HWzb_9C$sEOaBn&j}`lG4w@ zekq*-u|SE$Q>Bah4Ao{*6bGD_$Ll3SN0$I<&eXGyFB6S_CtY`5dRcko@oU8ajDLWG z-PdxHjz|3&p5<@#*xP)mX<`ZfD5IVKIgr9Wb!QTzmqF_U?qxrI|HN^$xpe1b=bka~ zI?8q6ZxEg_p*Rp~Gh>D15CrQeAT*_bjh7kV*UgNXDqSVtiL4_W!?}@-wVbaV9Ah)} z!0jK&y?F0)HzX^RAt#OXLMP0x*N0XK@^?vFO6uuKMXdc%!K;DX0P>!@;#16IsHac> z=o!6xQFIb}95bTai#rnpulAypn8$D#CR(v5{T}@CXuZvQ6&=uB8rVNYA?vT9tPQUF7=1cc{P-8N*p*KGZs}Q84R*J#U!x2D~>;h-+zQKTpVm3>YM| zUS={7L=owDWTid|plC1ZUI_N2nIGh%<&x2iPBCV(j|%;-x}-aVx(l6f5RoDr*V5BP z$XJY~Loz$cq%%J9Ce;{EYDYJSFe`}R<~`6G#Gh2UUuLiK@x0-Y^7TK7$Hq{3CG=lM zwcqHi$2~*YC93p0WO3ahnD2N>)XB>uARPli5t?#v(5_Fke9HH*;PtPMS07w_NGn1u zOd|Gn`uabo*v5$2G)gRwYg=C_=%=7-)+@brv2xi!=CC|zJlMC%i=g*bcolcDDaF($Mau8&Xm$xQY7<1@%7K8aqj$K zhO7t(+0gE=NstczOY@M(mnJY9cIRjb8$+H$1vX*Pj>lu0sRJiWQfmM3tMU33umD{*t_r@Bdeb#!pk{6V)()gIS# z2#|0_fFw9jkVT;(LqP76$URdv~1@dbrdAn=~sJr!d3 z+P}l$HDk+-D>=A6t9!(zps=h_CwtHU-eb}_RMUH_e&3NRsR;NMb$+7IY2>J+SV6Eh zVGJ-hnQ)K4%>MB4(lEMnB8W_Hqahtbm>GU12Qp5AcW7&KQo=iEg2~*;mTRDl&i2g& zID{>M#V#A$eqK(i+gsg=ospKmSsDQF zlqWOmS2(e9`WhA*dsb;AKNIdnt&M!EXe&zsF>&Ujen|b|4mO8pMan0_94;y}cV#_r zskI@2A@4K9x#q#JrPD~bXxk;*nf65IIA>{b2n>6Eura3tZwpVDWOs(R11f=9jcAqo zL!)MhI6BC>F5R4&_anRImH1$CO2t6MN*+&IBpQV+<|Q#O{LCkA2XTjPv;-TFM@n-742opOMe{ z04<^kyKrULwsbl)C0)JnXi2cPpK$3aTJw#wM{9$u4{?ewd7$q~e;9Jsw!$S6&pcwB zG|Q9eN5h1_Hh^tJ9rG6dO8D5I(De64#=>&elONy0F3H0V<@}*S=QEd<01W6bB6`1f z?<)lGe@142!$KBnH|&$wt>V=%UpN9VcFF`u;Wu$FO{)=ge=zeS_+7ZA*RE4blQKIrNtdd?EX{j~R|74~&UNXlz(w)ZI zDo?m(z6iXO5+Jzv_m#ew;7`6|6srj9$xZquHW*h{zW)sTWUmF7+|r$D&-&F4nK8!+ zKuRf&qHaIyd!x*)$yY#oEZHi&9UL)X=_UwD5q@{e>qnMa$gc-W3c}5^%;+OyX_FzG z!c*4`sCMH3AgmJOasl93WED+nyW)kT|LI((Cp-&Py;aefyR+mvqDdQn4f$KTS%>~n z3!233mf)}@wEm>R6V=u&#&QroQ{g5$*&ni~(OQWhap!(b4?!(rG0w38oMXyqkIpjO z+u*+QAJSc(%>L-2eHq4hNJPeaucDJ-rR}FRwyO1@y_|~*H_G#41Gw9DW$)4J*lKpp z4#H%+TjRKo^7im!T*qg5E$uGP!Q^Yr$&PVo94R8YId+mzgKI?J_e0m@M~>{2bq+hA z720<#Y>T?g>?T*?0-b^|m+70YtlA?+{`@T2=!ES0Yb7y|9EBb2Ax1*=71h0H3x+T_ zIw~`ZxFXyt`aXaZ-)cc=_X5PFNbQg_sGosnwm*|!GG(ld!ve0AzvzV(tC9h~H$fkWV2 z0Dza+0U6b^Yeflcw?$1J-lvg!u7dZSW)Sz_LIH2D1$YQCe;JGOs8$c37>xRJo8+S; zPi1OPEHG30J=;y@((BpV&7ELg+A3U4ue}7YFOxxb?}(aYUd+>$M!1InqIQ{YyP-#z zd5LPYdCxY^0Usmce>-De+zBlpgAf7``pC!sO*Q{_;r|`FP@+wxnGm?eZ-8WGf*L&s zNk?0z?nFJ^0@*WTWyup1T8El=i!*PU<^PCS|3}aRVetPaWm2f-@kd2gmTUbo71*qa zy_gVpj}YO+k+Zh0H7bP{=hzGM5<9j{1p&qB){obX!n5=|+NG|pjd<_DOaqCpgIw_T zo3}T5u5!32$Yxt6dn*s0;dqX5Krp;W(dy2N74$T!C`xP!swBb`cL1@TnOyWgbRS(H zV}4e9FGShB_dPT7j0~fls_LK2x-cobzMunanclV58($@pe@`Rj8!lvCalWgy%mc)x zJRc+6-^QiF_VR9@y@{?7axyXH&&dhfp@}PDdzdO zFmj#OHQx^KeDf+{7TvHDzXK;jW@l)^5aiml+R0nn;S}2Rmy-No>QDW(_{q_ofYHnS z>|jAqZ{~=?`768pD>+B2@b>)y#r?j>9@^?rc{{#?`kU-5#^;70h@Hx+e_GP<+cSy_ z?BtDYaR98B00uQQ#UgX-H%Z8<9U4)4EPSmi{YVikZk@Btc8gn^pBw{dr3ww|(nXEL zkoEB)9Sz#}6Z%kD?Wpvg4fpk@hd-^yp4Ncl&X4wcFhtU*rK_nOFbdTXY>zmAw&9cST5xMlP!2S17+W8;aB|+n)E-3t}!8$qN>W?|TyW*gljhxT)`nS-HpZVm^=4KhMj!VsyiQ0T1DNDb^ zBZ%LZzVI(+ArjqL+)_}tWpd6>5HAiGTX;cy$Nkzr)WgOA^r~W#8wLUEXUJ#CyW377 z1u-5JzUND+mJV-eQ3~-M6VI#9#!gPmFmhC&4)FX+1C$imD&i=-q8F zYM@Lxt*5V>2b#OpU({>lGBSkUU2pQhoX@dEkcHn}+e|Svykly;DhRhp{oaL70h=wrLYDLaVJPC)6Wjp%*e|Qy!k9cK5?^vGt#OR8VKbl_6;Gm>uALVcY<)=3)%eRCqz&^8h2 z$BQV^_LKIY3Xg;4FKx$RoOIB@?@?hLXP0lBG%9#aIN97z<#JL81sfWs)8^l{nzmB1 zX~*h63iXDi425~UMqQB6(t~u`geC2#jND{mV!$L(_gU!GQ)9AX1%Y@!!uC4_1if=Y zOvQ(iwZ+}7lBS+kwcS_~pqj<7#gYVy$@j2wu26=vAC}O~@i-URUb^G61hA!;`pnL0 zxXR#~j{JAarsPROBL$gVL)m*KUhShak_Asu9Y5Z(alN{H!M=R7J9Q|;J6C4XD8Xq` zhgL(nmfZev0)oIai_PYz=Ir?fciK+=tCvVqL9D3&u8)AMiwY)}meGcDlhppa2^V>1 zaEsF7m-t{s>y{H6*ldGw>hSuAHZkz-hnC)4d6=QA<&xTU^>~#(O0&5LI8Yl}jw{?R zD2K-qaL=%6z;V!rVoay%>Tp7jP2ZjKQ(ARgH;HYJS1G4NE8h61hg8YWgyKR1=kjCz zB%3$oS&{48fGuDv^otNQPx5Q*Ou~#D(GsPZ{tx&0-^~WlW_uHFSSt+sx8*n;$mn{U zp%emtP*?(~k}o2ijp)%$VWz|ZI0m*KVeA}lA$WI!H3#JaUy6Ewm0RB$VPHP^# zt!>Pg8vKCdMwJylj9fER3Hyku%fgb=0qbrd6tEMn#zQt(f}njo95t!uwN$0|mwltA zt|C~B5+2WZ42`Fmtkf#(8rB!D?$eT61V0Ol75@Ra{9p(@SWa99#0NTQ@x+(-cK^~f z+y<@E7K&9$WGy!b>mG-^b28nc?vk?^MNbK8LcS5SuZ!(mkUwUL>#>vUZ{+wX01~9z zp`X*VlD&Y(=Wh0*Sa93V@vL_~e`Us0FKY3GH6s5z$cE{9_VCRw;)N#Nq!WnCpQLmA zZFr!Yl5BFgiwVf-)+iT}#mO8AHsrG!6NzyoOH~fbIrw>atrU6}6!jR5uPDpXT>sl+ zxktrxVyF3NS4{B7I)0$dXNjvyBj4L z!CGiy!qcyqW>(?%COd<6Pr4L^GD4GwILAKWkIiqw=~hYJcIq2jRe;Pa{Q1?5x+ZRj zF4b*Zs=Oz3VgfX=p!?AaRqwy01f?jQ(b7nmzU(mT;ixT5yYOF9A`oXJ%B;lgcLUN-tvsj_J7t{nX`m z?$%;>1$HS7{l+l!DPqa-6W0Mxta$<~E-!zxQ010$Kk#*|a!iu=rkP9(zZ;(}=f=Dd zqG9Y>d~WjWskL|-FAc&jCD|HBEVDFY47|v-l48OYI1$iPPj`c77)d$}n)t;&PH_ZZ&AZekt z^s?6Pmp3Z`VJWGtTU+Eucvleg}w_W=f z#a1KJ0Rf6Q{i(rW-!p0IEos5qqgMz+s4eMek)0!vU6U>m=H(tzKD>jiH_ zaZatbnE=1cxF_Q}eK-zw8m^sl^RjU=*;sv|$+UG14;0N+vIX~Iad<*2Wz-fj>8~3o zvN*$ed1GeyBw<}MrI?}T-nu3(kM2fnPm4n~Yy?Sl(&A4dxGDt2>0kAG&C&~!wXP** zNY-A5HV2BNCk9VevjPlP{pmGS;~Hbe6j<0i!lq_stGXfU^LH&(Z_3E9s>94z9pLmC z_kR1sTG)#@qZ6jjc%bpf)CeZNz(cPAKh}4}VthPW1szUMVVIE3*w1%Jn5of!zfLca zPW<(-EkV$|ScVO$Qy_35jX;a&;e&733ln|i}`#%)@$LNsSeonBCic) z;ZnBO@tva0`q0IS^EsAk@7-_JglI}EC4R0hnl3EeKA=~R_dfiIkSFzGV%RmzbXP|z z?L(>EBat(Y5sYRgQ1f{dD%*bBVo%xha4w7tmCAnl00`=33Q)?S8r3#Nu~;^fM+p!O zk`zVpDJ{@dY?^WimqlImQ%Z&zVX!%)(iJxhx3^0?!qu@+SC#06HK)OM*K&;{i*%Gsg%yw}wsFUotuG4?jQiMx+>bPt(pyha7ROE3;7_Bs^ZQ;L zIo^wEBrV=VR9s&7di|>B>#R1930>QxALWCY(-!r<7(eADvO>9TOcpsI8ACRUvCNIbtYf*lTpR6)#6UPV6l zL3Hr*WN0&ygb(N+?-XneaC4@WM1}P(c`I8HdXzWXLnF|^!0?y+5>-(=Vo?K5OyF^J zm?!1blp`AUEgiTv6t_xNDL9b7-<#}p@Yiq6C?MYI4Jf^tuEB6T2a)<(Z^zT+1Jpz^ zch46&R6V@MC~*`|a4YH|3Z9WX=kO)evzgoVRQ0r{h$a&|9&gxwe!PmPvGXuvLH6ff z&>zuaM804=|MtSie=!0+%Yay=3r~}dWWiTo&vaYNRk*rcu;9=;^6U4v*NY8G#WtS= z{JjCW=&tBHbLq+H=Id(gi)9lnw^33lZp@QFs+EtfU%zma%AuvYiYW3dDr$HCJrfZ2 z`;~rS?t;D7Sg1`?imlcyH#>_-eaxswiQ)o8?J2bF=z+509FD|yQ91@9NHm1{XCHdW zmp%ArJo96-;^g8j#up|MCMKAC@! zd#tg}iDDaO5zMrFKQfotIBP2^3U{C{iaT=zxNV;3s-{hbfiU!UhFtzyRs)UYtScNI zQ@M58L?f}bD2>CJqNmuB5b8rvDN_U_!Zy>axg8NVIzR6x6cRGQ7eU6QgwZy5{_@DY zkmU3*t+H@L1AwjdH4^D2X1XB2R#w7pBK~TPVK7f{< zSym4XY$vvl?-#3ffKV8A$;E@92pgp9r)<2koUG@ z07kew8GOi!-G918HD6#(?~^6_=??5+MnmkV@rmClPi!K*2k~rw!3H*JL|zm_+%zd4 zR)C|pNmdH1-)1d_Rq`B$(WXC`P*#`ho!-Bz~k7Hf{c-$|TTB9T*nru0R^S!gQ#EJ^jhg{7> z2TO*(sJ!?>icu!q{iRwP=0WdqJ2e03o}~`av|?~Lt>*6EcdB|$+8(KahZHThy%lif zx0IfiNvn;vq-cwHq~dvA>kPi(LKwY<;2wW(c@MLFLhh1(yg3{!S=~a2LA_(u z@K(Nst^hA`-GYNp;nHBj@$Woe7V|aWYM&QMI9x}{Fq2HTRj+o9U=qO?{ zTG$6D^k4igquvc8;?Fo74*nT`?#5caR$%ps3(^}=a)Vn{1&aNyQn|7Zm8MU_hz zP|umly5wl;{_|wxOYj~=+~a@+8_9JD(K0w7nIFzoYI6>HnKMJx&rvAgB5R{b2%ow8 zy4|JX^M;-kai4?Ve`zH>Nop7OO8!F9eRHFptPA-MOQ-Mqf^1ByFxCS1XS^v9@oe1- zC+4QftK;Gs76GXf#TVznvw5WT3 zBS60@i9$sJX|=B%D+bw?N$d&3(LWn&E&dV1x=i!zPI+N&GSsFMt~RVYB*WfsH=N=> zlE9JaK~g#+uXu+6CNXr7!K+b3XPUN>f&mwa6!dfVfPk8bs0Dki1QokbT+(Z1sbpQh z@JVDz@2|S+k1WN3fz|wq9uL5Vfkl#pMN$0Et?d_zL#8Lc?9S)zUIf*|{3Stpes|DP z2Be|m5g*}lN2%+qLNl{8fMTyD>KP)$*);aHKWoWq{(5$(7bU}%JiWmzaZehO*4dti zaxdZqa@ss~HN~@TeYIm(nAQqCegRpO3jyQ?cxp#oC(RD=!0V}m81$sO6PA z^)zU;pnF?PxMDPO(^{G2pL0R);m&b-qir?(85ek_ccj^+s%}Hm2^lHR4)e&Lix`1^ zYEU!iyRF$@ktB2p@GoD=L>$_uX_j0<`BQa%(+ya`q)VlCasv4%h{Q0D3!O zyIs-49#m1yiFPJW?xv~ZAUI!(dhZtB3mEeYmVNvLI5o~FT*6>L>fn%L zosjr2kIXhO&;3$dnE0tLo(*gg0eKbAF5vk2C1otc(pCykb{{8ft;A`<19Jr9ki2)j z4fZW%BrTD}D*TWP8(|1-?6YFbBYs$a>}7`PE^alo;{C@Nf|dZkh=`Jh*)9gXnSZ%U zYWv7DKEJi;y>q{pn#q8W-RM|UD7yIYY|57&(){dytqNF1XEQY4m+Vk^f%ORS?bC@3 zOZdIr5MLqa>CWf(o4zH`fn;STw%IEDBR^K9{-;WnTTA3WBlAVMlX7Np#cgc4B+5y^ z&~2-M2a1?)wC|vCKQfbbIvh)w<)R7G#23rz%1IpyFmv15e_ed4G8|Jwmx9nRk7Rq> zc3sMK!pEYVjKYRifBiT1CuQDAsOK1k<1ggUZkxHJCudrv`B#hwmur&s*QMcZ+|5w* z%uUS$LDeD`SihoACgX0+FzyZJ}GPab(EynMoos zqXMzB*a@?;fS)Li8yXm0)NkVB)mAK|Gbyiw@375Se50g+3ALrO+cV~XWa;=;{)Lvz z94g=6ww-?N6kT3NbjO8O;jVrAH3-_qBf?xyTenqVN-vTfA1YaK>7?d9F}uTiGnK7g zB3xR_p?<~7Y|jA*{lFsdzgG~#zC@zu0s_swcwH$hGs>S4HLO+BUS2j`S@+N;q+W6} zVMa!NjY(L~NZ)Z9OETjaP(3;QjuFxaL4BnlqW0 zt6Gbo7KjHNk9D+mRBx^9A9PNb7P#O;3r?uxw1!AzNwfB!$$^q_)1DO7c(P3?Zl$|j zYXc3;fn$xqM2bdEFe#2YF~}uu@jXlJs)3xsp#EW4jjcw7)gITs`fRsd<9f8UST0Co z0_N_B5NA5Jq2bfQ?)?+*bdkNW7GTWl86PLi+Z?m8j2FY+HCIgd#PsUAQ!TlQseHdT z_yjoKt9UdCCd%~jN4yWEQtKkPuA5!H;z zoQ!Zy@#RSW07TO=wR#Vjw9Y5G=`tCW101^){+2E+jlVIBUN(V?ejP!S6Oc$5(B%hb zELb1bBV3Y$<8#tDbWP0ZJvTXKc+*TML#CH_uN#j`geU%@ls7KlC0aXBPWT|8fs(QD zpS&lVLg<;(t4{4tWld#mWt|{JCHe$MJ8x~ca!cbXjL>j#(lYYR`URiYCsf8SC5F_;1dERRC-zF1>RprppRmOjSCR#6vYd7+S_5y}U@lMw`ox`Qcd__w1B>YlK`w)JA5Lh~JY*&zDB^3%9@~ z6l^yPv2CqvYqA>Ra%lInF#0P@nkC^p8J6DIs9`;{5pQz7ec;HsJ)sWR>#$Rur^f0q zjAi%6p)iq?)Zh~Y&a&||iCe<)b1Qbo*BswEVcYSNNKCLOl1>@#5I>z#qI0zPZ8svX zquqw0A?)Z}L2QWaV6u}+C`dydCk{SnVzD=&P&$+^2dt8MfoEvQot z^Z2!t(GPjtCP`7MGf8V}PVvPnUdF;dQdutx8@$j2KKEANpKIV2VTS+Vag!55{Dvj88(xa~U~YuJKqh+QOf8Ywa{=o= zU^|(s-mvR3R+{?(!1zE+qm-gDv@$7MnMqA-TrDTKSN3 z|MH+UIdN1t<(7H^&8NFfCKscuYSUJY{Sjx~kEcqOI|-8BVo&mPIV;ja!*#%+fK z_tTA<7!Mz*vaozU&1{~cZvAD%1tIi))%P>+d(0(5bpM_Xonuyls%{I@ssO!`T}`*l z?thw}*D~EQGZjvLnVMmN8OL>`!hot-RT%D&T*dvL*bE}KrRNvprcx1Lg$k225;;UYYF7ui1$oQUD`6?{NnkJVUz)0txM;@9I zzFCUyy|G6>n+ym~Jwe+WKqL+)=?J^Qn5*}QD-t)`#ocFCQ&!($Ga?uUm$IEXQBtS| z%-nL)dPs8|*WqqZ-kEl?HQNNHPD!1Rr>yOhf&G6Q!BFIC@ni;=XmjZ+GU{eJ4Z{Ve z7t!rvncWKUI*rBrRat;2bC?n#g@^;5moBic#o1U<@!9AxwGmd&9fJ&01B!+b`PuA0 zEKdvBPD$Rz0@9@tXo6?Mh>Xc}qGLFdCV%86dXi z033~s*FQ-={){R7DHf*%PjYB)`_?}G_V3b&Yi`%jf&ccUPU%{n#KvCO3R={3ik;F?)GIcJ@-HVjy{Yyv1+W+GS3z=WbK+w&0e0|Sq_>7+RmAP zs!*Oo>s%lg3CB9rK^R0=i%NbUs?wxu$wx4PlXp!*HqjRP+~eneG&!G~O^V(ZnS%=` zAF@nV`CAZ8V3OvzLeon<9U0~(W)8bg+pirPkv{TS+-fW9WX154gDe6GV8r!uqK$36 z{^g!g=26t!2pf7na%Ye6{ciQM*m&aHn#|^rYh8ek&48RkfGO8b$f0Sz={NjfE^_XK z4X(S6z`LCcxax)#mF0qsMgqK`C>I`zVI(mUk+;_Wymig__CtN8sp+mSm?Z9mE3{D> zDcH@o`SE>So-|PmF74y_oO|TiX|(~#!AInv(Bg1e&;AXRCB<)n%x$}!JuJ2p7chpJ z<)QK7tH7BEXsiH88|A8gQcu>&8!xM!JOy701ufnhXxCP?Al|yOG4Bf%uIMC>WO2x%y(>j^W(YfsAzhAng1ru5KDt0v2mbaQ+;1O zmaC}cU`|4=(A3s3@<^~!Pr%?NFnq+#2PbU8!60Qz<#2^0TY_^bP_||-+a+sq?(6vh zae?g4KZ7twv`cAG;+)&3m_AElyKxG}mdfrE$@bB6e-R<-TYY_tKaCx_ z-=*r3Dbt>aGC9(4@=KMZoz+E$(u&~kdQE?tA2K!86W+Mxw2{)}ZPl>ZP}?Fk@}um5 z$u~!b8ohUE?k9CzV}0dbJa>#8&Sc||I6Q9@$;N&D{v4lUt8S)*K)mf?dtJ&f*RvD1 z4xi&n=$Bd6PQwd?xBB;|T}-Pu4`jc<3y*F`IuT|^h%j9Nvy(gJxUB`n1?v#oaKJv_ zFId)d3{$)rB7Q-ui0u?Il%aF_mMY&A7}#7O<4ChYye(}o{ZFPg(NiC>;Qsb< z&rnq;;o4h}`~35Rt;tF&!o*Fzj6;$)L9O+ce$fxz+2I&He;d}GdH(nFbz9=*hT_q@ zCp;x0lCcfI|5|nKUfORA@YKrj`-m_Vs{SfA;rQl9hc7Xa1pH?q(=CM81UMpu)4FMe z?el^fLHqlg%T6O#+IRj}%4~nD1q-d6iWdGuEQ$MDCGd%`mXMn~gOW3qg?yJ`&F6|v zIrQQAFwghENk}x(z4X4$-h6 z3nY+Fb#jlDoxjoY%Ig5vBC1J&Ib{RHnaCCbi)s$h5)cYt9tyop;K(8cMcb^0{Wq`^ zI%-Ij$_X=*8fp!^fDdflkdB%3OBkqqgy1g`o?9HzNuiN*9Hyr)dqhh^8kKoVu*Ayj0W~86 zdENFfH};4S;aDx(l|MhS$6QgSO)Hypw!fjhrp|VU1-Wpgz-#0FV^yLluK0%yOD}Qd z))AlW`{Y^4&!S#Od=FQc_}Ow#O)+PX3LB7l#YLY~THa0$LaO~x=4%_ZSzl^27tF_p zBu?)ST$`;d(6dQf@nqfu_Cy0l)dXMTLbSde5^@}+;)p0O*b>DUTJ@=!5~Q@$MEGqm=hN@;=kP8yujg8+{WExp?*|CTgN zsnhWCKEg#6`?Q`v;%o~2u3#nwQolXo(T_V)*(qSCN z`veyGmc|Un?%^q(WK%o+GcOi)Q*2f5XDWM-_)&q=2s4IL_Z%!$6l(H69v5ujd8;!f z)2HELVna(G?nTf3fXop3>dS?tP)IkcBQTbO38ac4T(EsGQ&1&4T{;EofH4Q>0sv{gcq!EjrBdU#zZ!183v#zYWNaltk2#e*3yOy9(19cR#X>tHEJ|BD`jCeBUmaJ;lr09)=}Lj?id3$xEJy zAdA}Ng*_SYsUScEI-j^OWIHg$^wsOxAMn6FYWp80tU$$&xXvm^Fzwdf`k7p4LhLy6 z4H)`z^$ua$#lpZeUQQ^3@6aGhBT*|`3B9d(BvX5PilYxD^KjYSKDmFxE!y$JlgQV8 z9eX-sPf{?RMw+j~9%#74Gb{&gbKqN8n?G8WD_^J87&1>* zx!0a}s(F>!DFD+ZzO#1ZVT>Yw@%3&^-{`4p#dkMja9Tb85niWda$gj2^ws$sw9& z^pmiD&r1v&ES|f&eq3I1u#*YgEY)b1`8LaqLftiDHOGGsC@!q4zW9#1l5D&+8MZSO68pQ7u?PFcWhqwoyDuH~ z_*?4}l`=*xQMXY}ZK>TfeR~W$D{uYj@6^*|V}sp~-;KdMcw=_psJpulF%STBXFT^b zn7Rr2g!ykZnAp8>g7y%)US2SHKUWJowxhX2?VDG>m!<(e{X-KD1s(`{Wl)1QI%fv} znOD_LyePB#o%s(ICwW#%Cf8M864f?_of>z-DSVw`3>|1ZquHGv*g2VZ`i+X!%#OCtWhknHHWtf}MLLz#$&>12S4=5a(YYrdTgNjdbuiBK zfU+W3Q^?HmI$QEl9918-IRPK;gV?zA*sR!eR5nN@`=kLGi znI*93KcN231~8)X*fghR^WuE!M5h0TFafXJKNIV%IY+Q&e?F? z(h(e!wgG8eF8<|T(8t^}#V*d%X+gqnxl`{{3Gn|uKt!Sy$AmOY{9oTLtzDtFcyW_l z_Te^`sicZAB-)*_{MrQFJsyT0gBJkdBAo4%oCVXO2XFw_!SN#B2k%`6(*$3GR1su^=GI6(N zPe81`&ixH3kn;Ko7*1d*@T?(UKIN$Se{uHZfl#;K+gd4=JY|Uxp7KPN%2u{Ev>}hJ ztd+7eWM4-cA}voz5sEC)G-b~=A{5y}V`41XjeW*2+k5VLzTe;PegA#^Rm^;rdpY+x z*SXGh47Ue?=uMyvHv8p`#v3B?VgD8=8iYuI76BR3OM+l0j#~3mK(TIu+HMSvB zIM?+!eL86{HDtBRqn{~Kz>g2xmrNbbUL@V0)p;S|vpT$jQ&;xV_%$EKF5p?3F%e)M zvI**pC(aT=+4&?)m3I(rs#W42osgQ3eWL_#vJP>GtaO@iM#tWE=?0DApp(L4<{IW} zDW{~7y)>7rJX(4po{`-ahiGj$LMvAImD|wJIW-`pO;;aW`+DeZd+hP4)1-gPV9gf{ z<}CQ1$()x@vv%0h@783`fAi#ehwu$XxMH0l87JAjxkdlc#i$b&A557IbB2T??^G?0 zE`97yy5OpMt@nDuU}qZJPt2b}PUA4g7I}(XdJA3sXA@;5m{CK+P;Z^qSYm1(YM2m= zlhIP*xyVnksY4jCf!(AIEfvdN0a#F)Nl|IlT~3@%vPo@v!0DbX=-am9uRfAvxbSG1 zDaG=+^i`Y;frVGy+_L+a&STDW3vYAlHqDv#X`2F$9HUEC*}w1LvD*jiD@sXkU*Kly zq9dO0eiAsJoYY4xAvp<1hFzx39d!7fO7CB3@@nmiaHlaVm#?)l$Nc+7TR9CU^Bs)) zG&q%f(P?G$n&}a_wA0JygH~GcPK(&ppnyK&^0mX9R+D`GZI$~3f-x&>7SS$|eUzY6 zy2KD%s7F)SlM)`tE+D5a3{P{yXI7bkcRn8zdb+L!l@0(66`k0yE?=#~sd_xUao6ik zsg*;xf6}DnQuof#4GpfNf*nGnEjw$AQVVNnqAv9Quk4Bul^H1N2ce69r8T(JQLJS{Hey>s2bPdo^!0DxYSQWt%q&QV#{kxhu|uZewxh6M>a&R^Twrc&)t@zenT5!0Q-qI-yn9 zkr;09O8W=H!%CSsUpLL368Zg^5~%BML30@EPt@_7n$7RA4_9P;sn#JZ>1J4K&Me+K ziEqxkD^)r5EpvG#WOY*{P4K4ayx+LyNa$ZWrsA)E70^yftzMA4aler7ZLA1y9QE%l zC!3CY#?0kc=*CIgIVehF2NotQnt3kY2LRQjjkzVB^ zot4|zCid8Z)X4DT{Ki%BYg6GvPl%jehODcxzrMwcM8TM|3ESznaiX11rci-mV804! zMi$d#DPC|kiQjHW?WLLu9L~KM z2n<=Tn?~#R696QDP;J^dc)Qr=lP^#7JqeK_$oYK4^F4QyX}fnlRv@rR#5ZpHZxAA| z3qYlXO*;p#zwGGa6Et|HJZG^(gg&|dd+eU~*_Tz;et@D&1tY3gX|ZAD)$l5~G!l_Y zBLRH3IHz{&7t*UrKImxh@Uxi-!+Bm;_7t;Tqw2BMd^emVi!tGuL1`bGhs{~}LClMZ zbqx<#3cZ3mXAggA)de6mnDYb4C1CI8*ebHWAA5ST`KOWtXBF^+CqeJ$nQn}T=3@_U z0!7V!%hTccY!J=KS}lPpR;8ZYKX*Rtx;Q&8I^u8MuN*>3Yhqx2IhYILi@L2{jXCMd z%a?BtRXreXZ!&wg=i%5cso!6C4~OfF0;}T3YKXx6C3kNTsIjzs3-A#p7H1!@fEf_((l}I%?j~$)DQf zMF@(Zkr>Z7+b_R4#aGd+=kcsE^r2CKg{8y(=FHAb*j>4UTd*gO{1A)jr%_(t*(-SK z%d9}wjeJ3A8>0gah~1OL&c*L3DVf`^lC{s^yQJMcZI%nM z2A7c*iK<0fsi+l3=MsaTzIy1X_q*(YBVTkyWP%=ge*%Jav4DU-VS4{lshrfcRK7n@ zos4SN^3=It!uW)9Tm$QJGmvvX9p>4X&U`s3IJHk#brKgF@@EZMr7c`bp5WY^ROCSf z6^$%Yj&K+4^@;S#+MZVIA;!`@{vx?fH+WV6G;5{A^JSBDQ={IG{(c$s6C!x$v;%zUs&U49**b@FY#(sDfI+%Jp+@XaEXti!i1 zt<;AnD!)_PZo2m7`kn6u}RFZ!f*ZS7}qt{TS!5w__PDa4`vXSt22js_xMaLE2MGZpnSmb!0ctVwr zU@@mGd``Kvc91y6{%3jJgX``Ad$w*N&{bJboc7&>(Hk@zznJ?Ez2RbEu!~%~dmvky zcSOX?hWTZ-^H=xw%9~jKfrbJ@6HRqS6F((1a{&1d;T+!{=Vb@Z#iUL#d)q=yf{Lvz zi1976q?p9R07N4{dA(q}TrQT&hXDY3G&Gm0d;DShM$W$Il-)S$J&RXnnmPQ>g9` z&95xnL99||qfF{arp-IrPpBbk5_~AqWSQ6U>w2Qy0{#k@NjB3+$`48_qKJ~?7!(DN zxT5ZA)kqCwY8{Lq9C)TS+CYmL`(s(>XZXi`k{UDFH0{`-`Js$okA6+-^A034Xu~{_sKv+csq+`{6ZPHiF6hs{+HB4a_)z+&x2GjE-SakiF3-@uQP;QQ*7R$# zlCf{3<@^sblM$;loL!FR6t1L2xw_3*#7#UVCpCQT(K^ym`c?uX@o)i|-T7AWQqgXa z3yg2BB_*D7;;Hs?wgKM`>NTe_<28hNc;f$_4j0$)mHk1Py5(2$>&7`r(VkmmZ^$BM z3%2b@Ab9TU-N&5kb?z`{*fyddw$&ioWTy!L=9pEoK{`ulEW7mcLj5Yfb%M?C6CWDzEi23 zbp?H=sIxx>&Yv}tRqp+hIi9CsMLXd-<*BQL=ZUCoJnCZc0Ro~H>NfKyb0xR6sY`jm*k2-STZ%&bY&ofPx`0vX zbFo%({Ldo(qORT}g~@QPrZKSv`Q)@^%Gv0n3hydz6psXpT+6GZdb+lJ4fbhoB3^f> z+#foFwLkBh>$)gG{)e*q$&=Zas{%v7(=5ZJY{yrW{mGc#PoO;K&yZNTr*WP0Ax<|| zMHFiOCN2BKMZDadYlYZK6%+N-gN}r!iY$esQS%=8Da<2J|IY&XLOr$$&ATG-m8&7N zVLa!Zqtbm^3CB6lqsBftbAPS@WEPGU@L{#Sz&R?zz?0HU%(_Yz&Soso1Z+Ijk2bGa z^_~9M;Dn-$CfgjGX_PF}j!D`;T*n)s9m*O>?0h+C zZVt60n7GvvPEb~ciFEkngjIpUXh414t;j(UAp$mJAQ2MVtihUD0e9*wMTFxvLB_SP zYNHWQH|gOFS26e+H2kfG_EJ(C2eO-1}h5k+#242yDK_ z(~jr%@^`F#;xc1z6We?KF3}-1c%PA4y*s`twJ)5Aue4OA_U7TQFS8p?c7twm+o3xI z#nBcA_I?4Ki5ty|t%@v8&ihS~$Aea5n~qiiXPate|1Fi>>fUMYs7-AvSlQ4zeuG2h z&+l<4BEX2?j?+n3BWR0y-~QUnUK(;IR;(yz?T@?}OfgZu)=CaFNs`g#6eNp@MJbn# z39!9{U^m_hHpxU9a-)B+i=W%fu*}3vJB4D-2}jO;8WuN$+AuBV8tqpmETE#LW<`%`$U4=A6QH+Yv!W(_@bJ$31wIQK?Q zaHAL$2;;poodf$hW0{=5&59QYcQu&KCzU-c_X;u%G(HuP;ES!x^BylG^tg8vGyl=dP~WWE7Vg8GA<=Xk8$W%``PeBuU^1(4Lv3k(vRT; zka562*qGDPWTN}RVCmYttiaZ{hkEb!uZpaJTJ&RwH(_nQ44YKFG+3%Xx^|K}dARi& zo;7h-Ro}#Zm2wL%Kb9I|YC?)Q5LTlLY9gwMb0zxY%OpZ}s^tp*59j&ellcK- zXZeqfRIE-PR?=7h&aU`+SCg@r?!O-H_5$wa!f1oL-ShvmWsS>Iv;nznAsJ-V%Or-I({e`&*wUKb$SFtbGdGVG>Jbg2Golj0 zT!?8xR>cp{3iu~$&4nWh_yS|GgSIH>6>Cp!cgMd(ik{7|7X`)N&l>q**`r1Z&KU-u z2E^M43H6(>jDksnVbF866hC}4e!O|S$Z|dqYOY=6!31n)(=fGEiwkS0mx5VaJ7W6p zOJ1dycZ*w{lMQi{fQf3}2iL`jm8Va>ig;HtbJlfzTZxdReel-=oJ9%5=v&?@FO+WW zSqjXP!tB=Dhh97BBn+!M$-4{IO>lbusk!ocH>H>Bekj)%g@EL6^49$oc8Bq9)WAHA zymwXqRDFdJf(Xv$3v!Hb75{9`?LYss=50ZB_5EP7vAh}MLwEP=nT*euFpD3F;|J$I z@6{pB^qqRw^gt8MPHbN1~v%H2OFQ?|anFUgtF z$P~PeU3~8!PKQTSO@R&Ju}tgQ@KPt{a#)RUUiSXT)l;RPdPjkduoKty>T+1da>bcy zVw_QNR{+eii|!zXKbWlU{zg!rMDX3Jcrfz#uEi<=_^%_Gb2I8rvA9u?S#ix-_I^{^ zXbCArV1S^NmDqI_ub75Uj z*Tu&e?|_Fh`V$PI9Zssk@jx#-#afr=_6})h*$|TDnT$~Bzzr#2dpyfh!neUgZLJ{- z7c^~iS7Z^+?d&?Wcd4|9{3wfOqnE8YC;xjXr_%joZSPhxj(W1aY`CYTJ1COiaJ* zd-3f3#jH6SRv)~Tb<|wRhVX^ihUT!^7|N+;+5bH4x!NX-tN{xyhQXA3sZ{ zHCtLEA*L5l5)oh4W^>7$Qx(|wY^$eS`vW$OQ&CG+gq@QKhu{zPlLn`$`xC$HOAy{f z@;I68x-R}>O{0SIl#$LW$J%c}>{<(|&Ef+)jTVE46Tf;WM#VOIHi2PF6p##ak$`6q z>LM^qBzqno*EeScO#1RhwQ_o$$-aW}WdQN-ULR~B^E2<$@0r$vraXH!lxBD{u z`+4}lt3a+jFWe1FBW|+C$34(qpw4Bv?VCkw^<2zkPkfd4(g#aIar)zTArZ@|$pP1> zQ(6HMY4MT;HxQwZdv{@>4-TB^WUa{|%#PxVipgbG&dVd~qfC}6!-rF`X<0{W_sOMd zr?VaevVIj4#gC2;VUW+4tT{r-jezz43|5N|hV17T@LR5XD&|BHwcBpLKzSv0V^n99 zgZ5FIVTswZW?i(=le)?Atm-$aYKQ8*J{S~FjrDg5UKzK>*eWJ7S11J<@21n>I6mgS z;FY=cd<40dSXFLOZZd+*7DOxLYwqv+gFTfZ9}9VISJxUz%ePPu@&yK&=}5(A&WmSR z29~x=kLdk2mPdIW=n}BYpF9h?8p<@`cx)?!?g&HRR~{q{+PS}ZJGruObLzUd=PL&p zl|z=+&M6*Gb`T!pKAcs%UEZaRb?={f{UZDCiZLnm){_Idy(4X#kZYfu1zW=!eAf7| zkpXG}m0bFFQtpG+u%JGWhP`yyXwCimYQ-VW>g=C8$M+bU zbkHf23F8_>ia^)^DF3lQYX9r<5WFyn_2OAOlmz0977yw_GB3` zC+d3s4R*r?s-BM)rX^U3C6wgTqwVT5k5pgAeW=-dNywoQgkPwF{6B^J{Tz!2p5m`# zg9lesvstI|qAe-cnG0zS&7-Lr%Avno3^NwqsJ>Yu4Eqq!-8~hHe*RYU+B@( zrW-W0le2mo(Ebl+6<}J5dp-P;)@`{vSHkrtiM0(8`$Jdmks*q5L|Z#Kl^*X=r`B`U z*SZs2(sjfN1u0`Qieo~TRpUNvMESI$P36Jn*JfOoS9nnQlftOd_mcww#QVvfrEZc; z%a4HvRpaJLeC!#W{J}g-rcPk?5gewHmF0wS&FB^q=v!q>N4;3gCsZsXLew1Jce#1` zw&Cm}9F<^c;yeB)B<6UM^yy897DoxsOKt`sxA6$hHBq9wu_|m}l_;+;^@J}T7Wcaz zhv2=8KBDtAY$>xtHCqk$VWE*|vyBr*%L=C%`Sv!L2`GMle`NUmbSu9DdY^qHT$<6* zYbS~)#UQNo1Ho7rEg5EJ^7y*2!LC*(uFKk(hbuah(W|k_@T$zr-7mVWhCDc*%^UH8 zH8Xv{vL=Ip1D#T%3K&%xXKnfPu#zXm(BQs(+W{x%{}~`s`2tu%wgO zCK@faXY<=m-BM;{!2AWn}b})o!nOs4Er~A>wwHK`!#5Ch`%74%_8R` zU%YwOkvva!_jGe~n7a64Kf%a1*LiRUwsGb;>oV4hV816U&&Bj}!CVy~1_N7Zb6KD5s|y805ieQOjR8x~MZt5nrRC0=_t0gOBUL z>tGu^gZ1aUt$pIv3O;I4UgC;sLl@NrQe;-w=!KDH3mRrZ4u!HGDrbGF9d5=rXpn+P z)coeauye(|&n-G^>4?~irtf-nx)}W#gZ-3zRoh%}&WqF1RRFh?<;c(79o^1WH?$R1 zT03C9efQ_N3%GnXFx0h1SyErSf351cdX2@p4S*YdNZ@vwy~^*LF5{i-Hch?a>oi8xFv((0x)ehw5X56 z3vGd2nVm}qLe?2>XAb$s705+U&+Ass-K$!+oHHG-Tji*JQO71QqiAtJWaEkdM7C?b z0+l-o@!cbxTtV^6t_ysU|1jUtKmNZu7A0l{K_Vs}gm14KN(Ns;tX)-+$eY$c;ZgS; z{XYF=hsjb1NZTLN$aq#|x@7!V&<6;?*N|>kz;L{V6UvK?#Y^ewayjCr83rSr)z!$p zC2V2YV_E+AtvVMgf_zGPI#(9Q#ugD8F%5O3A6lX=x2Nkjv2}$S)UY6M@(ec{5;m|- z>U0JnmJsU05BQUPah*32 zS>OL!HbuGIH?dbRFJpg64JvN|$1p_Z(SfMSgMCtim_WhjhSdyUU15=bqKqBUDL+cY2FW6Ah`!c%npyXfwUPTP9`Kj-i&e0=GTsS=lm`=Z^ zl-T?HZ}DMexzgSFpn*2_S|$F!Rzp?ae6e19+S(seF(?^3xD7j;euHYhrzDv#c*s&; z@E5d7kTbF{G^a@Td=Yfb{dk8!dD|_PEll55Bwy57L+*`!czbJ8-e304APo+xYKwUJ z+tH}L0U;y11O*Lt*7o1D9nX@im0-_+h$$j&cbfaZ^sd>of{T?lSx@7Y@VhY5?;$W& zrNs0Un7?wV;z9kCYsGu@)!T1#qX{3`L$_aB50F|EcN?5F-!NN3jvzkWZ?@30XQR>s zx&KjZY!~XVy0}k12N3edMoFss#dSjbilJ!#8Zc2pA9Vl`npo6 z4sK}IrdsI`iH6l!g5gK@Hto4;Z60A}w;VfF%J=(0V!b%#CBByK-CVW~S^>x~_hNk| zFnqw)<#@2DJ*~z4AydretJzEE*SkFB^LNK*3tAeCi{rCzPu%N&n_g2QOs^-ufl?Gk z=ntNc+hn!zcgMTIFYyBmBs$j2=^>DDw8!H$dgcwhY12f1J5$L>LT+WM*Qb$Z!FR7y zsD3#*?OH}J?fy89uMP~-AHjqj6C4a-ioc50=>`vs@CQ2}Cde$j`;zl@u8eAOcr$f(5#iXp#=KW+MV}8%xfa&Ht%PrPiVCjQukKtej?Qs| z`aE)~o%iPrt`K{)y$7%E&wlNOyO)MeXcqDF*J+M^`mp^NYc)hCezsk+XwO0Uw|A>3 zvTQq_w&T2S*KXvAR0pVMhfmqOTORJ2Ri?gw5g+jRR}H~_ z43O%HpoR2o6h?;v`+DEd)8D|^d9$BqW7E{cTx{cnHQ}0wKwebN&wj{@FIfP4Ik3U)a1_$A0?+fzb6ZxDHN zr0W9|YC8d+e=(ngs)|!!L-lz9k8?SH_92)ZmeW`akLC$1f-(x#;V@IV>@!$_ID^o$ zg8|N+(3M%Jc_}b6(NV+L2r6y`%56C`Ju=ar|Ke5Bq)Z4SE@uoKiI|O%eekvT=hZkr ze3yoM_W|zpG|~0;AVaJWAq9(1HCqQ6WW4o*56hEz_!fmQInaZz(o&fZbxsX-q7B>@ z7@gXCYk)5y4-QLU@hTvE2!{aFABwlgsA4b_lB1n3X=eQ4vTst{SFh5+C~jj9W9k>S59H`4 zH~>{BqUT$3g+qfF4fM$3nfXvCL-`H)iHU&{urmj_?a3%mpW|vw{hq3$RSs!yROHmQ z89is33_ZSk1I4ZJ>3(jBK~|ES5>FuX{MZDg&+vkA$jEb&~6`++MS+}9TidKX#^55&cM3>MC@JMpo zYPx23lUm5qxN5dF(7gOauwb@?rNKFTRuD8l;!zbtsy;}4*%whqGbx{un;&9^^hxQs-oHJ(^Gok@% zYnwrjcS!JOGjujED$34z6VIPp6t`}h=Bs~jQZ;Hlwod?^I(8Ti+sNzhONU~M@vA9F zv!N*ZV*j|Qt-i(2G}u)s=uX!WDL~cDfnrXzG3)9U_`m-23j}T!Y|2UAwOdK=<7>+M zsqfDgM_hzmh`M~J!YkcZg}Hl_cNW`S8e?5fyc(z2krQIhS<7>55#9S2jP#{@?01x( zdKxw4fGxPG_8EqS`L*&-|NV(y!kMDQqGa5TzZI~>)bT~0HA@Rpld5idg4X~0Ikl;7 zK13?CN|+0*q;{Xnp#9x7SF$-A+C#juL$C4j(fd!iVeKU#u_TW+m+To>yuXE&?O#Gq z9yf;YJ61iE7Gq|{H~dpRJDYgEEm zeF$u#FNwyEpDXE8z6e2g+iAGMt7J3dBbk&FKgOwfBOn2NERg$4zP}%84#lW;Nmi|P z;d57EM*o6BFP^3&kTJ_gecRr4tNRRn@c^@G47-o#2b$H9chI|oyZ7TIb&fW>+Ks*R zeUR$_?<|S_EUB+gxhrD-v*Yt5@>A4>;Cly5kSUSx-CsDlVE}bE1UH0i2K9$LZE>dO zo}G2H{t2po&r|$5&ba0tt$!$$rUM-!YH7f1unYJGra3t)clgc@bD*j zFPv3ya_qL>YqT{Umdi1_bly265F-&Za1)Y$XnCTh6Hch%<=)$9L16WzB=6> zK=KUM8+d%>5!K zm|i^;$-UrhMH7uCY>V~!LJ?c<CHPh%^W0VtbzKgUtI#Uy(~2W} zLB$)b)350Z8cJV;MSjHbd)DhW4Y~hPDjQF~F2a;2yXoyHkxL6ZBoIEVE3C`V#nbT+OC=CP|Z>QPh@V|kccCQn(&7r*m(^((XB!!NGD6>HHIRn9}lh(w9t-;aa+ zF~7LFoRUzqdm8O>)iz{0OMGJ{`*Rj_AYp6144cge9U`K)>f%JkSfK*oCVN1;38G}^ z^kevBRTW&g8Fld$eFdGSu7i+HBeXY6)1HfGS00pDgl%b!+@n|9p!3gHg%CNTq^G=9 zrW5E;s8|qjQG}XW_u|@s+3)>J^<4!RR?`btC6#|9g9Z|bBz3HB#O`{}mJz-4 z4<`kMd>rC(C3cLdkb-K@{3>$J=N9?JY$iiL$3ytl4ZSB{+Oy|bdM~A#I3Jd)h$(^O zw5oOY3#~UqXfkZZMjURAu0G_HhF#jrCSzS@|9hD?<6T;?46ciKc6O}kwJ{g5pMNq{ zu*jj3^W)WfHJdjW^n&@VoWA`WT526^-jFmwA|F-Y9HmsZ(#454;XRKT-is@&tLv4S z>w_}MMb}rAo<&_+1;j+=#kTcE!WMK+ShwCtcA``u^Pa0x+xhmW%x+#zt<=gzb^^pd z8+hcESLp_`{lK5p!!Ma)P7cKArsi|x7Vz8_vlsT)$&+2C zydm&^17}d}XZ`+HYAGvNlO{ra^r6t%#rClCV`!}>ylMm)gIf4Al}RgkIMPKQEa3d4 z4sJ<7=U*XioD>QH(%Xd%k^6ieYq%JVg)!0=@je~^OU*ucIB_1G0+dr0v&i~JR{ zWykh)SeevVn4kw=rZXF`wpG*9r2^RBjw@D2lVRhz95J0hp084Pt>!oVU*Pr_liA&u zR(j|IsJ#7qgCB|x+71oXc%n$$ej=0#8iAj>icx$4f>=$k(LbVg$FME8`&>!Bk=CAc zL#hJ`FVw<$d2+AA=KL;vAV^ipgvi;kVukjlkw|tH*T9&N`ISodG~Itq-ld%$P&8W9C3QP-8M`x95$T%|?(3?}E z`E4q6T~^Zq0kJ~J+VS)vWi8QvfCsNbf@Wp;yi#E*@EOJ8pEBhj<5z&J8VZ8B0t1)Z zP!lx@!g)|J^~=e7{Z)cY1Zkk){a7ek9k-Dn`TvF^=1ovU3tuQdFiX)w;R|}@aP-`3 z;s#L1vu5N`j+&nXf|?a!)dGP!Z$Phr2fPkAqQAboR3IUoIJN7z+1O_mIB6<5&DRi@PUhuQlMWkyx-7TP*x>G zpux$}MUQ7Bq`V-k6OqC7rRBIj^B4X8A-RuduTl?g|O^>o7G%%#VqaoyQV=I$p|^vDf%#}zjH5A#Xn=#SEFB?hF@HZ6H#tK zZZC{QQA?j6EK{ho#%ryn|?!43N=nZanCq1 z_j{19(3x`-3h3k%rQo9v_JJbFwvx-AZ$TsAV0hVd)W+tR+z$2$@eq*M@8G*Uo=(OH zp_3h=UY?^*<`U25O^S1J?VfIOd5;Q&6Q-8M;iY(vL5k#W*BXaNwm#IgLB0=`sORYm z>r^|qVm{VfZ3y&I&^R&E6F>)7b482yOrh@TuX&TaXYEbkag61dchw6aVsDHRBY=#d zJTY`9kTsv1clGR0yz%b`GCM%Pppt8pbFCqseqzui_!>aO0?H@^bJ3r6HYxFb22M1f0c*UqtD=FHe*g3}DMX zG)75tiH&(TrlJ>H^$33?Q_N3 zFgFDp**3tL&HJHK=Tn5z7+IK<)4s)9a!ca_xbWAp!|%!_b6w4iL>$6=#EPoqmmR>R z1`f!%5lY2kOTspd^wpBS5RO6hM=&kdxCKA!RL@y3(tEzA=@Ar_n8NrGB>iQvA0~S% zG+132O2@!nId9gr1ij^}Jl8@m3ZzOi4GIj<)gY>8Z=!8SKK`+f@E4>ViC`b4kWFCK zU(O{y*p8FRJ+R6yKFOnvj2rZbB|=xO7o$AQmefYF3Uc4w6ipL$OGTwXz`)&OATc_@ z2Y6jzCW9S?ATdG_NTmsnu(-!lMdMZ%iFRRUCVk#DHz~dAd|st){Q_v}z+r;aT6p5+ zn3=NYpR2r<%_XI;E-y4Go0#FGR|O5F&auK@`+FjhCv!Vg0Z8jOr=0nA^li9k0LbJN ze_4QUckN?lIwF~a1U!4IS+=p~3m!U{+OE+xy~pJ45KLL(_%qEO4`GA$&{beQL``cP zj@E;*S?Em8Q|grugZxR0%>h+~-!Zz+Pz@%x2jG!o3J5FF8mj#W8s^}4A2@9$D|c%w zht>y%FH96>=sx5rBxHMa&vOAC2U}ZD2uF9#zmk>=1k2Il1t+R1uZus5V8twK!Zd}Y zr@05opvcVvHwUoVkm&S#YV69ou!9vaeCq@X&yo&@xph@3O>U&ft)?UF)5yZ6!V*Qu zwGmFIC~7`X?kOv#4)eSd#{4fq?=QF~Unc4W{XF1Ys_o#{RI=YGqVzX~pMo>X#nys# zjlyv-VRZkYdvYO0u=K5O!4&(}?{fAtr(%rEU+XIm@coGny3uX+iUU941Sx97Lo7O{=++Q?4(&U^QrLaZS-z zxHC9;m$YptJrH@6{YzOI*U?_{EkC6ahH_L_eAW}Td`J3Z);lX0 zBFmJ}9GtCIm0`xmvbU=L1a^)?lu3CMSV#mTdY+(bX{U#OhCRJ+965lz#&j)}cs^_` zg!V7&P`+ccScY9KImd5xF%F*vf||Jeg;?TQ;Nq|*T7eH=eL6O`*23~q$QC7*R_Un` z<@==??@CXohb)PA{AlCjf+X}WbrDhnctyf8h7pL=gen8G zc#hiGE>Zq_h!#TsHwk6=L#-g3Qk-Y7R#eG>HE9+87wo?aYib-W%IE&dI{yMtx}zkh zlTA#4mY%vhVSBjv);OF_XVoGdCe;mX#G#WPH4nqN8A;pAyn7AAe44pl0%X5Te0h#q zRqS)S2j6PzzeBjNJ^QF3&%2(z$DW~ujnuR{kpdY7AoRU3(>r-IOwGmD;$%ZOAv%cH zW>VuYe%!olQ>3B2`L5KE$+yObqlFJa5!5tvaPhH)n-)qBDleVj(aujV86Qb>NJYml zlr7XDBg9_~?oONi9YP!Lnz^~S56h6>{rUhBYG6C`=hrlIU!cxAMp#__9psw~{$QWb zli_*beS`bxE>FktOxQ8<$*Qh@C#@^Y9dD43ibAo4>$dp6ZA!RqAbK%CTXIg`c?w=G zzAfV>lF{&hdh!$gF0#nWVPARPQ&WG>ABM2R30jkwB%$wU!gtIlTUXhP*|bp=Q5%ns z`c;y{HD0EajfIKUdBSVv+Un|-hNx|2Fbq+Eh;Q6e(+KKNDq0ZGP_OU4Q+nCl{KAy@ zW(rQ2$DMd6$Vae}F9H$+!@%FjMNt=H!PM|t`Rpot+6ZH-L#Xsz)EHYtc*#U|G-*<`eF6ffpx8hSx1K6{o@Z5`AMWH85^>u2trD40REX zGAER|OyV~`v;Kwd&$lKx=lts3VzVN81Z75a<#IEdmEM%ULSWBM3t*YPY^s6-%V721 z0epxxeY(JPv*2nb; zQ#g3eGgcp&k{U0ep_C@#z?t_obn4v3Jg+NZQ4JzI*bzsboPU}k{LZlbnRKHCLbSN& zQcw5Q`I32evZkU`1G$E*t*R&0c?6a#Pm%n<_=aB-$H$Pfih`c<)ycdK$J>s-+}d7X zO-goIQ~^x|RT0rMQtYn{plJtS6**Io$Gia}qWuip7c^|NfWHp8MMtrA@T1%Im~Plz z8HsXm(+D2U);hC(w_t=}Tr{+6l55^@^j?8^$CX)+iUAgqVvkM_w1(x|bV;`b369&Vk0miNeHFpWPUWmVMm~ZO zS3F^Pj=qL6`F8#^hzIUo5tep+xluye`_AX~9VIEyt;M`xQ~1r0x($0|x5w`ZLLk3y zUIvYMr>ZTaii06Hlh;QNKj2uighZQ^Hx4CQ}GgMgc_5#O1Nt*7o}nQjoN+(XI~(|mULna=8p zfpY7`fY-_@7uwejf&bR(z)NEJ|Ec>mjy+S<UAKD~2db zSD;Q-2@EAE33 zjdliFn^kNx7+H|hTlvPM`e>R{gC2NSNUU2RRM0|b(>2}w$`hB&MGRVNb5`RCpG(N| zO&-&wJ`?4Ix4W!t_S<+aoh3@81p_fKg;F)eNVFI<{A)VMBRs<@{ABT{n1Zv39^-G? z>eST?ODkr%Mg(&okw`wOjPc)DiEUk>={Y0A#T=8_o*F*?P~x`d<&gx-@AdC0|Hokn zh(hoNfyHI?Jn$3UO_E(34@yEhoZK=I(fv?)z_a_fcLlqS_jt1~{-)r?2;dEUB^Z#z zj{+G{C*d}E{HADjO$VGvU4JNEtXit8UGy>Mn0wII;70;*4q!3ZYari2Je$W2PN4D(upDr2k z3zucH)>t3E^Lf6^Ds=0+O37`BE@uR#O)K`j2hHgk`x`Z6nk*f+Z4grgz3p(sXx&nG zU14@H?;wT|qi*P9TCyEy$)1@FXl=OepgS2;u~w6<%U*p=&i+>bE>FH@Z0HJ*ISU z4Q*M!gyn%a=^EIu4!JEHH-tUG2!QUcPFR>fME+T05 zTT{{91IYmc&I^u@yR#P(8)G8kAXRkH9rvlYzpw3-slRBrfvh@S_Y2;;?;lV60<@&llz|0Gq%M5@1a_3vdkl)Iy|f_U{dlOY541U|p20 zj4VzxGxqDLF|4I?Rmugc(wO-b_Z$*3O}Ou@C-wCvmyT~ z=>kv+@0~PPVx*1KDb2T{sf1Szf|d3K>67QM(D)G1t+UW)BL`KGnY2A4nA8vO$BH5o zq?KpYP9qKhElZD2|^2t^rRnW|In?US_D zpkp!*_Y|idk~#gbwc`0GkF4ubF6L{ut>eh_gtJ4VJ+`vrqhR5i1S{)<;srE$S2q`V z5w?<i4<|H-uHmF0M901wVYsmR31~kkW!RwL-4*MZE z;@@V`wV9M!GG7p)t9XE(^mivvXgg5pBLa}1Rjx7c>q6%~^LGAv3@3z1rfF!LJpsZ( z6NKP_pW!++7YNGML;$H$z*W$I!p3%ks|Zm?*-%yI-{vE?Fu1l8;4cGIblfJ2V0uqn zdKyx1hlg;YGIyZY(qzms8FzMMHd=Q{S|kL0FjBBBU=Ra``6$xFJm;I__(r+f@@^x! zG^QW=dvn7L9+j89@=V+Y0!sTPb=u1A;<1uu#y=C_W`EJT9^cHj7nFkOOcz|BeTnEnbu9>P+`Iag!H!qAafl%hsa z(7z8N`+B67d`{`ppYgr5D1tAEbh(!4RUS>?-R&LuOaNU?rN;UwGer*@5#g@Uo9cKE*;Af9_9gAvK}a{tZ4Q*1%8F-}8D01Yi!Nn! zN!8=${>2VEa&+`LWRwgr+6rp}y27ze;TssGph_e|qp*1v!E)Nax0Q6Z$1j-kwSW!xbZU>0P zf9AHsTAdj`SFm{yle3|Eo$H$UB7C^#S4G*Rz1BLXnh@R!nK<|i;K>%9Ibt$A{h;($ zz|BBS^l|AG5NEpPvOsuhKXJSr%7)Na+O{b6(WF=FEzd0R=#Ia-oe#jKW-=r=X{^p7CS>N7PLBrjTjZ(Fb{5$;3huqHaYa}3;A)hf*u54t{2@7SBgkaIpLOhr) zCyig;w}BS&>JRop?-a3sUj%jIz*Y60CAwu;HL~g7<6O6?{9}M$je`C>mMy6fw9IL0 z4{6c*R9lOsy=$Gv_ujlWZ(qAkG?Mvt0P1$!T$+wt_IwBzFU)bxuwr^N|` zrp0UE-+&FtgbR8j{9cxn{s6c!(-2Fi4scHt>kCcM1Rv>-_y}s=+Iry~T~*L!GK#|8 zR*Jc>8n+8&aS>EioAm^19dx;4?(T7O8X+9}0OEBnp?0+GfGH&ufVIs~MFjIzSENrD z2eKF!voi7(J%a+~02EOpZa9jl0SU-2-h{2a!y!~^J^Wav$)l-TJ(F)+rgqRR_p!Qb zwPDT@Ym>Ca`+xg}?a?XwQ zpCLW9uvy0w+-b-k+0S7BOy%KG%iTAOxn|`yt2z4Iu_o6duf{I0PYARd2^Bzm2lh!) zQanLuUc0ySBo$@A6v712z>e+LgQhE2022=8K1Ys)niMtzNY@%b%2;=0Wnsj1Og^m- z`Z?ADXjB?XrzE`*Lm>eI-U=C z%{FBx7*Q?S>sC8r2B}x*A67rGo0k_hwB6W6|WJeB%iRd@*(o&+Mi@SYAYOT z(ACfNpAU}LOH|HV+^d*p_S4t$ymIy= z=BxQtjQr#s7pN7VI_c zb~@APL+{glIP=f>m+$+(J9GYfKuUZRJ>1L73rwz$ixG7~OTj}VdF*f&9+AKteHkRe zyn-VlgkOSQ!6z&d@ZZ3}Sqqc^>8wr`$x6BA)n--p5G&Vy5_8&H)ba_j{t&zxezsly zq{loqO4JxFb`+@nbWD7zRzeBO)76xWSh?=c40JcOp_f7$MNg)mBsI4tQ|3@t@F$sb z!g>a2mN4ItFu;h0Tn1m5nt#E^EZFr+;eWIj>@?dEz_4D@>ogL?4Sr2$yG;Vu;w%O- z_XGm2%GDab>RrK9+h`E22I^8;7g)G)t2X$YUFZ z-&ND{k*i|(b$iL7HIF`Ozo{&VKXj&ta0Y}>cCg0@w7Czv+*3n2?ne`)>2we|&Se>K zn`ErWH*a`C{#zw{k=wo~-^qeq{>Nm6R>HE#Q5_OE5pYM>~jh!C$lS8UQ!_~_bH z@A)xt|73Dh({NWihZN9zCvu32W?EQ9zJ*W0AkfXP_*NepO^MGHIZ3N@nT6Hm;n7O& z_0cXdlUhR52}31bh7Qo;^wtQ zX43u!%VavS(kZN>&bh>*g%EFgu&Q9L&L&h|wu^1MCn$NU#@l_wR6|>p_ify;8rz`t zq}~+(?rJNsnqi#mL4gMvOvO37Uef^9Wvs@UPwS{Gemrwf^g8I`@QRtM!uH%-QC&Bi zIa)|Esj)m}1m(GI%_t@Ser`Hj8eoN4{xBE-H>s3-TMx7Ox^(Asbo z#Jw;H)k)JPd(mXXBwMhFC12T86l+qt8| zrgSUY!TR(I&yv|YVH{t4xiL!SV<(6$ZBvLu_KO$H=pf1;h&cy(cGyo4Q#q~0uQlbzDEoN1Cl*I(o@^$P$u1G|xg`f-PB6Si%M zo!rV&1scbWOeF2j)7vtVpnQ}wL6-&Zok+>B({(lx;Trf+5wiJrEG&&6j(i1=3?5-FzjK&uc~W@veFSQu%GxS2 zO~nL{^?l}P^K@hJ-C(CUL*xc%Ftd|45m390D#7?h?wS`k18zKtOcPBi z8FdBEpE4Y}-y;DR%VdvR7z}SQNnJQI)YI+PQ~1geo_HZwKZu6a&{G*GJF#=SE1Wub z`ntVqHd~-MkcY|RD!h4hY#R*Kk!?F&N#Gdd_@qk%g~V$Hmrj1^Af%hziX}{M5)WoM zvL6gCgmpdA%ZVvVlLiN3V!5kAQE1tS(&&b|jlsI$-G~{kgyIwedR7Tidnet%8zDs> z`lijb^q0nJ?Mr*826%J|vI05ICtK8bKtB>`1gr}PZBOApoIFv0QetQS$SL>oZBF|8 zG07)D=%=0;U8k_|-4iUsE)UEWYski$9i5}1TC|hA$~cP7DU9jU11TfoLI-(k-RdFU zYj>F>s}Oa>c~%z)^Xj7j(DH0Scr#i))o6KfO!CBQ^;#JH!hyKk=e(s2 zxF${RcdB@i@_yGG$DYf^U;Z8Xh6~}J@&Eq@RL%515X9ooU6Xqc0)FNLfD5>cQqByI zEbE=?>Ar7MgP@d$KYQ;HgxPz6$+9WKc1lxpQ0om1Q-hFKJ^c$Bwk8c0~ diff --git a/docs/src/layout.md b/docs/src/layout.md index 1052354..8ceea3b 100644 --- a/docs/src/layout.md +++ b/docs/src/layout.md @@ -14,15 +14,16 @@ p = Presentation(title="GridLayout demo", author="PPTX.jl") s = Slide(; title="Layout demo slide") text = TextBox( - content="welcome to layouting in PPTX.jl\n here we use a 2x2 grid", - textstyle=(align=:center, fontsize=30), - anchor=:center, - linecolor=:black, - wrap=true - ) + content="welcome to layouting in PPTX.jl\n here we use a 2x2 grid", + textstyle=(align=:center, fontsize=30), + anchor=:center, + linecolor=:black, + wrap=true +) table_cells = TableCell.( reshape(1:9, 3,3); textstyle=(fontsize=20, align=:center), + anchor=:center ) table = Table(table_cells, bandrow=false) diff --git a/src/Tables.jl b/src/Tables.jl index affb436..5fd1af8 100644 --- a/src/Tables.jl +++ b/src/Tables.jl @@ -64,20 +64,29 @@ struct Table <: AbstractShape row_heights::Union{Nothing, Vector{Int}} = nothing, header::Bool = true, bandrow::Bool = true, - style_id::String="{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}", + style_id::String="{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"; + convert_to_emu::Bool=true, ) # input is in mm + if convert_to_emu + offset_x = mm_to_emu(offset_x) + offset_y = mm_to_emu(offset_y) + size_x = mm_to_emu(size_x) + size_y = mm_to_emu(size_y) + column_widths = mm_to_emu(column_widths) + row_heights = mm_to_emu(row_heights) + end return new( content, - mm_to_emu(offset_x), - mm_to_emu(offset_y), - mm_to_emu(size_x), - mm_to_emu(size_y), - mm_to_emu(column_widths), - mm_to_emu(row_heights), + offset_x, + offset_y, + size_x, + size_y, + column_widths, + row_heights, header, bandrow, - style_id, + style_id ) end end @@ -93,7 +102,8 @@ function set_geometry(t::Table, geom::Geometry) t.row_heights, t.header, t.bandrow, - t.style_id, + t.style_id; + convert_to_emu=false, ) end @@ -316,7 +326,11 @@ function TableCell(; end function has_tc_properties(c::TableCell) - return !isnothing(c.color) || has_lines(c.lines) + return !isnothing(c.color) || + !isnothing(c.anchor) || + !isnothing(c.direction) || + has_lines(c.lines) || + has_margins(c.margins) end has_margins(c::TableCell) = has_margins(c.margins) diff --git a/src/TextBox.jl b/src/TextBox.jl index 08a9be1..dfc06a2 100644 --- a/src/TextBox.jl +++ b/src/TextBox.jl @@ -95,7 +95,10 @@ end anchor_string(::Nothing) = nothing function anchor_string(x) s = string(x) - @assert s in ("top", "center", "bottom") "unknown anchor $s, must be top, center or bottom" + @assert s in ("top", "center", "middle", "bottom") "unknown anchor $s, must be top, center or bottom" + if s == "middle" + s = "center" + end return s end diff --git a/test/testTables.jl b/test/testTables.jl index ba42ee0..9c1fcc8 100644 --- a/test/testTables.jl +++ b/test/testTables.jl @@ -20,6 +20,24 @@ using Colors @test_throws AssertionError Table(rand(3,2); row_heights = [20,]) end +@testset "Table set geometry" begin + content = TableCell.(rand(3,2);anchor=:center) + t = Table(content, header=false, bandrow=false) + + # this is in EMUs + geom = PPTX.Geometry(100, 100, 200, 150) + t2 = PPTX.set_geometry(t, geom) + + @test t2.offset_x == 100 + @test t2.offset_y == 100 + @test t2.size_x == 200 + @test t2.size_y == 150 + @test t2.content === content + @test t2.header == t.header + @test t2.bandrow == t.bandrow + @test t2.style_id == t.style_id +end + @testset "PPTX Tables from a DataFrame" begin lines = PPTX.TableLines(left=(width=1,)) @test lines.left.width == 12700 # EMUs @@ -33,6 +51,7 @@ end t_margins = TableCell(3; margins=(bottom=0.1,)) @test PPTX.has_margins(t_margins) + @test PPTX.has_tc_properties(t_margins) @test t_margins.margins.bottom == 36000 @test t_margins.margins.left === nothing @@ -43,6 +62,9 @@ end @test PPTX.has_tc_properties(t4) @test !PPTX.has_margins(t4) + @test PPTX.has_tc_properties(TableCell(1; anchor=:center)) + @test PPTX.has_tc_properties(TableCell(1; direction=:vert)) + t3 = TableCell( 3; lines=(bottom=(width=3,color=:black),), diff --git a/test/testTextBox.jl b/test/testTextBox.jl index 0592547..cadd491 100644 --- a/test/testTextBox.jl +++ b/test/testTextBox.jl @@ -53,9 +53,11 @@ b = TextBox("bla", anchor = :center) @test b.content.anchor == "center" + b = TextBox("bla", anchor = :middle) + @test b.content.anchor == "center" b = TextBox("bla", anchor = "bottom") @test b.content.anchor == "bottom" b = TextBox("bla") @test b.content.anchor === nothing - @test_throws AssertionError TextBox("bla", anchor = "middle") + @test_throws AssertionError TextBox("bla", anchor = "banana") end \ No newline at end of file From bfa8878a4dcf6842c500ad906ec2e088aaa50535 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Tue, 14 Apr 2026 14:01:37 +0200 Subject: [PATCH 16/19] refactor SlideSize --- src/Presentation.jl | 15 ++++++++------- src/Slide.jl | 24 ++++++++++++++++-------- src/write.jl | 24 ++++++++++++++---------- test/testGridLayout.jl | 8 ++++---- 4 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/Presentation.jl b/src/Presentation.jl index ab552cf..3d51a21 100644 --- a/src/Presentation.jl +++ b/src/Presentation.jl @@ -1,12 +1,7 @@ -struct PresentationSize - x::Int # EMUs - y::Int # EMUs -end - # presentation properties that are not for the user # these may be gathered from the .pptx template upon writing Base.@kwdef mutable struct PresentationState - size::Union{Nothing, PresentationSize} = nothing + size::Union{Nothing, SlideSize} = nothing end """ @@ -72,6 +67,12 @@ function Base.push!(pres::Presentation, slide::Slide) return push!(slides(pres), slide) end +function slide_size(p::Presentation) + p_sz = p._state.size + sz = isnothing(p_sz) ? SlideSize() : p_sz + return sz +end + # default show used by Array show function Base.show(io::IO, p::Presentation) compact = get(io, :compact, true) @@ -182,7 +183,7 @@ function update_presentation_state!(p::Presentation, template::ZipBufferReader) r = root(doc) n = findfirst("//p:sldSz", r) cx, cy = n["cx"], n["cy"] - sz = PresentationSize(parse(Int, cx), parse(Int, cy)) + sz = SlideSize(parse(Int, cx), parse(Int, cy)) p._state.size = sz return nothing end diff --git a/src/Slide.jl b/src/Slide.jl index 3db216d..a5fcafd 100644 --- a/src/Slide.jl +++ b/src/Slide.jl @@ -1,3 +1,14 @@ +# used in Presentation to set all slide sizes +struct SlideSize + x::Int # EMUs + y::Int # EMUs +end + +# default slide size +function SlideSize() + return SlideSize(inch_to_emu(13.333), inch_to_emu(7.5)) +end + """ ```julia Slide( @@ -116,8 +127,7 @@ function _make_xml_nodes( shape::AbstractShape, start_id::Int, relationship_map::Dict, - slide_size_x::Int, - slide_size_y::Int, + slide_size::SlideSize, ) return Any[make_xml(shape, start_id, relationship_map)] end @@ -126,13 +136,12 @@ function _make_xml_nodes( layout::GridLayout, start_id::Int, relationship_map::Dict, - slide_size_x::Int, - slide_size_y::Int, + slide_size::SlideSize ) xml_nodes = Any[] current_id = start_id for (shape, row_range, col_range) in layout._entries - geom = gridlayout_geometry(layout, row_range, col_range, slide_size_x, slide_size_y) + geom = gridlayout_geometry(layout, row_range, col_range, slide_size.x, slide_size.y) geom = geometry_in_span(shape, geom, layout.keepratio) shape_updated = set_geometry(shape, geom) _bind_relationships!(relationship_map, shape, shape_updated) @@ -145,15 +154,14 @@ end function make_slide( s::Slide, relationship_map::Dict = slide_relationship_map(s); - slide_size_x::Int = inch_to_emu(13.333), # default 16:9 aspect ratio - slide_size_y::Int = inch_to_emu(7.5), + slide_size::SlideSize = SlideSize(), )::AbstractDict xml_slide = OrderedDict("p:sld" => main_attributes()) spTree = init_sptree() next_id = 2 for shape in shapes(s) - xml_nodes = _make_xml_nodes(shape, next_id, relationship_map, slide_size_x, slide_size_y) + xml_nodes = _make_xml_nodes(shape, next_id, relationship_map, slide_size) append!(spTree["p:spTree"], xml_nodes) next_id += length(xml_nodes) end diff --git a/src/write.jl b/src/write.jl index 8f34bb9..424b22b 100644 --- a/src/write.jl +++ b/src/write.jl @@ -55,25 +55,29 @@ function write_slides!(w::ZipWriter, p::Presentation, template::ZipBufferReader) error("input template pptx already contains slides, please use an empty template") end layoutmap = get_layoutnamemap(template) - sz = p._state.size - sz_x = isnothing(sz) ? Int(13.333 * _EMUS_PER_INCH) : sz.x - sz_y = isnothing(sz) ? Int(7.5 * _EMUS_PER_INCH) : sz.y + sz = slide_size(p) for (idx, slide) in enumerate(slides(p)) layoutnametoint!(slide, layoutmap) - xml = make_slide(slide; slide_size_x=sz_x, slide_size_y=sz_y) + + # write slide + xml = make_slide(slide; slide_size = sz) doc::EzXML.Document = xml_document(xml) add_title_shape!(doc, slide, template) - zip_newfile(w, "ppt/slides/slide$idx.xml"; compress=true) - print(w, doc) - zip_commitfile(w) + zip_write_doc(w, doc, "ppt/slides/slide$idx.xml") + + # write slide relationships xml = make_slide_relationships(slide) doc = xml_document(xml) - zip_newfile(w, "ppt/slides/_rels/slide$idx.xml.rels"; compress=true) - print(w, doc) - zip_commitfile(w) + zip_write_doc(w, doc, "ppt/slides/_rels/slide$idx.xml.rels") end end +function zip_write_doc(w::ZipWriter, doc::EzXML.Document, path::String) + zip_newfile(w, path; compress=true) + print(w, doc) + zip_commitfile(w) +end + 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)) diff --git a/test/testGridLayout.jl b/test/testGridLayout.jl index 714bf5d..fb4c7a4 100644 --- a/test/testGridLayout.jl +++ b/test/testGridLayout.jl @@ -137,10 +137,10 @@ layout[2, :] = pic push!(slide, layout) + sz = PPTX.SlideSize(PPTX.mm_to_emu(100), PPTX.mm_to_emu(50)) xml = PPTX.make_slide( slide; - slide_size_x=PPTX.mm_to_emu(100), - slide_size_y=PPTX.mm_to_emu(50), + slide_size = sz, ) sp_tree = xml["p:sld"][end]["p:cSld"][1]["p:spTree"] @@ -179,10 +179,10 @@ layout[2, :] = Picture(joinpath(PPTX.ASSETS_DIR, "julia_logo.emf"); size_x=10, size_y=10) push!(slide, layout) + sz = PPTX.SlideSize(PPTX.mm_to_emu(100), PPTX.mm_to_emu(50)) xml = PPTX.make_slide( slide; - slide_size_x=PPTX.mm_to_emu(100), - slide_size_y=PPTX.mm_to_emu(50), + slide_size = sz, ) sp_tree = xml["p:sld"][end]["p:cSld"][1]["p:spTree"] From 003022f915bf616ca194a3340486948e91aeffcb Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Tue, 14 Apr 2026 14:15:11 +0200 Subject: [PATCH 17/19] test Video in GridLayout --- test/testGridLayout.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/testGridLayout.jl b/test/testGridLayout.jl index fb4c7a4..8d29b38 100644 --- a/test/testGridLayout.jl +++ b/test/testGridLayout.jl @@ -135,6 +135,9 @@ layout[1, 1] = TextBox("Title") pic = Picture(joinpath(PPTX.ASSETS_DIR, "julia_logo.emf")) layout[2, :] = pic + # also test Video in layout + video_path = joinpath(PPTX.ASSETS_DIR, "sample_video.mp4") + layout[1, 2] = Video(video_path; size_x=10, size_y=10) push!(slide, layout) sz = PPTX.SlideSize(PPTX.mm_to_emu(100), PPTX.mm_to_emu(50)) @@ -147,6 +150,7 @@ # spTree contains 2 group entries first, then our 2 layout shapes text_sp = sp_tree[3]["p:sp"] pic_sp = sp_tree[4]["p:pic"] + vid_sp = sp_tree[5]["p:pic"] # size of a grid cell for this slide size and layout dimensions, after accounting for padding and margins cell_x_size = PPTX.mm_to_emu(35) @@ -171,6 +175,14 @@ centered_x = PPTX.mm_to_emu(45) + Int(round((cell_y_size - scaled_y) / 2)) @test pic_off[1]["x"] == string(centered_x) @test pic_off[2]["y"] == string(PPTX.mm_to_emu(30)) + + # Video should also be keepratio=true and centered in its span + vid_xfrm = vid_sp[3]["p:spPr"][1]["a:xfrm"] + vid_off = vid_xfrm[1]["a:off"] + @test vid_xfrm[2]["a:ext"][1]["cx"] == string(cell_y_size) + @test vid_xfrm[2]["a:ext"][2]["cy"] == string(cell_y_size) + @test vid_off[1]["x"] == string(PPTX.mm_to_emu(50 + 35/2)) + @test vid_off[2]["y"] == string(PPTX.mm_to_emu(10)) end @testset "make_slide picture keepratio=false stretches" begin From 40b4028d64d95feb1edaad39cf3e983deac028e2 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Tue, 14 Apr 2026 14:23:56 +0200 Subject: [PATCH 18/19] rename internal functions --- src/Slide.jl | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Slide.jl b/src/Slide.jl index a5fcafd..a95bee0 100644 --- a/src/Slide.jl +++ b/src/Slide.jl @@ -116,14 +116,14 @@ function Base.push!(slide::Slide, layout::GridLayout) return push!(shapes(slide), layout) end -function _bind_relationships!(relationship_map::Dict, original_shape::AbstractShape, updated_shape::AbstractShape) +function update_relationship!(relationship_map::Dict, original_shape::AbstractShape, updated_shape::AbstractShape) if has_rid(original_shape) && haskey(relationship_map, original_shape) relationship_map[updated_shape] = relationship_map[original_shape] end return nothing end -function _make_xml_nodes( +function make_xml_shapes( shape::AbstractShape, start_id::Int, relationship_map::Dict, @@ -132,7 +132,7 @@ function _make_xml_nodes( return Any[make_xml(shape, start_id, relationship_map)] end -function _make_xml_nodes( +function make_xml_shapes( layout::GridLayout, start_id::Int, relationship_map::Dict, @@ -144,7 +144,7 @@ function _make_xml_nodes( geom = gridlayout_geometry(layout, row_range, col_range, slide_size.x, slide_size.y) geom = geometry_in_span(shape, geom, layout.keepratio) shape_updated = set_geometry(shape, geom) - _bind_relationships!(relationship_map, shape, shape_updated) + update_relationship!(relationship_map, shape, shape_updated) push!(xml_nodes, make_xml(shape_updated, current_id, relationship_map)) current_id += 1 end @@ -161,9 +161,9 @@ function make_slide( spTree = init_sptree() next_id = 2 for shape in shapes(s) - xml_nodes = _make_xml_nodes(shape, next_id, relationship_map, slide_size) - append!(spTree["p:spTree"], xml_nodes) - next_id += length(xml_nodes) + xml_shapes = make_xml_shapes(shape, next_id, relationship_map, slide_size) + append!(spTree["p:spTree"], xml_shapes) + next_id += length(xml_shapes) end push!(xml_slide["p:sld"], OrderedDict("p:cSld" => [spTree])) @@ -239,12 +239,12 @@ function relationship_xml(url::AbstractString, r_id::Integer) ) end -_nested_shapes(shape::AbstractShape) = (shape,) +get_nested_shapes(shape::AbstractShape) = (shape,) -function _nested_shapes(layout::GridLayout) +function get_nested_shapes(layout::GridLayout) nested = AbstractShape[] for (shape, _, _) in layout._entries - append!(nested, _nested_shapes(shape)) + append!(nested, get_nested_shapes(shape)) end return nested end @@ -253,7 +253,7 @@ function slide_relationship_map(s::Slide) d = Dict{Union{Slide, AbstractShape, AbstractString}, Int}() r_id = 1 # first rid is reserved by slideLayout for shape in shapes(s) - for nested_shape in _nested_shapes(shape) + for nested_shape in get_nested_shapes(shape) if has_rid(nested_shape) && !haskey(d, nested_shape) r_id += 1 d[nested_shape] = r_id @@ -286,7 +286,7 @@ function make_slide_relationships(s::Slide, relationship_map::Dict = slide_relat ) used_r_ids = [1] for shape in shapes(s) - for nested_shape in _nested_shapes(shape) + for nested_shape in get_nested_shapes(shape) r_id = 1 if has_rid(nested_shape) r_id = relationship_map[nested_shape] From efe28a1ef0da881e10befa86a45e4f3ab32d8df5 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Tue, 14 Apr 2026 14:27:08 +0200 Subject: [PATCH 19/19] mention nesting error in docstring --- src/GridLayout.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/GridLayout.jl b/src/GridLayout.jl index efea3ff..7f1a3d6 100644 --- a/src/GridLayout.jl +++ b/src/GridLayout.jl @@ -53,6 +53,7 @@ to fill the assigned span. Rules: - Empty cells are silently skipped. - Assigning to overlapping cell regions raises an `ArgumentError`. +- Cannot nest `GridLayout`s inside each other (not yet implemented, will throw an error if attempted). """ mutable struct GridLayout <: AbstractShape nrows::Int @@ -130,7 +131,7 @@ function Base.setindex!(layout::GridLayout, shape::AbstractShape, row_idx, col_i end function Base.setindex!(layout::GridLayout, shape::GridLayout, row_idx, col_idx) - error("we do not yet supported nested GridLayouts") + error("we do not yet support nested GridLayouts") end function gridlayout_geometry(