(custom-export-formats)=
# Add Custom Export Formats

The Tiled server can provide data---in whole or in part---in a variety of
formats. See {doc}`../tutorials/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:

```py
def f(
    array: numpy.ndarray,
    metadata: Optional[dict]
): -> str | bytes
    ...
```

Here is an implementation that exports an array as smiley-separated variables.

```py
# 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.

```py
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.

```yaml
# 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](https://www.iana.org/assignments/media-types/media-types.xhtml)
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](https://httpie.io/):

```
$ 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.

```py
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`.

```py
# 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.

```yaml
# 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

```yaml
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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept).
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.

```python
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").
