Skip to content

Commit 2f01ecf

Browse files
authored
Add option for images written at render time (#29)
* Refactor so `Image` can be used to pass objects that are only written to file at render time * add tests * add reference
1 parent eb442c4 commit 2f01ecf

File tree

9 files changed

+453
-47
lines changed

9 files changed

+453
-47
lines changed

src/WriteDocx.jl

Lines changed: 107 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -480,57 +480,119 @@ Base.@kwdef struct TableLevelCellMargins
480480
stop::Maybe{Twip} = nothing
481481
end
482482

483+
struct Path
484+
path::String
485+
end
486+
487+
"""
488+
Image(m::MIME, object)
489+
490+
Represents an image of MIME type `m` that can be written to an appropriate file
491+
when writing out a docx document. The `object` needs to have a `show` method
492+
for `m` defined for the default behavior to work.
493+
"""
494+
struct Image{MIME,T}
495+
m::MIME
496+
object::T
497+
end
498+
499+
"""
500+
Image(path::String)
501+
502+
Create an `Image` pointing to the file at `path`. The MIME type is determined by file extension.
503+
"""
504+
Image(path::String) = Image(Path(path))
505+
Image(i::Image) = i
506+
507+
function Image(path::Path)
508+
if endswith(path.path, r"\.svg"i)
509+
Image(MIME"image/svg+xml"(), path)
510+
elseif endswith(path.path, r"\.png"i)
511+
Image(MIME"image/png"(), path)
512+
else
513+
throw(ArgumentError("Unknown image file extension, only .svg and .png are allowed. Path was \"$(path.path)\""))
514+
end
515+
end
516+
517+
# avoid writing file again which is already given as a path
518+
function get_image_file(i::Image{M,Path}, tempdir) where M
519+
return i.object.path
520+
end
521+
522+
file_extension(::Type{MIME"image/svg+xml"}) = "svg"
523+
file_extension(::Type{MIME"image/png"}) = "png"
524+
525+
function get_image_file(i::Image{M}, tempdir) where M
526+
ext = file_extension(M)
527+
filepath = joinpath(tempdir, "temp.$(ext)")
528+
open(filepath, "w") do io
529+
Base.show(io::IO, M(), i.object)
530+
end
531+
return filepath
532+
end
533+
483534
"""
484535
InlineDrawing{T}(; image::T, width::EMU, height::EMU)
485536
486537
Create an `InlineDrawing` object which, as the name implies, can be placed inline with
487538
text inside [`Run`](@ref)s.
488539
489540
WriteDocx supports different types `T` for the `image` argument.
490-
If `T` is a `String`, `image` is treated as the file path to a .png or .svg image.
541+
If `T` is a `String`, `image` is treated as the file path to an existing .png or .svg image.
542+
You can pass an `Image` object which can hold a reference to an object that can be written
543+
to a file with the desired MIME type at render time.
491544
You can also use [`SVGWithPNGFallback`](@ref) to place .svg images with better fallback behavior.
492545
493546
Width and height of the placed image are set via `width` and `height`, note that you have to
494547
determine these values yourself for any image you place, a correct aspect ratio will not
495548
be determined automatically.
496549
"""
497-
Base.@kwdef struct InlineDrawing{T}
550+
struct InlineDrawing{T}
498551
image::T
499552
width::EMU
500553
height::EMU
501554
end
502555

556+
function InlineDrawing(; image, width, height)
557+
InlineDrawing(image, width, height)
558+
end
559+
560+
InlineDrawing(image::T, width, height) where T = InlineDrawing{T}(image, convert(EMU, width), convert(EMU, height))
561+
562+
# backwards-compatibility, interpret String as path to an image
563+
function InlineDrawing(path::AbstractString, width, height)
564+
InlineDrawing(Image(path), width, height)
565+
end
566+
567+
# fix ambiguity
568+
function InlineDrawing(path::AbstractString, width::EMU, height::EMU)
569+
InlineDrawing(Image(path), width, height)
570+
end
571+
503572
is_inline_element(::Type{<:InlineDrawing}) = true
504573

505574
"""
506-
SVGWithPNGFallback(; svg::String, png::String)
575+
SVGWithPNGFallback(; svg, png)
507576
508-
Create a `SVGWithPNGFallback` for the svg file at path `svg` and the fallback png
509-
file at path `png`.
577+
Create a `SVGWithPNGFallback` for the svg `svg` and the fallback `png`.
578+
If `svg` or `png` are `AbstractString`s, they will be treated as paths to image files.
579+
Otherwise, they should be `Image`s with the appropriate MIME types. Use `SVGImage` and
580+
`PNGImage` as shortcuts to create these.
510581
511582
Word Online and other services like Slack preview don't work when a simple svg file is added via
512583
[`InlineDrawing`](@ref)`{String}`.
513584
`SVGWithPNGFallback` supplies a fallback png file which will be used for display in those situations.
514585
Note that it is your responsibility to check whether the png file is an accurate
515586
replacement for the svg.
516587
"""
517-
struct SVGWithPNGFallback
518-
svg::String # path to SVG
519-
png::String # path to PNG
520-
function SVGWithPNGFallback(; svg::AbstractString, png::AbstractString)
521-
svg = convert(String, svg)
522-
png = convert(String, png)
523-
if !endswith(svg, ".svg")
524-
throw(ArgumentError("SVG file needs to end with .svg, got $svg"))
525-
end
526-
if !endswith(png, ".png")
527-
throw(ArgumentError("PNG file needs to end with .png, got $png"))
528-
end
529-
new(svg, png)
530-
end
588+
struct SVGWithPNGFallback{T1,T2}
589+
svg::Image{MIME"image/svg+xml",T1} # path to SVG
590+
png::Image{MIME"image/png",T2} # path to PNG
531591
end
532592

533-
InlineDrawing(image::T, width, height) where T = InlineDrawing{T}(image, convert(EMU, width), convert(EMU, height))
593+
function SVGWithPNGFallback(; svg, png)
594+
SVGWithPNGFallback(Image(svg), Image(png))
595+
end
534596

535597
@partialkw struct Style{T}
536598
id::String
@@ -1303,10 +1365,6 @@ struct Relationship
13031365
target::String
13041366
end
13051367

1306-
struct ImageMedium{T}
1307-
image::T
1308-
end
1309-
13101368
struct StyleRel end
13111369

13121370
function resolve_rel!(x::StyleRel, i, dir, prefix)
@@ -1316,16 +1374,18 @@ function resolve_rel!(x::StyleRel, i, dir, prefix)
13161374
)
13171375
end
13181376

1319-
function resolve_rel!(x::ImageMedium{String}, i, dir, prefix)
1320-
imgpath = x.image
1321-
mediapath = joinpath(dir, "word", "media")
1322-
mkpath(mediapath)
1323-
_, extension = splitext(imgpath)
1324-
targetpath = joinpath(mediapath, "$(prefix)image_$i" * extension)
1325-
cp(imgpath, targetpath)
1326-
type = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
1327-
target = relpath(targetpath, joinpath(dir, "word"))
1328-
return Relationship(type, target)
1377+
function resolve_rel!(x::Image, i, dir, prefix)
1378+
mktempdir() do tempdir
1379+
imgpath = get_image_file(x, tempdir)
1380+
mediapath = joinpath(dir, "word", "media")
1381+
mkpath(mediapath)
1382+
_, extension = splitext(imgpath)
1383+
targetpath = joinpath(mediapath, "$(prefix)image_$i" * extension)
1384+
cp(imgpath, targetpath)
1385+
type = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
1386+
target = relpath(targetpath, joinpath(dir, "word"))
1387+
return Relationship(type, target)
1388+
end
13291389
end
13301390

13311391
function resolve_rel!(h::Union{Header, Footer}, i, dir, prefix)
@@ -1369,14 +1429,14 @@ end
13691429

13701430
gather_rels!(rels, zipdir, ::Nothing) = nothing
13711431

1372-
function gather_rels!(rels, zipdir, i::InlineDrawing{String})
1373-
add_rel!(rels, ImageMedium(i.image))
1432+
function gather_rels!(rels, zipdir, i::InlineDrawing{<:Image})
1433+
add_rel!(rels, i.image)
13741434
return
13751435
end
13761436

1377-
function gather_rels!(rels, zipdir, i::InlineDrawing{SVGWithPNGFallback})
1378-
add_rel!(rels, ImageMedium(i.image.png))
1379-
add_rel!(rels, ImageMedium(i.image.svg))
1437+
function gather_rels!(rels, zipdir, i::InlineDrawing{<:SVGWithPNGFallback})
1438+
add_rel!(rels, i.image.png)
1439+
add_rel!(rels, i.image.svg)
13801440
return
13811441
end
13821442

@@ -1637,9 +1697,9 @@ xml(name, pairs::Pair{String,<:Any}...) = xml(name, [], pairs...)
16371697

16381698
to_xml(xml::E.Node, _) = xml
16391699

1640-
function get_ablip(i::InlineDrawing{String}, rels)
1641-
index = rels[ImageMedium(i.image)]
1642-
ablip = if endswith(i.image, r"\.svg"i)
1700+
function get_ablip(i::InlineDrawing{<:Image{M}}, rels) where M
1701+
index = rels[i.image]
1702+
ablip = if M <: MIME"image/svg+xml"
16431703
xml("a:blip", [
16441704
xml("a:extLst", [
16451705
# this is the empty scaffolding of a png thumbnail that doesn't actually need to be there in modern Word as it seems
@@ -1650,17 +1710,17 @@ function get_ablip(i::InlineDrawing{String}, rels)
16501710
], "uri" => "{96DAC541-7B7A-43D3-8B79-37D633B846F1}")
16511711
])
16521712
])
1653-
elseif endswith(i.image, r"\.png"i)
1713+
elseif M <: MIME"image/png"
16541714
xml("a:blip", "r:embed" => "rId$index")
16551715
else
1656-
error("Cannot deal with image that is not a .svg or .png.")
1716+
error("Cannot deal with image of MIME type $M")
16571717
end
16581718
return (; ablip, index)
16591719
end
16601720

1661-
function get_ablip(i::InlineDrawing{SVGWithPNGFallback}, rels)
1662-
index_png = rels[ImageMedium(i.image.png)]
1663-
index_svg = rels[ImageMedium(i.image.svg)]
1721+
function get_ablip(i::InlineDrawing{<:SVGWithPNGFallback}, rels)
1722+
index_png = rels[i.image.png]
1723+
index_svg = rels[i.image.svg]
16641724
ablip =
16651725
xml("a:blip", [
16661726
xml("a:extLst", [
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+
<Types
3+
xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
4+
<Default Extension="png" ContentType="image/png" />
5+
<Default Extension="svg" ContentType="image/svg+xml" />
6+
<Default Extension="xml" ContentType="application/xml" />
7+
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
8+
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" />
9+
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml" />
10+
</Types>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+
<Relationships
3+
xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
4+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
5+
</Relationships>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
3+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
4+
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image_2.svg"/>
5+
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image_3.png"/>
6+
</Relationships>
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing">
3+
<w:body>
4+
<w:p>
5+
<w:pPr/>
6+
<w:r>
7+
<w:rPr/>
8+
<w:drawing>
9+
<wp:inline>
10+
<wp:extent cx="700000" cy="300000"/>
11+
<wp:docPr id="2" name="Picture 2"/>
12+
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
13+
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
14+
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
15+
<pic:nvPicPr>
16+
<pic:cNvPr id="2" name="Picture 2"/>
17+
<pic:cNvPicPr/>
18+
</pic:nvPicPr>
19+
<pic:blipFill>
20+
<a:blip>
21+
<a:extLst>
22+
<a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}"/>
23+
<a:ext uri="{96DAC541-7B7A-43D3-8B79-37D633B846F1}">
24+
<asvg:svgBlip xmlns:asvg="http://schemas.microsoft.com/office/drawing/2016/SVG/main" r:embed="rId2"/>
25+
</a:ext>
26+
</a:extLst>
27+
</a:blip>
28+
<a:stretch>
29+
<a:fillRect/>
30+
</a:stretch>
31+
</pic:blipFill>
32+
<pic:spPr>
33+
<a:xfrm>
34+
<a:off x="0" y="0"/>
35+
<a:ext cx="700000" cy="300000"/>
36+
</a:xfrm>
37+
<a:prstGeom prst="rect">
38+
<a:avLst/>
39+
</a:prstGeom>
40+
</pic:spPr>
41+
</pic:pic>
42+
</a:graphicData>
43+
</a:graphic>
44+
</wp:inline>
45+
</w:drawing>
46+
</w:r>
47+
<w:r>
48+
<w:rPr/>
49+
<w:drawing>
50+
<wp:inline>
51+
<wp:extent cx="1800000" cy="1097280"/>
52+
<wp:docPr id="3" name="Picture 3"/>
53+
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
54+
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
55+
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
56+
<pic:nvPicPr>
57+
<pic:cNvPr id="3" name="Picture 3"/>
58+
<pic:cNvPicPr/>
59+
</pic:nvPicPr>
60+
<pic:blipFill>
61+
<a:blip r:embed="rId3"/>
62+
<a:stretch>
63+
<a:fillRect/>
64+
</a:stretch>
65+
</pic:blipFill>
66+
<pic:spPr>
67+
<a:xfrm>
68+
<a:off x="0" y="0"/>
69+
<a:ext cx="1800000" cy="1097280"/>
70+
</a:xfrm>
71+
<a:prstGeom prst="rect">
72+
<a:avLst/>
73+
</a:prstGeom>
74+
</pic:spPr>
75+
</pic:pic>
76+
</a:graphicData>
77+
</a:graphic>
78+
</wp:inline>
79+
</w:drawing>
80+
</w:r>
81+
<w:r>
82+
<w:rPr/>
83+
<w:drawing>
84+
<wp:inline>
85+
<wp:extent cx="700000" cy="300000"/>
86+
<wp:docPr id="2" name="Picture 2"/>
87+
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
88+
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
89+
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
90+
<pic:nvPicPr>
91+
<pic:cNvPr id="2" name="Picture 2"/>
92+
<pic:cNvPicPr/>
93+
</pic:nvPicPr>
94+
<pic:blipFill>
95+
<a:blip r:embed="rId3">
96+
<a:extLst>
97+
<a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}"/>
98+
<a:ext uri="{96DAC541-7B7A-43D3-8B79-37D633B846F1}">
99+
<asvg:svgBlip xmlns:asvg="http://schemas.microsoft.com/office/drawing/2016/SVG/main" r:embed="rId2"/>
100+
</a:ext>
101+
</a:extLst>
102+
</a:blip>
103+
<a:stretch>
104+
<a:fillRect/>
105+
</a:stretch>
106+
</pic:blipFill>
107+
<pic:spPr>
108+
<a:xfrm>
109+
<a:off x="0" y="0"/>
110+
<a:ext cx="700000" cy="300000"/>
111+
</a:xfrm>
112+
<a:prstGeom prst="rect">
113+
<a:avLst/>
114+
</a:prstGeom>
115+
</pic:spPr>
116+
</pic:pic>
117+
</a:graphicData>
118+
</a:graphic>
119+
</wp:inline>
120+
</w:drawing>
121+
</w:r>
122+
</w:p>
123+
<w:sectPr/>
124+
</w:body>
125+
</w:document>
Lines changed: 1 addition & 0 deletions
Loading
9.74 KB
Loading

0 commit comments

Comments
 (0)