From 227a312ab5f7ee458a35adc1467b538d18cc5811 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Sat, 3 Aug 2013 21:38:54 -0400 Subject: [PATCH 1/2] added new base64(...) function to base64-encode binary data, along with a Base64Pipe <: IO type --- base/base64.jl | 141 ++++++++++++++++++++++++++++++++++++++++++++ base/exports.jl | 2 + base/sysimg.jl | 2 + doc/helpdb.jl | 12 ++++ doc/stdlib/base.rst | 25 ++++++-- 5 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 base/base64.jl diff --git a/base/base64.jl b/base/base64.jl new file mode 100644 index 0000000000000..ceb6e7b8f9e40 --- /dev/null +++ b/base/base64.jl @@ -0,0 +1,141 @@ +module Base64 +import Base: read, write, close +export Base64Pipe, base64 + +# Base64Pipe is a pipe-like IO object, which converts writes (and +# someday reads?) into base64 encoded (decoded) data send to a stream. +# (You must close the pipe to complete the encode, separate from +# closing the target stream). We also have a function base64(f, +# args...) which works like sprint except that it produces +# base64-encoded data, along with base64(args...) which is equivalent +# to base64(write, args...), to return base64 strings. + +############################################################################# + +type Base64Pipe <: IO + io::IO + # writing works in groups of 3, so we need to cache last two bytes written + b0::Uint8 + b1::Uint8 + nb::Uint8 # number of bytes in cache: 0, 1, or 2 + + function Base64Pipe(io::IO) + b = new(io,0,0,0) + finalizer(b, close) + return b + end +end + +############################################################################# + +# Based on code by Stefan Karpinski from https://github.com/hackerschool/WebSockets.jl (distributed under the same MIT license as Julia) + +const b64chars = ['A':'Z','a':'z','0':'9','+','/'] + +function b64(x::Uint8, y::Uint8, z::Uint8) + n = int(x)<<16 | int(y)<<8 | int(z) + b64chars[(n >> 18) + 1], + b64chars[(n >> 12) & 0b111111 + 1], + b64chars[(n >> 6) & 0b111111 + 1], + b64chars[(n ) & 0b111111 + 1] +end + +function b64(x::Uint8, y::Uint8) + a, b, c = b64(x, y, 0x0) + a, b, c, '=' +end + +function b64(x::Uint8) + a, b = b64(x, 0x0, 0x0) + a, b, '=', '=' +end + +############################################################################# + +function write(b::Base64Pipe, x::AbstractVector{Uint8}) + n = length(x) + s = 1 # starting index + # finish any cached data to write: + if b.nb == 1 + if n >= 2 + write(b.io, b64(b.b0, x[1], x[2])...) + s = 3 + elseif n == 1 + b.b1 = x[1] + b.nb = 2 + return + else + return + end + elseif b.nb == 2 + if n >= 1 + write(b.io, b64(b.b0, b.b1, x[1])...) + s = 2 + else + return + end + end + # write all groups of three bytes: + while s + 2 <= n + write(b.io, b64(x[s], x[s+1], x[s+2])...) + s += 3 + end + # cache any leftover bytes: + if s + 1 == n + b.b0 = x[s] + b.b1 = x[s+1] + b.nb = 2 + elseif s == n + b.b0 = x[s] + b.nb = 1 + else + b.nb = 0 + end +end + +function write(b::Base64Pipe, x::Uint8) + if b.nb == 0 + b.b0 = x + b.nb = 1 + elseif b.nb == 1 + b.b1 = x + b.nb = 2 + else + write(b.io, b64(b.b0,b.b1,x)...) + b.nb = 0 + end +end + +function close(b::Base64Pipe) + try + flush(b.io) + catch + end + if b.nb > 0 + # write leftover bytes + padding + if b.nb == 1 + write(b.io, b64(b.b0)...) + else # b.nb == 2 + write(b.io, b64(b.b0, b.b1)...) + end + b.nb = 0 + end +end + +# like sprint, but returns base64 string +function base64(f::Function, args...) + s = IOBuffer() + b = Base64Pipe(s) + f(b, args...) + close(b) + takebuf_string(s) +end +base64(x...) = base64(write, x...) + +############################################################################# + +# read(b::Base64Pipe, ::Type{Uint8}) = # TODO: decode base64 + +############################################################################# + +end # module diff --git a/base/exports.jl b/base/exports.jl index 54c72be0d53bb..644c754d772e6 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -726,6 +726,8 @@ export # strings and text output ascii, base, + base64, + Base64Pipe, beginswith, bin, bits, diff --git a/base/sysimg.jl b/base/sysimg.jl index ddad97df00eb6..68249187b1e98 100644 --- a/base/sysimg.jl +++ b/base/sysimg.jl @@ -62,6 +62,8 @@ include("utf8.jl") include("iobuffer.jl") include("string.jl") include("regex.jl") +include("base64.jl") +importall .Base64 # system & environment include("libc.jl") diff --git a/doc/helpdb.jl b/doc/helpdb.jl index 3e1a5ea3215df..63d52ee67d1f6 100644 --- a/doc/helpdb.jl +++ b/doc/helpdb.jl @@ -1827,6 +1827,18 @@ "), +("Text I/O","Base","base64","base64(stream, args...) + + Given a \"write\"-like function \"writefunc\", which takes an I/O + stream as its first argument, \"base64(writefunc, args...)\" + calls \"writefunc\" to write \"args...\" to a base64-encoded string, + and returns the string. \"base64(args...)\" is equivalent to + \"base64(write, args...)\": it converts its arguments into bytes + using the standard \"write\" functions and returns the base64-encoded + string. + +"), + ("Memory-mapped I/O","Base","mmap_array","mmap_array(type, dims, stream[, offset]) Create an \"Array\" whose values are linked to a file, using diff --git a/doc/stdlib/base.rst b/doc/stdlib/base.rst index dae9e7af014a0..02ac9131d10c0 100644 --- a/doc/stdlib/base.rst +++ b/doc/stdlib/base.rst @@ -1010,13 +1010,13 @@ I/O .. function:: readbytes!(stream, b::Vector{Uint8}, nb=length(b)) - Read at most nb bytes from the stream into b, returning the - number of bytes read (increasing the size of b as needed). + Read at most ``nb`` bytes from the stream into ``b``, returning the + number of bytes read (increasing the size of ``b`` as needed). .. function:: readbytes(stream, nb=typemax(Int)) - Read at most nb bytes from the stream, returning a - Vector{Uint8} of the bytes read. + Read at most ``nb`` bytes from the stream, returning a + ``Vector{Uint8}`` of the bytes read. .. function:: position(s) @@ -1186,6 +1186,23 @@ Text I/O Equivalent to ``writedlm`` with ``delim`` set to comma. +.. function:: Base64Pipe(ostream) + + Returns a new write-only I/O stream, which converts any bytes written + to it into base64-encoded ASCII bytes written to ``ostream``. Calling + ``close`` on the ``Base64Pipe`` stream is necessary to complete the + encoding (but does not close ``ostream``). + +.. function:: base64(writefunc, args...), base64(args...) + + Given a ``write``-like function ``writefunc``, which takes an I/O + stream as its first argument, ``base64(writefunc, args...)`` + calls ``writefunc`` to write ``args...`` to a base64-encoded string, + and returns the string. ``base64(args...)`` is equivalent to + ``base64(write, args...)``: it converts its arguments into bytes + using the standard ``write`` functions and returns the base64-encoded + string. + Memory-mapped I/O ----------------- From ebe0912eb084033e034acb56e4b107589b14e9a0 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Sat, 3 Aug 2013 23:44:31 -0400 Subject: [PATCH 2/2] add multimedia I/O mechanism (display, mm_write, and friends) --- base/base64.jl | 4 - base/exports.jl | 16 ++++ base/multimedia.jl | 214 ++++++++++++++++++++++++++++++++++++++++++++ base/stream.jl | 1 + base/sysimg.jl | 2 + doc/stdlib/base.rst | 162 ++++++++++++++++++++++++++++++++- 6 files changed, 394 insertions(+), 5 deletions(-) create mode 100644 base/multimedia.jl diff --git a/base/base64.jl b/base/base64.jl index ceb6e7b8f9e40..25d4f1ab0c4af 100644 --- a/base/base64.jl +++ b/base/base64.jl @@ -107,10 +107,6 @@ function write(b::Base64Pipe, x::Uint8) end function close(b::Base64Pipe) - try - flush(b.io) - catch - end if b.nb > 0 # write leftover bytes + padding if b.nb == 1 diff --git a/base/exports.jl b/base/exports.jl index 644c754d772e6..88ff57c6dd620 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -1121,6 +1121,22 @@ export wait, workers, +# multimedia I/O + Display, + display, + displayable, + TextDisplay, + istext, + MIME, + @MIME, + reprmime, + stringmime, + writemime, + mimewritable, + popdisplay, + pushdisplay, + redisplay, + # distributed arrays dfill, distribute, diff --git a/base/multimedia.jl b/base/multimedia.jl new file mode 100644 index 0000000000000..655bb1bb95927 --- /dev/null +++ b/base/multimedia.jl @@ -0,0 +1,214 @@ +module Multimedia + +export Display, display, pushdisplay, popdisplay, displayable, redisplay, + MIME, @MIME, writemime, reprmime, stringmime, istext, + mimewritable, TextDisplay, reinit_displays + +########################################################################### +# We define a singleton type MIME{mime symbol} for each MIME type, so +# that Julia's dispatch and overloading mechanisms can be used to +# dispatch writemime and to add conversions for new types. + +immutable MIME{mime} end + +import Base: show, string, convert +MIME(s) = MIME{symbol(s)}() +show{mime}(io::IO, ::MIME{mime}) = print(io, "MIME type ", string(mime)) +string{mime}(::MIME{mime}) = string(mime) + +# needs to be a macro so that we can use ::@mime(s) in type declarations +macro MIME(s) + quote + MIME{symbol($s)} + end +end + +########################################################################### +# For any type T one can define writemime(io, ::@MIME(mime), x::T) = ... +# in order to provide a way to export T as a given mime type. + +# We provide a fallback text/plain representation of any type: +writemime(io, ::@MIME("text/plain"), x) = repl_show(io, x) + +mimewritable{mime}(::MIME{mime}, T::Type) = + method_exists(writemime, (IO, MIME{mime}, T)) + +# it is convenient to accept strings instead of ::MIME +writemime(io, m::String, x) = writemime(io, MIME(m), x) +mimewritable(m::String, T::Type) = mimewritable(MIME(m), T) + +########################################################################### +# MIME types are assumed to be binary data except for a set of types known +# to be text data (possibly Unicode). istext(m) returns whether +# m::MIME is text data, and reprmime(m, x) returns x written to either +# a string (for text m::MIME) or a Vector{Uint8} (for binary m::MIME), +# assuming the corresponding write_mime method exists. stringmime +# is like reprmime except that it always returns a string, which in the +# case of binary data is Base64-encoded. +# +# Also, if reprmime is passed a String for a text type or Vector{Uint8} for +# a binary type, the argument is assumed to already be in the corresponding +# format and is returned unmodified. This is useful so that raw data can be +# passed to display(m::MIME, x). + +for mime in ["text/cmd", "text/css", "text/csv", "text/html", "text/javascript", "text/plain", "text/vcard", "text/xml", "application/atom+xml", "application/ecmascript", "application/json", "application/rdf+xml", "application/rss+xml", "application/xml-dtd", "application/postscript", "image/svg+xml", "application/x-latex", "application/xhtml+xml", "application/javascript", "application/xml", "model/x3d+xml", "model/x3d+vrml", "model/vrml"] + @eval begin + istext(::@MIME($mime)) = true + reprmime(m::@MIME($mime), x::String) = x + reprmime(m::@MIME($mime), x) = sprint(writemime, m, x) + stringmime(m::@MIME($mime), x) = reprmime(m, x) + # avoid method ambiguities with definitions below: + # (Q: should we treat Vector{Uint8} as a bytestring?) + reprmime(m::@MIME($mime), x::Vector{Uint8}) = sprint(writemime, m, x) + stringmime(m::@MIME($mime), x::Vector{Uint8}) = reprmime(m, x) + end +end + +istext(::MIME) = false +function reprmime(m::MIME, x) + s = IOBuffer() + writemime(s, m, x) + takebuf_array(s) +end +reprmime(m::MIME, x::Vector{Uint8}) = x +stringmime(m::MIME, x) = base64(writemime, m, x) +stringmime(m::MIME, x::Vector{Uint8}) = base64(write, x) + +# it is convenient to accept strings instead of ::MIME +istext(m::String) = istext(MIME(m)) +reprmime(m::String, x) = reprmime(MIME(m), x) +stringmime(m::String, x) = stringmime(MIME(m), x) + +########################################################################### +# We have an abstract Display class that can be subclassed in order to +# define new rich-display output devices. A typical subclass should +# overload display(d::Display, m::MIME, x) for supported MIME types m, +# (typically using reprmime or stringmime to get the MIME +# representation of x) and should also overload display(d::Display, x) +# to display x in whatever MIME type is preferred by the Display and +# is writable by x. display(..., x) should throw a MethodError if x +# cannot be displayed. The return value of display(...) is up to the +# Display type. + +abstract Display + +# it is convenient to accept strings instead of ::MIME +display(d::Display, mime::String, x) = display(d, MIME(mime), x) +display(mime::String, x) = display(MIME(mime), x) +displayable(d::Display, mime::String) = displayable(d, MIME(mime)) +displayable(mime::String) = displayable(MIME(mime)) + +# simplest display, which only knows how to display text/plain +immutable TextDisplay <: Display + io::IO +end +display(d::TextDisplay, ::@MIME("text/plain"), x) = + writemime(d.io, MIME("text/plain"), x) +display(d::TextDisplay, x) = display(d, MIME("text/plain"), x) + +import Base: close, flush +flush(d::TextDisplay) = flush(d.io) +close(d::TextDisplay) = close(d.io) + +########################################################################### +# We keep a stack of Displays, and calling display(x) uses the topmost +# Display that is capable of displaying x (doesn't throw an error) + +const displays = Display[] +function pushdisplay(d::Display) + global displays + push!(displays, d) +end +popdisplay() = pop!(displays) +function popdisplay(d::Display) + for i = length(displays):-1:1 + if d == displays[i] + return splice!(displays, i) + end + end + throw(KeyError(d)) +end +function reinit_displays() + empty!(displays) + pushdisplay(TextDisplay(STDOUT)) +end + +function display(x) + for i = length(displays):-1:1 + try + return display(displays[i], x) + catch e + if !isa(e, MethodError) + rethrow() + end + end + end + throw(MethodError(display, (x,))) +end + +function display(m::MIME, x) + for i = length(displays):-1:1 + try + return display(displays[i], m, x) + catch e + if !isa(e, MethodError) + rethrow() + end + end + end + throw(MethodError(display, (m, x))) +end + +displayable{D<:Display,mime}(d::D, ::MIME{mime}) = + method_exists(display, (D, MIME{mime}, Any)) + +function displayable(m::MIME) + for d in displays + if displayable(d, m) + return true + end + end + return false +end + +########################################################################### +# The redisplay method can be overridden by a Display in order to +# update an existing display (instead of, for example, opening a new +# window), and is used by the IJulia interface to defer display +# until the next interactive prompt. This is especially useful +# for Matlab/Pylab-like stateful plotting interfaces, where +# a plot is created and then modified many times (xlabel, title, etc.). + +function redisplay(x) + for i = length(displays):-1:1 + try + return redisplay(displays[i], x) + catch e + if !isa(e, MethodError) + rethrow() + end + end + end + throw(MethodError(redisplay, (x,))) +end + +function redisplay(m::Union(MIME,String), x) + for i = length(displays):-1:1 + try + return redisplay(displays[i], m, x) + catch e + if !isa(e, MethodError) + rethrow() + end + end + end + throw(MethodError(redisplay, (m, x))) +end + +# default redisplay is simply to call display +redisplay(d::Display, x) = display(d, x) +redisplay(d::Display, m::Union(MIME,String), x) = display(d, m, x) + +########################################################################### + +end # module diff --git a/base/stream.jl b/base/stream.jl index 67946cbf747c0..b75d7b0c46452 100644 --- a/base/stream.jl +++ b/base/stream.jl @@ -232,6 +232,7 @@ function reinit_stdio() global STDIN = init_stdio(ccall(:jl_stdin_stream ,Ptr{Void},()),0) global STDOUT = init_stdio(ccall(:jl_stdout_stream,Ptr{Void},()),1) global STDERR = init_stdio(ccall(:jl_stderr_stream,Ptr{Void},()),2) + reinit_displays() # since Multimedia.displays uses STDOUT as fallback end flush(::TTY) = nothing diff --git a/base/sysimg.jl b/base/sysimg.jl index 68249187b1e98..80ba34e38491d 100644 --- a/base/sysimg.jl +++ b/base/sysimg.jl @@ -82,6 +82,8 @@ include("stat.jl") include("fs.jl") importall .FS include("process.jl") +include("multimedia.jl") +importall .Multimedia reinit_stdio() ccall(:jl_get_uv_hooks, Void, ()) include("grisu.jl") diff --git a/doc/stdlib/base.rst b/doc/stdlib/base.rst index 02ac9131d10c0..6cea5e1363b87 100644 --- a/doc/stdlib/base.rst +++ b/doc/stdlib/base.rst @@ -1193,7 +1193,8 @@ Text I/O ``close`` on the ``Base64Pipe`` stream is necessary to complete the encoding (but does not close ``ostream``). -.. function:: base64(writefunc, args...), base64(args...) +.. function:: base64(writefunc, args...) + base64(args...) Given a ``write``-like function ``writefunc``, which takes an I/O stream as its first argument, ``base64(writefunc, args...)`` @@ -1203,6 +1204,165 @@ Text I/O using the standard ``write`` functions and returns the base64-encoded string. +Multimedia I/O +-------------- + +Just as text output is performed by ``print`` and user-defined types +can indicate their textual representation by overloading ``show``, +Julia provides a standardized mechanism for rich multimedia output +(such as images, formatted text, or even audio and video), consisting +of three parts: + +* A function ``display(x)`` to request the richest available multimedia + display of a Julia object ``x`` (with a plain-text fallback). +* Overloading ``writemime`` allows one to indicate arbitrary multimedia + representations (keyed by standard MIME types) of user-defined types. +* Multimedia-capable display backends may be registered by subclassing + a generic ``Display`` type and pushing them onto a stack of display + backends via ``pushdisplay``. + +The base Julia runtime provides only plain-text display, but richer +displays may be enabled by loading external modules or by using graphical +Julia environments (such as the IPython-based IJulia notebook). + +.. function:: display(x) + display(d::Display, x) + display(mime, x) + display(d::Display, mime, x) + + Display ``x`` using the topmost applicable display in the display stack, + typically using the richest supported multimedia output for ``x``, with + plain-text ``STDOUT`` output as a fallback. The ``display(d, x)`` variant + attempts to display ``x`` on the given display ``d`` only, throwing + a ``MethodError`` if ``d`` cannot display objects of this type. + + There are also two variants with a ``mime`` argument (a MIME type + string, such as ``"image/png"``) attempt to display ``x`` using the + requesed MIME type *only*, throwing a ``MethodError`` if this type + is not supported by either the display(s) or by ``x``. With these + variants, one can also supply the "raw" data in the requested MIME + type by passing ``x::String`` (for MIME types with text-based storage, + such as text/html or application/postscript) or ``x::Vector{Uint8}`` + (for binary MIME types). + +.. function:: redisplay(x) + redisplay(d::Display, x) + redisplay(mime, x) + redisplay(d::Display, mime, x) + + By default, the `redisplay` functions simply call ``display``. However, + some display backends may override ``redisplay`` to modify an existing + display of ``x`` (if any). Using ``redisplay`` is also a hint to the + backend that ``x`` may be redisplayed several times, and the backend + may choose to defer the display until (for example) the next interactive + prompt. + +.. function:: displayable(mime) + displayable(d::Display, mime) + + Returns a boolean value indicating whether the given ``mime`` type (string) + is displayable by any of the displays in the current display stack, or + specifically by the display ``d`` in the second variant. + +.. function:: writemime(stream, mime, x) + + The ``display`` functions ultimately call ``writemime`` in order to + write an object ``x`` as a given ``mime`` type to a given I/O + ``stream`` (usually a memory buffer), if possible. In order to + provide a rich multimedia representation of a user-defined type + ``T``, it is only necessary to define a new ``writemime`` method for + ``T``, via: ``writemime(stream, ::@MIME(mime), x::T) = ...``, where + ``mime`` is a MIME-type string and the function body calls + ``write`` (or similar) to write that representation of ``x`` to + ``stream``. + + For example, if you define a ``MyImage`` type and know how to write + it to a PNG file, you could define a function ``writemime(stream, + ::@MIME("image/png"), x::MyImage) = ...``` to allow your images to + be displayed on any PNG-capable ``Display`` (such as IJulia). + As usual, be sure to ``import Base.writemime`` in order to add + new methods to the built-in Julia function ``writemime``. + + Technically, the ``@MIME(mime)`` macro defines a singleton type for + the given ``mime`` string, which allows us to exploit Julia's + dispatch mechanisms in determining how to display objects of any + given type. + +.. function:: mimewritable(mime, T::Type) + + Returns a boolean value indicating whether or not objects of type + ``T`` can be written as the given ``mime`` type. (By default, this + is determined automatically by the existence of the corresponding + ``writemime`` function.) + +.. function:: reprmime(mime, x) + + Returns a ``String`` or ``Vector{Uint8}`` containing the + representation of ``x`` in the requested ``mime`` type, as written + by ``writemime`` (throwing a ``MethodError`` if no appropriate + ``writemime`` is available). A ``String`` is returned for MIME + types with textual representations (such as ``"text/html"`` or + ``"application/postscript"``), whereas binary data is returned as + ``Vector{Uint8}``. (The function ``istext(mime)`` returns whether + or not Julia treats a given ``mime`` type as text.) + + As a special case, if ``x`` is a ``String`` (for textual MIME types) + or a ``Vector{Uint8}`` (for binary MIME types), the ``reprmime`` function + assumes that ``x`` is already in the requested ``mime`` format and + simply returns ``x``. + +.. function:: stringmime(mime, x) + + Returns a ``String`` containing the representation of ``x`` in the + requested ``mime`` type. This is similar to ``reprmime`` except + that binary data is base64-encoded as an ASCII string. + +As mentioned above, one can also define new display backends. For +example, a module that can display PNG images in a window can register +this capability with Julia, so that calling `display(x)` on types +with PNG representations will automatically display the image using +the module's window. + +In order to define a new display backend, one should first create a +subtype ``D`` of the abstract class ``Display``. Then, for each MIME +type (``mime`` string) that can be displayed on ``D``, one should +define a function ``display(d::D, ::@MIME(mime), x) = ...`` that +displays ``x`` as that MIME type, usually by calling ``reprmime(mime, +x)``. A ``MethodError`` should be thrown if ``x`` cannot be displayed +as that MIME type; this is automatic if one calls ``reprmime``. +Finally, one should define a function ``display(d::D, x)`` that +queries ``mimewritable(mime, x)`` for the ``mime`` types supported by +``D`` and displays the "best" one; a ``MethodError`` should be thrown +if no supported MIME types are found for ``x``. Similarly, some +subtypes may wish to override ``redisplay(d::D, ...)``. (Again, one +should ``import Base.display`` to add new methods to ``display``.) +The return values of these functions are up to the implementation +(since in some cases it may be useful to return a display "handle" of +some type). The display functions for ``D`` can then be called +directly, but they can also be invoked automatically from +``display(x)`` simply by pushing a new display onto the display-backend +stack with: + +.. function:: pushdisplay(d::Display) + + Pushes a new display ``d`` on top of the global display-backend + stack. Calling ``display(x)`` or ``display(mime, x)`` will display + ``x`` on the topmost compatible backend in the stack (i.e., the + topmost backend that does not throw a ``MethodError``). + +.. function:: popdisplay() + popdisplay(d::Display) + + Pop the topmost backend off of the display-backend stack, or the + topmost copy of ``d`` in the second variant. + +.. function:: TextDisplay(stream) + + Returns a ``TextDisplay <: Display``, which can display any object + as the text/plain MIME type (only), writing the text representation + to the given I/O stream. (The text representation is the same + as the way an object is printed in the Julia REPL.) + Memory-mapped I/O -----------------