Add Custom Export Formats
The Tiled server can provide data—in whole or in part—in a variety of
formats. See Deliberate Export for a list of the formats supported out
of the box for each structure family ("array"
, "table"
, "container"
, …).
This set of formats can easily be extended. A complete working example is
included in the tiled source tree at example_configs/custom_export_formats
.
We will build it up from scratch.
We’ll start with a text-based format and then address a binary one.
Text Format Example
As our first example, we will invent a variation on CSV (comma-separated variables) that uses a 🙂 instead of a comma, as in
1🙂2🙂3
4🙂5🙂6
We will apply this format to arrays. Tiled expects a function with the interface:
def f(
array: numpy.ndarray,
metadata: Optional[dict]
): -> str | bytes
...
Here is an implementation that exports an array as smiley-separated variables.
# custom_exporters.py
def smiley_separated_variables(array, metadata):
return "\n".join("🙂".join(str(number) for number in row) for row in array)
In real-world cases, there is often already a library that writes the format of interest. Then, our goal isn’t to write an exporter from “scratch”; it’s to integrate some existing exporter with Tiled. For example, numpy can be made to write smiley-separated variables. The trick is to make the library write to a buffer in memory rather than to a file on disk, and then return a string. Most libraries support the following approach.
import io
import numpy
def smiley_separated_variables(array, metadata):
# This StringIO presents a file-like interface that numpy can write to.
file = io.StringIO()
numpy.savetxt(buffer, array, delimiter="🙂", fmt="%s")
return file.getvalue()
Either approach—from scracth or using numpy—-will work in our case. Notice that we also get a dictionary of metadata. Some formats give us nowhere to put this extra information, and we can just drop it in that case.
To integrate this with Tiled, we invoke it in a configuration file.
# config.yml
# Register a custom format for the "array" structure family.
media_types:
array:
application/x-smileys: custom_exporters:smiley_separated_variables
# And provide some example data to try it with....
trees:
- path: /
tree: tiled.examples.generated_minimal:tree
The term application/x-smileys
is a “media type”, also known as “MIME type”.
In our case, there is no registered
IANA Media Type
for our exotic format. Therefore, the standard tells us to invent one of the form
application/x-*
. There is, of course, some risk of name
collisions when we invent names outside of the official list, so be specific.
With custom_exporters.py
and config.yml
placed side by side in some
directory, we can start the server.
tiled serve config --public config.yml
Note
If custom_exporters.py
is placed in the same directory as config.yml
,
the Tiled server will be able to find and import the custom_exporters
module even if it isn’t installed in the normal Python module search
path or placed in the current working directory.
When it loads the configuration, Tiled temporarily adds the directory containing
config.yml
to the Python module search path (sys.path
). This makes it easy
to prototype and integrate custom code. Of course, the configuration can also
load modules that are installed in the normal fashion.
We can request data as smiley-separated variables from the command line using HTTPie:
$ http :8000/array/full/A?slice=:5,:5 Accept:application/x-smileys
HTTP/1.1 200 OK
content-length: 159
content-type: application/x-smileys; charset=utf-8
date: Wed, 12 Jan 2022 21:38:24 GMT
etag: 8b6ec7a60f30c181762a4c73a6b433b0
server: uvicorn
server-timing: read;dur=3.3, tok;dur=0.1, pack;dur=0.2, app;dur=8.1
set-cookie: tiled_csrf=JDHYkMUIBWECLqIJJvTaEcinv_Vd3kTxS08XCw3N4Yg; HttpOnly; Path=/; SameSite=lax
1.0🙂1.0🙂1.0🙂1.0🙂1.0
1.0🙂1.0🙂1.0🙂1.0🙂1.0
1.0🙂1.0🙂1.0🙂1.0🙂1.0
1.0🙂1.0🙂1.0🙂1.0🙂1.0
1.0🙂1.0🙂1.0🙂1.0🙂1.0
or using the Tiled Python client.
from tiled.client import from_uri
c = from_uri("http://localhost:8000/api")
c['A'][:5, :5].export("test.txt", format="application/x-smileys")
Binary Format Example
Let’s add support for JPEG images. Tiled doesn’t build in support for JPEG; for scientific uses, PNG is better because it is lossless.
We’ll use the library PIL to write the JPEG data. As with the numpy example
above, we need to intercept its output in a buffer. In this case, it will be a
binary buffer, BytesIO
, instead of StringIO
.
# custom_exporters.py
import io
from PIL import Image
from tiled.structures.image_serializer_helpers import img_as_ubyte
def to_jpeg(array, metadata):
# PIL detail: ensure array has compatible data type before handing to PIL.
prepared_array = img_as_ubyte(array)
image = Image.fromarray(prepared_array)
file = io.BytesIO()
image.save(file, format="jpeg")
return file.getbuffer()
This covers the basic functionality. See the built-in exporters in
tiled/strucures/array.py
for details that add polish, like scaling the
image’s dynamic range and failing gracefully when given arrays that have the
wrong dimensionality to be exported as an image.
We’ll add it to our configuration.
# config.yml
media_types:
array:
application/x-smileys: custom_exporters:smiley_separated_variables
image/jpeg: custom_exporters:to_jpeg
trees:
- path: /
tree: tiled.examples.generated_minimal:tree
Start the server again
tiled serve config --public config.yml
and navigate a web browser to http://localhost:8000/api/v1/array/full/A?format=image/jpeg
.
Since the example data is just an array of ones, this will appear as a white square image.
File extensions as convenience aliases
Now, image/jpeg
is unwieldy for users unfamiliar with MIME types. Adding
file_extensions:
jpg: image/jpeg
jpeg: image/jpeg
to the configuration enables
http://localhost:8000/api/v1/array/full/A?format=jpeg
http://localhost:8000/api/v1/array/full/A?format=jpg
as equivalent to
http://localhost:8000/api/v1/array/full/A?format=image/jpeg
Note
The format can also be specified as an
HTTP Accept
Header.
In that case, it must be given as a MIME type, in accordance with the standard.
The file extension alias is not accepted.
Advanced: Streaming export
HTTP supports chunked responses, where data is streamed incrementally. This is a good fit for streaming-oriented formats such as newline-delimited JSON.
To create a chunked exporter, implement your exporter as a Python generator that yields bytes.
def export(array, metadata):
for ... in ...:
yield b"..."
Further examples
At the bottom of each of the modules in tiled/structures
, you will
find the code for the built-in exporters (a.k.a “serializers”).