diff --git a/docs/src/library/public.md b/docs/src/library/public.md
index 5f5290d6..bad3dfa3 100644
--- a/docs/src/library/public.md
+++ b/docs/src/library/public.md
@@ -46,3 +46,10 @@ IJulia.load_string
IJulia.readprompt
IJulia.set_max_stdio
```
+
+
+## Multimedia display
+```@docs
+IJulia.display_data
+IJulia.update_display_data
+```
diff --git a/src/inline.jl b/src/inline.jl
index a08baa80..3e8531cf 100644
--- a/src/inline.jl
+++ b/src/inline.jl
@@ -26,6 +26,155 @@ israwtext(::MIME, x::AbstractString) = true
israwtext(::MIME"text/plain", x::AbstractString) = false
israwtext(::MIME, x) = false
+
+# Check mime bundle dict key type and convert to string keys for JSON
+_format_mime_key(k::String) = k
+_format_mime_key(k::MIME) = string(k)
+_format_mime_key(k) = error("MIME bundle keys should be instances of String or MIME")
+_format_mimebundle(d::Dict{String}) = d
+_format_mimebundle(d::AbstractDict) = Dict(_format_mime_key(k) => v for (k, v) in pairs(d))
+
+
+"""Create JSON content for "display_data" or "redisplay_data" message, properly formatting arguments."""
+function _display_msg(mimebundle::AbstractDict, metadata::AbstractDict, display_id::Union{Integer, AbstractString, Nothing})
+ content = Dict{String, Any}("data" => _format_mimebundle(mimebundle), "metadata" => _format_mimebundle(metadata))
+ if display_id !== nothing
+ content["transient"] = Dict("display_id" => display_id)
+ end
+ return content
+end
+
+function _display_msg(mime::String, data, metadata::AbstractDict, display_id::Union{Integer, AbstractString, Nothing})
+ bundle = Dict{String, Any}(mime => data)
+ md = Dict{String, Any}(mime => metadata)
+ mime != "text/plain" && (bundle["text/plain"] = "Unable to display data with MIME type $mime") # Fallback
+ return _display_msg(bundle, md, display_id)
+end
+
+
+"""
+ display_data(mime::Union{MIME, String}, data, metadata::AbstractDict=Dict(); display_id=nothing)
+ display_data(mimebundle::AbstractDict, metadata::AbstractDict=Dict(); display_id=nothing)
+
+Publish encoded multimedia data to be displayed all Jupyter front ends.
+
+This is a low-level function which acts as a direct interface to Jupyter's display system. It does
+not perform any additional processing on the input data, use `display(::IJulia.InlineDisplay, x)` to
+calculate and display the multimedia representation of an arbitrary object `x`.
+
+In the Jupyter notebook/lab the data will be displayed in the output area of the cell being executed.
+This will appear in addition to the display of the cell's execution result, if any. Multiple calls
+to this function within the same cell will result in multiple displays within the same output area.
+
+The first form of the function takes a single MIME type `mime` and encoded data `data`, which should
+be one of the following:
+
+* A string containing text data (e.g. for MIME types `text/html` or `application/javascript`) or
+ base64-encoded binary data (e.g. for `image/png`).
+* Any other value which can be converted to a JSON string by `JSON.json`, including `JSON.JSONText`.
+
+The second form of the function takes a MIME bundle, which is a dictionary containing multiple
+representations of the data keyed by MIME type. The front end will automatically select the richest
+supported type to display.
+
+`metadata` is an additional JSON dictionary describing the output. See the
+[jupyter client documentation](https://jupyter-client.readthedocs.io/en/latest/messaging.html#display-data)
+for the keys used by IPython, notable ones are `width::Int` and `height::Int` to control the size
+of displayed images. When using the second form of the function the argument should be a dictionary
+of dictionaries keyed by MIME type.
+
+`display_id` is an arbitrary integer or string which can be used to update this display at a later
+time using [`update_display_data`](@ref). This can be used to create simple animations by updating
+the display at regular intervals, or to update a display created in a different cell.
+An empty MIME bundle can be passed to initialize an empty display that can be updated later.
+
+
+# Examples
+
+Displaying a MIME bundle containing rich text in three different formats (the front end
+will select only the richest type to display):
+
+```julia
+bundle = Dict(
+ "text/plain" => "text/plain: foo bar baz",
+ "text/html" => "text/html
: foo bar baz",
+ "text/markdown" => "`text/markdown`: foo **bar** *baz*",
+)
+
+IJulia.display_data(bundle)
+```
+
+Display each of these types individually:
+
+```julia
+for (mime, data) in pairs(bundle)
+ IJulia.display_data(mime, data)
+end
+```
+
+Displaying base64-encoded PNG image data:
+
+```julia
+using Base64
+
+data = open(read, "example.png") # Array{UInt8}
+data_enc = base64encode(data) # String
+
+IJulia.display_data("image/png", data_enc)
+```
+
+Adjust the size of the displayed image by passing a metadata dictionary:
+
+```julia
+IJulia.display_data("image/png", data_enc, Dict("width" => 800, "height" => 600))
+```
+
+Updating existing display data using the `display_id` argument and [`update_display_data`](@ref):
+
+```julia
+IJulia.display_data("text/plain", "Before update", display_id="my_id")
+
+# After some time or from a different cell:
+IJulia.update_display_data("my_id", "text/plain", "After update")
+```
+"""
+function display_data(mimebundle::AbstractDict, metadata::AbstractDict=Dict(); display_id::Union{Integer, AbstractString, Nothing}=nothing)
+ content = _display_msg(mimebundle, metadata, display_id)
+ flush_all() # so that previous stream output appears in order
+ send_ipython(publish[], msg_pub(execute_msg, "display_data", content))
+end
+
+function display_data(mime::Union{MIME, AbstractString}, data, metadata::AbstractDict=Dict(); display_id::Union{Integer, AbstractString, Nothing}=nothing)
+ content = _display_msg(string(mime), data, metadata, display_id)
+ flush_all() # so that previous stream output appears in order
+ send_ipython(publish[], msg_pub(execute_msg, "display_data", content))
+end
+
+
+"""
+ update_display_data(display_id, mime::Union{MIME, AbstractString}, data, metadata::AbstractDict=Dict())
+ update_display_data(display_id, mimebundle::AbstractDict, metadata::AbstractDict=Dict())
+
+Update multimedia data previously displayed with [`display_data`](@ref) using
+the `display_id` argument.
+
+Note that in the Jupyter notebook/lab this function need not be called from the
+same cell as the original call to `display_data`. The updated data also does
+not need to contain the same MIME types as the original.
+"""
+function update_display_data(display_id::Union{Integer, AbstractString}, mimebundle::AbstractDict, metadata::AbstractDict=Dict())
+ content = _display_msg(mimebundle, metadata, display_id)
+ flush_all()
+ send_ipython(publish[], msg_pub(execute_msg, "update_display_data", content))
+end
+
+function update_display_data(display_id::Union{Integer, AbstractString}, mime::Union{MIME, AbstractString}, data, metadata::AbstractDict=Dict())
+ content = _display_msg(string(mime), data, metadata, display_id)
+ flush_all()
+ send_ipython(publish[], msg_pub(execute_msg, "update_display_data", content))
+end
+
+
InlineIOContext(io, KVs::Pair...) = IOContext(
io,
:limit=>true, :color=>true, :jupyter=>true,
@@ -54,17 +203,15 @@ function limitstringmime(mime::MIME, x)
return String(take!(buf))
end
-for mime in ipy_mime
+for mimestr in ipy_mime
+ M = MIME{Symbol(mimestr)}
@eval begin
- function display(d::InlineDisplay, ::MIME{Symbol($mime)}, x)
- flush_all() # so that previous stream output appears in order
- send_ipython(publish[],
- msg_pub(execute_msg, "display_data",
- Dict(
- "metadata" => metadata(x), # optional
- "data" => Dict($mime => limitstringmime(MIME($mime), x)))))
+ function display(d::InlineDisplay, ::$M, x)
+ s = limitstringmime($M(), x)
+ m = get(metadata(x), $mimestr, Dict())
+ display_data($M(), s, m)
end
- displayable(d::InlineDisplay, ::MIME{Symbol($mime)}) = true
+ displayable(d::InlineDisplay, ::$M) = true
end
end
@@ -77,28 +224,21 @@ display(d::InlineDisplay, m::MIME"text/javascript", x) = display(d, MIME("applic
# If the user explicitly calls display("foo/bar", x), we send
# the display message, also sending text/plain for text data.
displayable(d::InlineDisplay, M::MIME) = istextmime(M)
+
function display(d::InlineDisplay, M::MIME, x)
sx = limitstringmime(M, x)
- d = Dict(string(M) => sx)
+ bundle = Dict(string(M) => sx)
if istextmime(M)
- d["text/plain"] = sx # directly show text data, e.g. text/csv
+ bundle["text/plain"] = sx # directly show text data, e.g. text/csv
end
- flush_all() # so that previous stream output appears in order
- send_ipython(publish[],
- msg_pub(execute_msg, "display_data",
- Dict("metadata" => metadata(x), # optional
- "data" => d)))
+ display_data(bundle, metadata(x))
end
# override display to send IPython a dictionary of all supported
# output types, so that IPython can choose what to display.
function display(d::InlineDisplay, x)
undisplay(x) # dequeue previous redisplay(x)
- flush_all() # so that previous stream output appears in order
- send_ipython(publish[],
- msg_pub(execute_msg, "display_data",
- Dict("metadata" => metadata(x), # optional
- "data" => display_dict(x))))
+ display_data(display_dict(x), metadata(x))
end
# we overload redisplay(d, x) to add x to a queue of objects to display,