EXPLAIN EXTENDED

How to create fast database queries

Happy New Year: GIF decoder in SQL

Comments enabled. I *really* need your comment

As readers of my blog know, SQL is a wonderful tool for graphics processing. You can use it to draw Mandelbrot sets, table game boards and even snowflakes, all in a single query.

As I was preparing this year's entry, I found myself all out of ideas. What image shall we be generating this year? A cat? A pig? A winter scenery? It's all doable in SQL (or course), however I couldn't make myself pick anything in particular. And frankly speaking, even with all the power of SQL at my hands, I'm a lousy artist.

Then an idea struck me. Why try and create art when there's so many excellent artists out there on the Internet, whose work I could just steal put to fair use? And my phone camera makes better pictures than I could ever aspire to create myself.

Images that come out of the camera or from the Internet are all digital and compressed. Digital is of course a good thing when it comes to computer processing, but compressed is a challenge. And challenges is something I like.

So this year, we will be creating a GIF decoder in SQL.

GIF is one of the earliest compressed image storage formats, famous for its early adoption by the World Wide Web and for being named with an acronym no one can agree how to pronounce correctly. At its core lies LZW, a lossless compression algorithm which uses dictionary tables to encode repeating patterns of data. GIF is not the best format out there, of course, and it has fallen out of use in the last years (or even decades). Its algorithm, however, is not particularly memory or CPU intensive and as the image compression algorithms go, its implementation is quite simple. In other words, it makes a perfect SQL exercise.

To become familiar with GIF, I used an excellent GIF tutorial called What's in a GIF? by Eric. S Raymond and Mike Flickinger. This tutorial is built around explanation of contents of a tiny sample GIF file:

and this file is what we will be using during the first part of our journey.

Let's get started!

Feeding a binary file to the database

As usual, we will be using the latest version of PostgreSQL for this, because of its excellent support of recursive queries, rowset generators, lateral joins and complex types. The first is an absolute must for anything beyond the most simple tasks, the rest make even the complex tasks easier and more readable.

Unfortunately, we cannot easily use binary files with psql directly. We will have to recourse to a little bit of shell magic: we will hex-encode the file and pipe it to the \copy command, which will insert its contents into a temporary table.

We will need two scripts for that:

pre.sql

CREATE TEMPORARY TABLE
        input
        (
        data TEXT
        );

01.bin.sql

WITH    RECURSIVE
        bin AS
        (
        SELECT  DECODE(data, 'HEX') AS buffer
        FROM    input
        )
SELECT  ENCODE(buffer, 'HEX')
FROM    bin;

, which we will be calling in a single shell command:

cat sample_1.gif | od -A n -vt x1 | tr -d '\n ' | psql -f pre.sql -c "COPY input FROM stdin" -f 01.bin.sql
encode
4749463839610a000a00910000ffffffff00000000ff00000021f90400000000002c000000000a000a000002168c2d99872a1cdc33a00275ec95faa8de608c04914c01003b

Here we see the hex-encoded content of the sample GIF file in all its entirety. Note that we have put it into a BYTEA field in a temporary table. This will allow working with the parts of the file more easily than if it would be a hex-encoded string.

Let's try and parse the header of the file. To do this, we will be using BIT operators and BYTEA functions:

02.header.sql

WITH    RECURSIVE
        bin AS
        (
        SELECT  DECODE(data, 'HEX') AS buffer
        FROM    input
        ),
        header AS
        (
        SELECT  h.*, io.*
        FROM    bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, 10)::BIT(8) AS flags
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CONVERT_FROM(SUBSTR(buffer, 1, 6), 'LATIN1') AS version,
                        (GET_BYTE(buffer, 7)::BIT(8) || GET_BYTE(buffer, 6)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, 9)::BIT(8) || GET_BYTE(buffer, 8)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = B'1' AS gtc,
                        (flags << 1)::BIT(3)::INT + 1 AS depth,
                        (flags << 4) = B'1' AS color_sort,
                        2 << (flags << 5)::BIT(3)::INT AS gct_size,
                        GET_BYTE(buffer, 11)::INT AS bci,
                        GET_BYTE(buffer, 12)::INT AS aspect
                ) h
        CROSS JOIN LATERAL
                (
                SELECT  13 + 3 * gct_size AS blocks_offset
                ) io
        )
SELECT  *
FROM    header;
version width height gtc depth color_sort gct_size bci aspect blocks_offset
GIF89a 10 10 True 2 False 4 0 0 25

This is some basic information about the GIF file.

version, width and height are self-explanatory.

gct,depth, color_sort and gct_size are related to the image palette. The GIF standard allows encoding any colors from 24-bit RGB color space, but no more than 256 of them at the same time. The set of 24-bit colors used within a single image is called a palette. Palettes are the reason GIF pictures look grainy on the screen: there are just not enough colors available in the palette to encode the smooth transitions. It was enough for most screens back in the day, though, and this is more than enough for the sample file which only has four colors.

To define a palette, we need to know how many different colors are used in the file and what are their values in the color space. This information is stored in a color table. Static GIF files mostly use a Global Color Table for that. There can also be local color tables, but we won't be dealing with them in this post. So, gct is a flag showing if a Global Color Table is being used (spoiler: it is). depth is the number of colors in the palette (as a base-2 logarithm, so 2 means 22 = 4 colors). color_sort is a legacy flag which, if set, means that the colors are sorted from the "most important" to the "least important", so if a decoder or a rendering device is really short on memory, it can choose to only display a selected subset of colors. Finally, gct_size is the size of the Global Color Table in 3-byte chunks, one per color.

The rest of header fields deals with interlacing, pixel aspect ratio and canvas color. Those are of no importance to us.

The Global Color Table immediately follows the header, and we will get back to it later. Now we have to find where the real fun begins. And blocks_offset is a calculated field which brings us past the GCT into the most interesting parts of the file.

Blocks

Data in a GIF file is composed of blocks. Those can be images, captions, comments and the designers have even left room for other extensions, although the chances are low we will see new versions of the GIF standard any time soon. We are interested in image blocks, and we have to local the first image block to work with it.

An image block always goes after a so called Graphic Extension block. All extension blocks start with a value 0x21 (called extension introducer), followed by a byte-long extension type and a byte-long block length. Every extension block ends with a value of 0x00. The image block itself starts with a value 0x2C.

To track the beginning of an image block, we need to recursively read through all the blocks, skipping over those which start with 0x21, and stopping when we find a 0x2C.

To do that, we will use a recursive CTE, which would track the offsets, starting from the first one past the Global Color Table, look at the value at the offset, and if this value is 0x21, skip to the next block (block length at offset + 2, plus 3-byte header plus 1-byte terminator). This stops when the value at the offset is 0x2C (image data).

03.image.sql

WITH    RECURSIVE
        bin AS
        (
        SELECT  DECODE(data, 'HEX') AS buffer
        FROM    input
        ),
        header AS
        (
        SELECT  h.*, io.*
        FROM    bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, 10)::BIT(8) AS flags
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CONVERT_FROM(SUBSTR(buffer, 1, 6), 'LATIN1') AS version,
                        (GET_BYTE(buffer, 7)::BIT(8) || GET_BYTE(buffer, 6)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, 9)::BIT(8) || GET_BYTE(buffer, 8)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = B'1' AS gct,
                        (flags << 1)::BIT(3)::INT + 1 AS depth,
                        (flags << 4) = B'1' AS color_sort,
                        2 << (flags << 5)::BIT(3)::INT AS gct_size,
                        GET_BYTE(buffer, 11)::INT AS bci,
                        GET_BYTE(buffer, 12)::INT AS aspect
                ) h
        CROSS JOIN LATERAL
                (
                SELECT  13 + 3 * gct_size AS blocks_offset
                ) io
        ),
        blocks AS
        (
        SELECT  blocks_offset AS current,
                GET_BYTE(buffer, blocks_offset) AS intro
        FROM    bin
        CROSS JOIN
                header
        UNION ALL
        SELECT  current,
                GET_BYTE(buffer, current) AS intro
        FROM    (
                SELECT  current AS previous,
                        intro AS previous_intro
                FROM    blocks
                ) b
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  previous + GET_BYTE(buffer, previous + 2) + 4 AS current
                ) q
        WHERE   previous_intro = x'21'::INT
        ),
        image_offset AS
        (
        SELECT  current AS image_offset
        FROM    blocks
        WHERE   intro = x'2C'::INT
        )
SELECT  *
FROM    image_offset;
image_offset
33

This means the image block starts 33 bytes into the file.

Image header

The image header also has a fixed format easily retrievable using a little bit of BIT and BYTEA functionality:

04.image-header.sql

WITH    RECURSIVE
        bin AS
        (
        SELECT  DECODE(data, 'HEX') AS buffer
        FROM    input
        ),
        header AS
        (
        SELECT  h.*, io.*
        FROM    bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, 10)::BIT(8) AS flags
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CONVERT_FROM(SUBSTR(buffer, 1, 6), 'LATIN1') AS version,
                        (GET_BYTE(buffer, 7)::BIT(8) || GET_BYTE(buffer, 6)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, 9)::BIT(8) || GET_BYTE(buffer, 8)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = B'1' AS gct,
                        (flags << 1)::BIT(3)::INT + 1 AS depth,
                        (flags << 4) = B'1' AS color_sort,
                        2 << (flags << 5)::BIT(3)::INT AS gct_size,
                        GET_BYTE(buffer, 11)::INT AS bci,
                        GET_BYTE(buffer, 12)::INT AS aspect
                ) h
        CROSS JOIN LATERAL
                (
                SELECT  13 + 3 * gct_size AS blocks_offset
                ) io
        ),
        blocks AS
        (
        SELECT  blocks_offset AS current,
                GET_BYTE(buffer, blocks_offset) AS intro
        FROM    bin
        CROSS JOIN
                header
        UNION ALL
        SELECT  current,
                GET_BYTE(buffer, current) AS intro
        FROM    (
                SELECT  current AS previous,
                        intro AS previous_intro
                FROM    blocks
                ) b
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  previous + GET_BYTE(buffer, previous + 2) + 4 AS current
                ) q
        WHERE   previous_intro = x'21'::INT
        ),
        image_offset AS
        (
        SELECT  current AS image_offset
        FROM    blocks
        WHERE   intro = x'2C'::INT
        ),
        image_header AS
        (
        SELECT  q.*, l.*, l2.*
        FROM    image_offset
        CROSS JOIN
                bin
        CROSS JOIN
                header h
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, image_offset + 9)::BIT(8) AS flags
                ) f
        CROSS JOIN LATERAL
                (
                SELECT  (GET_BYTE(buffer, image_offset + 2)::BIT(8) || GET_BYTE(buffer, image_offset + 1)::BIT(8))::BIT(16)::INT AS left,
                        (GET_BYTE(buffer, image_offset + 4)::BIT(8) || GET_BYTE(buffer, image_offset + 3)::BIT(8))::BIT(16)::INT AS top,
                        (GET_BYTE(buffer, image_offset + 6)::BIT(8) || GET_BYTE(buffer, image_offset + 5)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, image_offset + 8)::BIT(8) || GET_BYTE(buffer, image_offset + 7)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = b'1' AS has_lct,
                        (flags << 1)::BIT(1) = b'1' AS interlace,
                        (flags << 2)::BIT(1) = b'1' AS sort
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CASE WHEN has_lct THEN 2 << (flags << 5)::BIT(3)::INT ELSE 0 END AS lct_size
                ) l
        CROSS JOIN LATERAL
                (
                SELECT  image_offset + 10 + lct_size AS image_data_offset
                ) l2
        )
SELECT  *
FROM    image_header;
left top width height has_lct interlace sort lct_size image_data_offset
0 0 10 10 False False False 0 43

We have width and height here, the same ones as in the file header (not surprising as this is the only image in the file). The rest of the header fields is also of no immediate use to us (please refer to the tutorial if you're interested in them). They, however, let us calculate image_data_offset, the offset to the actual image data.

Image data

Compressed image data is composed of multiple data sub-blocks within the image block. Every image data sub-block is 1 to 255 bytes long, and it starts with the block length and ends with 0x00, the block terminator. Before the first sub-block, there is one more value, the minimum code size. This is something that LZW compression uses, and we will need this field a little bit later. We need to obtain the compressed image data in a single BYTEA field. To do this, we extract the data from the sub-blocks recursively, ending the recursion when we come across a zero-length sub-block, then concatenated all the extracted chunks of data together:

05.lzw-data.sql

WITH    RECURSIVE
        bin AS
        (
        SELECT  DECODE(data, 'HEX') AS buffer
        FROM    input
        ),
        header AS
        (
        SELECT  h.*, io.*
        FROM    bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, 10)::BIT(8) AS flags
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CONVERT_FROM(SUBSTR(buffer, 1, 6), 'LATIN1') AS version,
                        (GET_BYTE(buffer, 7)::BIT(8) || GET_BYTE(buffer, 6)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, 9)::BIT(8) || GET_BYTE(buffer, 8)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = B'1' AS gct,
                        (flags << 1)::BIT(3)::INT + 1 AS depth,
                        (flags << 4) = B'1' AS color_sort,
                        2 << (flags << 5)::BIT(3)::INT AS gct_size,
                        GET_BYTE(buffer, 11)::INT AS bci,
                        GET_BYTE(buffer, 12)::INT AS aspect
                ) h
        CROSS JOIN LATERAL
                (
                SELECT  13 + 3 * gct_size AS blocks_offset
                ) io
        ),
        blocks AS
        (
        SELECT  blocks_offset AS current,
                GET_BYTE(buffer, blocks_offset) AS intro
        FROM    bin
        CROSS JOIN
                header
        UNION ALL
        SELECT  current,
                GET_BYTE(buffer, current) AS intro
        FROM    (
                SELECT  current AS previous,
                        intro AS previous_intro
                FROM    blocks
                ) b
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  previous + GET_BYTE(buffer, previous + 2) + 4 AS current
                ) q
        WHERE   previous_intro = x'21'::INT
        ),
        image_offset AS
        (
        SELECT  current AS image_offset
        FROM    blocks
        WHERE   intro = x'2C'::INT
        ),
        image_header AS
        (
        SELECT  q.*, l.*, l2.*
        FROM    image_offset
        CROSS JOIN
                bin
        CROSS JOIN
                header h
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, image_offset + 9)::BIT(8) AS flags
                ) f
        CROSS JOIN LATERAL
                (
                SELECT  (GET_BYTE(buffer, image_offset + 2)::BIT(8) || GET_BYTE(buffer, image_offset + 1)::BIT(8))::BIT(16)::INT AS left,
                        (GET_BYTE(buffer, image_offset + 4)::BIT(8) || GET_BYTE(buffer, image_offset + 3)::BIT(8))::BIT(16)::INT AS top,
                        (GET_BYTE(buffer, image_offset + 6)::BIT(8) || GET_BYTE(buffer, image_offset + 5)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, image_offset + 8)::BIT(8) || GET_BYTE(buffer, image_offset + 7)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = b'1' AS has_lct,
                        (flags << 1)::BIT(1) = b'1' AS interlace,
                        (flags << 2)::BIT(1) = b'1' AS sort
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CASE WHEN has_lct THEN 2 << (flags << 5)::BIT(3)::INT ELSE 0 END AS lct_size
                ) l
        CROSS JOIN LATERAL
                (
                SELECT  image_offset + 10 + lct_size AS image_data_offset
                ) l2
        ),
        image_data AS
        (
        SELECT  GET_BYTE(buffer, image_data_offset) AS code_size,
                image_data_offset + 1 AS image_first_block_offset
        FROM    image_header
        CROSS JOIN
                bin
        ),
        image_blocks AS
        (
        SELECT  image_first_block_offset AS block_offset,
                block_size,
                SUBSTR(buffer, image_first_block_offset + 2, block_size) AS block_data
        FROM    image_data
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, image_first_block_offset) AS block_size
                ) l
        UNION ALL
        SELECT  new_offset AS block_offset,
                new_block_size AS block_size,
                SUBSTR(buffer, new_offset + 2, new_block_size) AS block_data
        FROM    image_blocks
        CROSS JOIN LATERAL
                (
                SELECT  block_offset + block_size + 1 AS new_offset
                ) no
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, new_offset) AS new_block_size
                ) nbs
        WHERE   new_block_size > 0
        ),
        lzw_data AS
        (
        SELECT  code_size,
                (1 << code_size) AS clear_code,
                (1 << code_size) + 1 AS eof_code,
                compressed
        FROM    (
                SELECT  STRING_AGG(block_data, '' ORDER BY block_offset) compressed
                FROM    image_blocks
                ) i
        CROSS JOIN
                image_data
        )
SELECT  code_size, ENCODE(compressed, 'HEX')
FROM    lzw_data;
code_size encode
2 8c2d99872a1cdc33a00275ec95faa8de608c04914c01

There are always at least two sub-blocks in an image block, the last one having zero length. We now see the compressed data and a cryptic number called code_size. We will need it later to do the decompression.

LZW decompression

Here comes the most interesting part of the post, the actual decompression.

LZW is a dictionary algorithm. It tries to find the repeating patterns of colors and assign them a code in a lookup table. Here's what the lookup table for our sample file looks like (I only list first several entries here):

CodePattern
61, 1
71, 1, 1
81, 1, 2
92, 2
102, 2, 2

, etc.

Note how this table starts from 6. There is a reason for that.

The sample file only uses four colors, so the first four entries in the lookup table will correspond to those colors directly. This is common for all implementations of LZW.

Now, the first four entries (0 to 3) correspond to the colors. What do 4 and 5 do then? Those are specific to the flavor of LZW used by GIF.

4 (or, generally speaking, 2palette_size) means "clear code". It does not correspond to any color or a pattern. It acts like a soft-reset switch: when the algorithm comes across this code, it resets its state and clears the lookup table.

5 (or, generally speaking, 2palette_size + 1) means "EOF code". It's a signal to stop the conversion.

One of the most amazing properties of LZW is that although we need to look up every pattern in the code table for decompression, the code table itself is not anywhere in the file. Instead, it is reconstructed in real time. The compression algorithm always adds codes to the table successively, and does so for every (yet) unknown pattern it comes across. So does the decompression algorithm. The first entry it adds to the table is always
2palette_size + 2, the second one is 2palette_size + 2 and so on. The patterns for those entries are reconstructed from patterns for the previous codes. The lookup table grows with every new code.

Since LZW is a compression algorithm after all, it tries to pack codes to bits in the most efficient manner. Since there are at least
2palette_size + 2 size codes, every code is encoded with palette_size + 1 bits, at first at least. Since the lookup table grows with every new pattern, at some point codes will stop to fit into this number of bits. When this happens , the algorithm increases the number of bits it packs each code into by one (but no more than 12).

This means that the code stream has variable bit length. For a 4-color image, codes start coming in 3 bits, then the bit length switches to 4, then 5 and so on, up to 12 bits per code if the image is large enough.

This is one of the challenges we have to deal with while implementing the decoder in SQL.

Another challenge is that the bit stream deals with bits, but the file storage format deals with bytes. Within each byte, the bits are stored LSB first. This means that three 3-bit codes would span two bytes. The first two codes would take up the least significant bits of the first byte, and the third code would take up one most significant bit from the first byte and two least significant ones from the second one.

This does not play well with the way PostgreSQL deals with the BYTEA to BIT conversion and BIT arithmetic in general. That's why we have to shuffle the bytes around a little bit and do so for every code we extract from the stream.

To extract a code from the stream, on each step we need to know the offset to the last code (in bits) and current bit length. We then take three next bytes from the stream and concatenate them in reverse. Once we have done that, we can convert them to a BIT(24) value and just use PostgreSQL's native SUBSTRING to extract as many bits as we need to get the next code.

Once we know the next code, we should look at its value and decide what to do with that. If it's a eof_code, we just end the processing. If it's a clear_code, we reset our lookup table and the current bit length to their initial values. Finally, if it's a valid code, we reconstruct the original pattern either from the lookup table (by looking up either current code or the one we retrieved from the previous iteration), and add the new pattern to the lookup table.

To do this we need a shortcut (an analog of a return statement in imperative languages). We would implement it as a series of a sourceless queries (those without a FROM clause), combined with a UNION ALL in an inline view, which is then selected from with LIMIT 1. The first query to return a value will shortcut and the other ones won't be executed.

Finally, we need to store the lookup table between the iterations. We will employ PostgreSQL's HSTORE type for that, as it's more efficient for key lookups than an array. It only can store text keys and text values, so we will have to convert INT keys (codes) and INT[] values (patterns) to text.

06.lzw-stream.sql

WITH    RECURSIVE
        bin AS
        (
        SELECT  DECODE(data, 'HEX') AS buffer
        FROM    input
        ),
        header AS
        (
        SELECT  h.*, io.*
        FROM    bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, 10)::BIT(8) AS flags
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CONVERT_FROM(SUBSTR(buffer, 1, 6), 'LATIN1') AS version,
                        (GET_BYTE(buffer, 7)::BIT(8) || GET_BYTE(buffer, 6)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, 9)::BIT(8) || GET_BYTE(buffer, 8)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = B'1' AS gct,
                        (flags << 1)::BIT(3)::INT + 1 AS depth,
                        (flags << 4) = B'1' AS color_sort,
                        2 << (flags << 5)::BIT(3)::INT AS gct_size,
                        GET_BYTE(buffer, 11)::INT AS bci,
                        GET_BYTE(buffer, 12)::INT AS aspect
                ) h
        CROSS JOIN LATERAL
                (
                SELECT  13 + 3 * gct_size AS blocks_offset
                ) io
        ),
        blocks AS
        (
        SELECT  blocks_offset AS current,
                GET_BYTE(buffer, blocks_offset) AS intro
        FROM    bin
        CROSS JOIN
                header
        UNION ALL
        SELECT  current,
                GET_BYTE(buffer, current) AS intro
        FROM    (
                SELECT  current AS previous,
                        intro AS previous_intro
                FROM    blocks
                ) b
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  previous + GET_BYTE(buffer, previous + 2) + 4 AS current
                ) q
        WHERE   previous_intro = x'21'::INT
        ),
        image_offset AS
        (
        SELECT  current AS image_offset
        FROM    blocks
        WHERE   intro = x'2C'::INT
        ),
        image_header AS
        (
        SELECT  q.*, l.*, l2.*
        FROM    image_offset
        CROSS JOIN
                bin
        CROSS JOIN
                header h
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, image_offset + 9)::BIT(8) AS flags
                ) f
        CROSS JOIN LATERAL
                (
                SELECT  (GET_BYTE(buffer, image_offset + 2)::BIT(8) || GET_BYTE(buffer, image_offset + 1)::BIT(8))::BIT(16)::INT AS left,
                        (GET_BYTE(buffer, image_offset + 4)::BIT(8) || GET_BYTE(buffer, image_offset + 3)::BIT(8))::BIT(16)::INT AS top,
                        (GET_BYTE(buffer, image_offset + 6)::BIT(8) || GET_BYTE(buffer, image_offset + 5)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, image_offset + 8)::BIT(8) || GET_BYTE(buffer, image_offset + 7)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = b'1' AS has_lct,
                        (flags << 1)::BIT(1) = b'1' AS interlace,
                        (flags << 2)::BIT(1) = b'1' AS sort
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CASE WHEN has_lct THEN 2 << (flags << 5)::BIT(3)::INT ELSE 0 END AS lct_size
                ) l
        CROSS JOIN LATERAL
                (
                SELECT  image_offset + 10 + lct_size AS image_data_offset
                ) l2
        ),
        image_data AS
        (
        SELECT  GET_BYTE(buffer, image_data_offset) AS code_size,
                image_data_offset + 1 AS image_first_block_offset
        FROM    image_header
        CROSS JOIN
                bin
        ),
        image_blocks AS
        (
        SELECT  image_first_block_offset AS block_offset,
                block_size,
                SUBSTR(buffer, image_first_block_offset + 2, block_size) AS block_data
        FROM    image_data
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, image_first_block_offset) AS block_size
                ) l
        UNION ALL
        SELECT  new_offset AS block_offset,
                new_block_size AS block_size,
                SUBSTR(buffer, new_offset + 2, new_block_size) AS block_data
        FROM    image_blocks
        CROSS JOIN LATERAL
                (
                SELECT  block_offset + block_size + 1 AS new_offset
                ) no
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, new_offset) AS new_block_size
                ) nbs
        WHERE   new_block_size > 0
        ),
        lzw_data AS
        (
        SELECT  code_size,
                (1 << code_size) AS clear_code,
                (1 << code_size) + 1 AS eof_code,
                compressed
        FROM    (
                SELECT  STRING_AGG(block_data, '' ORDER BY block_offset) compressed
                FROM    image_blocks
                ) i
        CROSS JOIN
                image_data
        ),
        lzw_bits AS
        (
        SELECT  current_code_size,
                clear_code AS code,
                ARRAY[]::INT[] AS output_chunk,
                0 AS next_bit_offset,
                NULL::HSTORE AS codes,
                0 AS next_table_key,
                0 AS next_index
        FROM    lzw_data
        CROSS JOIN LATERAL
                (
                SELECT  code_size + 1 AS current_code_size
                ) cc
        UNION ALL
        SELECT  next_code_size,
                code,
                output_chunk,
                bit_offset + current_code_size,
                new_codes AS codes,
                next_table_key,
                next_index
        FROM    (
                SELECT  code AS previous_code, current_code_size, next_bit_offset AS bit_offset, codes, next_table_key AS current_table_key,
                        next_index + COALESCE(ARRAY_UPPER(output_chunk, 1), 0) AS next_index,
                        ld.*
                FROM    lzw_bits
                CROSS JOIN
                        (
                        SELECT  code_size AS initial_code_size,
                                clear_code,
                                eof_code,
                                compressed
                        FROM    lzw_data
                        ) ld
                ) lb
        CROSS JOIN LATERAL
                (
                SELECT  bit_offset / 8 AS byte_offset
                ) bo
        CROSS JOIN LATERAL
                (
                SELECT  (
                        CASE WHEN byte_offset < LENGTH(compressed) - 2 THEN GET_BYTE(compressed, byte_offset + 2) ELSE 0 END::BIT(8) ||
                        CASE WHEN byte_offset < LENGTH(compressed) - 1 THEN GET_BYTE(compressed, byte_offset + 1) ELSE 0 END::BIT(8) ||
                        GET_BYTE(compressed, byte_offset)::BIT(8)
                        )::BIT(24) AS cut
                ) cc
        CROSS JOIN LATERAL
                (
                SELECT  SUBSTRING(cut, 25 - current_code_size - bit_offset % 8, current_code_size)::INT AS code
                ) l
        CROSS JOIN LATERAL
                (
                SELECT  *
                FROM    (
                        SELECT  ARRAY[]::INT[] AS output_chunk,
                                HSTORE(ARRAY[]::TEXT[][]) AS new_codes,
                                eof_code + 1 AS next_table_key,
                                initial_code_size + 1 AS next_code_size
                        WHERE   code = clear_code
                        UNION ALL
                        SELECT  CASE WHEN code < clear_code THEN ARRAY ELSE (codes->(code::TEXT))::INT[] END AS output_chunk,
                                codes AS new_codes,
                                current_table_key AS next_table_key,
                                current_code_size AS next_code_size
                        WHERE   previous_code = clear_code
                        UNION ALL
                        SELECT  output_chunk,
                                CASE current_table_key WHEN 4095 THEN codes ELSE codes || HSTORE(current_table_key::TEXT, next_table_chunk::TEXT) END AS new_codes,
                                next_table_key,
                                CASE next_table_key WHEN (1 << current_code_size) THEN current_code_size + 1 ELSE current_code_size END AS next_code_size
                        FROM    (
                                SELECT  CASE WHEN previous_code < clear_code THEN ARRAY[previous_code] ELSE (codes->(previous_code::TEXT))::INT[] END AS previous_chunk,
                                        LEAST(current_table_key + 1, 4095) AS next_table_key,
                                        code < clear_code OR codes ? (code::TEXT) AS code_in_table
                                ) pc
                        CROSS JOIN LATERAL
                                (
                                SELECT  output_chunk,
                                        previous_chunk || output_chunk[1] AS next_table_chunk
                                FROM    (
                                        SELECT  CASE WHEN code < clear_code THEN ARRAY ELSE (codes->(code::TEXT))::INT[] END AS output_chunk
                                        ) q
                                WHERE   code_in_table
                                UNION ALL
                                SELECT  output_chunk, output_chunk AS next_table_chunk
                                FROM    (
                                        SELECT  previous_chunk || previous_chunk[1] AS output_chunk
                                        ) q
                                WHERE   NOT code_in_table
                                ) q
                        WHERE   code <> eof_code
                        ) q
                LIMIT 1
                ) ns
        WHERE   bit_offset < LENGTH(compressed) * 8 
        )
SELECT  *
FROM    lzw_bits;
current_code_size code output_chunk next_bit_offset codes next_table_key next_index
3 4 [] 0 None 0 0
3 4 [] 3 6 0
3 1 [1] 6 6 0
3 6 [1, 1] 9 "6"=>"{1,1}" 7 1
4 6 [1, 1] 12 "6"=>"{1,1}", "7"=>"{1,1,1}" 8 3
4 2 [2] 16 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}" 9 5
4 9 [2, 2] 20 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}" 10 6
4 9 [2, 2] 24 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}" 11 8
4 7 [1, 1, 1] 28 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}" 12 10
4 8 [1, 1, 2] 32 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}" 13 13
4 10 [2, 2, 2] 36 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}" 14 16
4 2 [2] 40 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}" 15 19
5 12 [1, 1, 1, 1] 44 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}" 16 20
5 1 [1] 49 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}" 17 24
5 14 [2, 2, 2, 2] 54 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}" 18 25
5 15 [2, 1] 59 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}" 19 29
5 6 [1, 1] 64 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}" 20 31
5 0 [0] 69 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}" 21 33
5 21 [0, 0] 74 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}" 22 34
5 0 [0] 79 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}" 23 36
5 10 [2, 2, 2] 84 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}" 24 37
5 7 [1, 1, 1] 89 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}" 25 40
5 22 [0, 0, 0] 94 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}" 26 43
5 23 [0, 2] 99 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}", "26"=>"{0,0,0,0}" 27 46
5 18 [2, 2, 2, 2, 2] 104 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}", "26"=>"{0,0,0,0}", "27"=>"{0,2,2}" 28 48
5 26 [0, 0, 0, 0] 109 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}", "26"=>"{0,0,0,0}", "27"=>"{0,2,2}", "28"=>"{2,2,2,2,2,0}" 29 53
5 7 [1, 1, 1] 114 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}", "26"=>"{0,0,0,0}", "27"=>"{0,2,2}", "28"=>"{2,2,2,2,2,0}", "29"=>"{0,0,0,0,1}" 30 57
5 10 [2, 2, 2] 119 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}", "26"=>"{0,0,0,0}", "27"=>"{0,2,2}", "28"=>"{2,2,2,2,2,0}", "29"=>"{0,0,0,0,1}", "30"=>"{1,1,1,2}" 31 60
6 29 [0, 0, 0, 0, 1] 124 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}", "26"=>"{0,0,0,0}", "27"=>"{0,2,2}", "28"=>"{2,2,2,2,2,0}", "29"=>"{0,0,0,0,1}", "30"=>"{1,1,1,2}", "31"=>"{2,2,2,0}" 32 63
6 13 [1, 1, 2, 2] 130 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}", "26"=>"{0,0,0,0}", "27"=>"{0,2,2}", "28"=>"{2,2,2,2,2,0}", "29"=>"{0,0,0,0,1}", "30"=>"{1,1,1,2}", "31"=>"{2,2,2,0}", "32"=>"{0,0,0,0,1,1}" 33 68
6 24 [2, 2, 2, 1] 136 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}", "26"=>"{0,0,0,0}", "27"=>"{0,2,2}", "28"=>"{2,2,2,2,2,0}", "29"=>"{0,0,0,0,1}", "30"=>"{1,1,1,2}", "31"=>"{2,2,2,0}", "32"=>"{0,0,0,0,1,1}", "33"=>"{1,1,2,2,2}" 34 72
6 12 [1, 1, 1, 1] 142 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}", "26"=>"{0,0,0,0}", "27"=>"{0,2,2}", "28"=>"{2,2,2,2,2,0}", "29"=>"{0,0,0,0,1}", "30"=>"{1,1,1,2}", "31"=>"{2,2,2,0}", "32"=>"{0,0,0,0,1,1}", "33"=>"{1,1,2,2,2}", "34"=>"{2,2,2,1,1}" 35 76
6 18 [2, 2, 2, 2, 2] 148 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}", "26"=>"{0,0,0,0}", "27"=>"{0,2,2}", "28"=>"{2,2,2,2,2,0}", "29"=>"{0,0,0,0,1}", "30"=>"{1,1,1,2}", "31"=>"{2,2,2,0}", "32"=>"{0,0,0,0,1,1}", "33"=>"{1,1,2,2,2}", "34"=>"{2,2,2,1,1}", "35"=>"{1,1,1,1,2}" 36 80
6 16 [1, 1, 1, 1, 1] 154 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}", "26"=>"{0,0,0,0}", "27"=>"{0,2,2}", "28"=>"{2,2,2,2,2,0}", "29"=>"{0,0,0,0,1}", "30"=>"{1,1,1,2}", "31"=>"{2,2,2,0}", "32"=>"{0,0,0,0,1,1}", "33"=>"{1,1,2,2,2}", "34"=>"{2,2,2,1,1}", "35"=>"{1,1,1,1,2}", "36"=>"{2,2,2,2,2,1}" 37 85
6 36 [2, 2, 2, 2, 2, 1] 160 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}", "26"=>"{0,0,0,0}", "27"=>"{0,2,2}", "28"=>"{2,2,2,2,2,0}", "29"=>"{0,0,0,0,1}", "30"=>"{1,1,1,2}", "31"=>"{2,2,2,0}", "32"=>"{0,0,0,0,1,1}", "33"=>"{1,1,2,2,2}", "34"=>"{2,2,2,1,1}", "35"=>"{1,1,1,1,2}", "36"=>"{2,2,2,2,2,1}", "37"=>"{1,1,1,1,1,2}" 38 90
6 12 [1, 1, 1, 1] 166 "6"=>"{1,1}", "7"=>"{1,1,1}", "8"=>"{1,1,2}", "9"=>"{2,2}", "10"=>"{2,2,2}", "11"=>"{2,2,1}", "12"=>"{1,1,1,1}", "13"=>"{1,1,2,2}", "14"=>"{2,2,2,2}", "15"=>"{2,1}", "16"=>"{1,1,1,1,1}", "17"=>"{1,2}", "18"=>"{2,2,2,2,2}", "19"=>"{2,1,1}", "20"=>"{1,1,0}", "21"=>"{0,0}", "22"=>"{0,0,0}", "23"=>"{0,2}", "24"=>"{2,2,2,1}", "25"=>"{1,1,1,0}", "26"=>"{0,0,0,0}", "27"=>"{0,2,2}", "28"=>"{2,2,2,2,2,0}", "29"=>"{0,0,0,0,1}", "30"=>"{1,1,1,2}", "31"=>"{2,2,2,0}", "32"=>"{0,0,0,0,1,1}", "33"=>"{1,1,2,2,2}", "34"=>"{2,2,2,1,1}", "35"=>"{1,1,1,1,2}", "36"=>"{2,2,2,2,2,1}", "37"=>"{1,1,1,1,1,2}", "38"=>"{2,2,2,2,2,1,1}" 39 96

Here's what we have here:

  • In lb and bo, we select the values from the previous recursion step and give them more meaningful names (so they become "previous" and "current" instead of "current" and "next" they used to be on the previous step). We are also doing some calculations we will need later and give their results aliases.
  • In cc, we shuffle the bytes for better representation of BIT so that we could cut our codes from the bit stream more easily.
  • In l, we cut the next code from the bit stream and give it a numeric value we will be working with.
  • In ns, the actual decompression happens.
    • The first two queries in ns.q do the special processing for the CLEAR code and the one right after it (the first code after CLEAR does not update the lookup table, as it's not a pattern yet).
    • The third query does the table lookup and the table update, as described in the tutorial.
    • Note that there is another UNION ALL within the third query: the table lookup and update rules are different, depending on whether the code is in the lookup table already or not.
    • The third query also handles the EOF code (which just ends the recursion as no results would be returned from either query) and the possible 12-bit overflow.
  • Note that we also explicitly select two additional fields in lzw_bits: next_table_key (the next code which we will be putting in the lookup table) and next_index (the running count of decoded color indexes so far). Those are not strictly necessary as they can be reconstructed, respectively, from the lookup table and previous query records (using window functions). However, they improve the query performance greatly, so it makes sense to drag them through the recursion, even though it makes it a little bit more complex.

Mapping color indexes to actual colors

Now that we have a recordset of patterns (arrays), we can make a recordset of individual color indexes by unnesting every array with ordinality and adding the ordinality to the next_index (which is, I will remind, the total length of patterns selected so far). This will give us a rowset of color indexes, each with its ordinal position in the original bitmap:

07.indexes.sql

WITH    RECURSIVE
        bin AS
        (
        SELECT  DECODE(data, 'HEX') AS buffer
        FROM    input
        ),
        header AS
        (
        SELECT  h.*, io.*
        FROM    bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, 10)::BIT(8) AS flags
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CONVERT_FROM(SUBSTR(buffer, 1, 6), 'LATIN1') AS version,
                        (GET_BYTE(buffer, 7)::BIT(8) || GET_BYTE(buffer, 6)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, 9)::BIT(8) || GET_BYTE(buffer, 8)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = B'1' AS gct,
                        (flags << 1)::BIT(3)::INT + 1 AS depth,
                        (flags << 4) = B'1' AS color_sort,
                        2 << (flags << 5)::BIT(3)::INT AS gct_size,
                        GET_BYTE(buffer, 11)::INT AS bci,
                        GET_BYTE(buffer, 12)::INT AS aspect
                ) h
        CROSS JOIN LATERAL
                (
                SELECT  13 + 3 * gct_size AS blocks_offset
                ) io
        ),
        blocks AS
        (
        SELECT  blocks_offset AS current,
                GET_BYTE(buffer, blocks_offset) AS intro
        FROM    bin
        CROSS JOIN
                header
        UNION ALL
        SELECT  current,
                GET_BYTE(buffer, current) AS intro
        FROM    (
                SELECT  current AS previous,
                        intro AS previous_intro
                FROM    blocks
                ) b
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  previous + GET_BYTE(buffer, previous + 2) + 4 AS current
                ) q
        WHERE   previous_intro = x'21'::INT
        ),
        image_offset AS
        (
        SELECT  current AS image_offset
        FROM    blocks
        WHERE   intro = x'2C'::INT
        ),
        image_header AS
        (
        SELECT  q.*, l.*, l2.*
        FROM    image_offset
        CROSS JOIN
                bin
        CROSS JOIN
                header h
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, image_offset + 9)::BIT(8) AS flags
                ) f
        CROSS JOIN LATERAL
                (
                SELECT  (GET_BYTE(buffer, image_offset + 2)::BIT(8) || GET_BYTE(buffer, image_offset + 1)::BIT(8))::BIT(16)::INT AS left,
                        (GET_BYTE(buffer, image_offset + 4)::BIT(8) || GET_BYTE(buffer, image_offset + 3)::BIT(8))::BIT(16)::INT AS top,
                        (GET_BYTE(buffer, image_offset + 6)::BIT(8) || GET_BYTE(buffer, image_offset + 5)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, image_offset + 8)::BIT(8) || GET_BYTE(buffer, image_offset + 7)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = b'1' AS has_lct,
                        (flags << 1)::BIT(1) = b'1' AS interlace,
                        (flags << 2)::BIT(1) = b'1' AS sort
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CASE WHEN has_lct THEN 2 << (flags << 5)::BIT(3)::INT ELSE 0 END AS lct_size
                ) l
        CROSS JOIN LATERAL
                (
                SELECT  image_offset + 10 + lct_size AS image_data_offset
                ) l2
        ),
        image_data AS
        (
        SELECT  GET_BYTE(buffer, image_data_offset) AS code_size,
                image_data_offset + 1 AS image_first_block_offset
        FROM    image_header
        CROSS JOIN
                bin
        ),
        image_blocks AS
        (
        SELECT  image_first_block_offset AS block_offset,
                block_size,
                SUBSTR(buffer, image_first_block_offset + 2, block_size) AS block_data
        FROM    image_data
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, image_first_block_offset) AS block_size
                ) l
        UNION ALL
        SELECT  new_offset AS block_offset,
                new_block_size AS block_size,
                SUBSTR(buffer, new_offset + 2, new_block_size) AS block_data
        FROM    image_blocks
        CROSS JOIN LATERAL
                (
                SELECT  block_offset + block_size + 1 AS new_offset
                ) no
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, new_offset) AS new_block_size
                ) nbs
        WHERE   new_block_size > 0
        ),
        lzw_data AS
        (
        SELECT  code_size,
                (1 << code_size) AS clear_code,
                (1 << code_size) + 1 AS eof_code,
                compressed
        FROM    (
                SELECT  STRING_AGG(block_data, '' ORDER BY block_offset) compressed
                FROM    image_blocks
                ) i
        CROSS JOIN
                image_data
        ),
        lzw_bits AS
        (
        SELECT  current_code_size,
                clear_code AS code,
                ARRAY[]::INT[] AS output_chunk,
                0 AS next_bit_offset,
                NULL::HSTORE AS codes,
                0 AS next_table_key,
                0 AS next_index
        FROM    lzw_data
        CROSS JOIN LATERAL
                (
                SELECT  code_size + 1 AS current_code_size
                ) cc
        UNION ALL
        SELECT  next_code_size,
                code,
                output_chunk,
                bit_offset + current_code_size,
                new_codes AS codes,
                next_table_key,
                next_index
        FROM    (
                SELECT  code AS previous_code, current_code_size, next_bit_offset AS bit_offset, codes, next_table_key AS current_table_key,
                        next_index + COALESCE(ARRAY_UPPER(output_chunk, 1), 0) AS next_index,
                        ld.*
                FROM    lzw_bits
                CROSS JOIN
                        (
                        SELECT  code_size AS initial_code_size,
                                clear_code,
                                eof_code,
                                compressed
                        FROM    lzw_data
                        ) ld
                ) lb
        CROSS JOIN LATERAL
                (
                SELECT  bit_offset / 8 AS byte_offset
                ) bo
        CROSS JOIN LATERAL
                (
                SELECT  (
                        CASE WHEN byte_offset < LENGTH(compressed) - 2 THEN GET_BYTE(compressed, byte_offset + 2) ELSE 0 END::BIT(8) ||
                        CASE WHEN byte_offset < LENGTH(compressed) - 1 THEN GET_BYTE(compressed, byte_offset + 1) ELSE 0 END::BIT(8) ||
                        GET_BYTE(compressed, byte_offset)::BIT(8)
                        )::BIT(24) AS cut
                ) cc
        CROSS JOIN LATERAL
                (
                SELECT  SUBSTRING(cut, 25 - current_code_size - bit_offset % 8, current_code_size)::INT AS code
                ) l
        CROSS JOIN LATERAL
                (
                SELECT  *
                FROM    (
                        SELECT  ARRAY[]::INT[] AS output_chunk,
                                HSTORE(ARRAY[]::TEXT[][]) AS new_codes,
                                eof_code + 1 AS next_table_key,
                                initial_code_size + 1 AS next_code_size
                        WHERE   code = clear_code
                        UNION ALL
                        SELECT  CASE WHEN code < clear_code THEN ARRAY ELSE (codes->(code::TEXT))::INT[] END AS output_chunk,
                                codes AS new_codes,
                                current_table_key AS next_table_key,
                                current_code_size AS next_code_size
                        WHERE   previous_code = clear_code
                        UNION ALL
                        SELECT  output_chunk,
                                CASE current_table_key WHEN 4095 THEN codes ELSE codes || HSTORE(current_table_key::TEXT, next_table_chunk::TEXT) END AS new_codes,
                                next_table_key,
                                CASE next_table_key WHEN (1 << current_code_size) THEN current_code_size + 1 ELSE current_code_size END AS next_code_size
                        FROM    (
                                SELECT  CASE WHEN previous_code < clear_code THEN ARRAY[previous_code] ELSE (codes->(previous_code::TEXT))::INT[] END AS previous_chunk,
                                        LEAST(current_table_key + 1, 4095) AS next_table_key,
                                        code < clear_code OR codes ? (code::TEXT) AS code_in_table
                                ) pc
                        CROSS JOIN LATERAL
                                (
                                SELECT  output_chunk,
                                        previous_chunk || output_chunk[1] AS next_table_chunk
                                FROM    (
                                        SELECT  CASE WHEN code < clear_code THEN ARRAY ELSE (codes->(code::TEXT))::INT[] END AS output_chunk
                                        ) q
                                WHERE   code_in_table
                                UNION ALL
                                SELECT  output_chunk, output_chunk AS next_table_chunk
                                FROM    (
                                        SELECT  previous_chunk || previous_chunk[1] AS output_chunk
                                        ) q
                                WHERE   NOT code_in_table
                                        AND code <> eof_code
                                ) q
                        ) q
                LIMIT 1
                ) ns
        WHERE   previous_code IS DISTINCT FROM eof_code
                AND bit_offset < LENGTH(compressed) * 8 
        ),
        indices AS
        (
        SELECT  idx, next_index + position - 1 AS rn
        FROM    lzw_bits
        CROSS JOIN LATERAL
                UNNEST(output_chunk) WITH ORDINALITY q (idx, position)
        )
SELECT  *
FROM    indices;
idx rn
1 0
1 1
1 2
1 3
1 4
2 5
2 94
1 95
1 96
1 97
1 98
1 99

(results cut for readability).

We have exactly 100 records here, which corresponds to our 10×10 image. Those are not actual colors though yet, just the indexes.

To retrieve the actual colors we have to look them up by index in the Global Color Table (remember that thing)? This is just a region in our original binary stream, and the color indexes are nothing but offsets from the beginning of this region. And while we're at that, we could just as well convert our image to grayscale: there is a formula for that.

08.pixels.sql

WITH    RECURSIVE
        bin AS
        (
        SELECT  DECODE(data, 'HEX') AS buffer
        FROM    input
        ),
        header AS
        (
        SELECT  h.*, io.*
        FROM    bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, 10)::BIT(8) AS flags
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CONVERT_FROM(SUBSTR(buffer, 1, 6), 'LATIN1') AS version,
                        (GET_BYTE(buffer, 7)::BIT(8) || GET_BYTE(buffer, 6)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, 9)::BIT(8) || GET_BYTE(buffer, 8)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = B'1' AS gct,
                        (flags << 1)::BIT(3)::INT + 1 AS depth,
                        (flags << 4) = B'1' AS color_sort,
                        2 << (flags << 5)::BIT(3)::INT AS gct_size,
                        GET_BYTE(buffer, 11)::INT AS bci,
                        GET_BYTE(buffer, 12)::INT AS aspect
                ) h
        CROSS JOIN LATERAL
                (
                SELECT  13 + 3 * gct_size AS blocks_offset
                ) io
        ),
        blocks AS
        (
        SELECT  blocks_offset AS current,
                GET_BYTE(buffer, blocks_offset) AS intro
        FROM    bin
        CROSS JOIN
                header
        UNION ALL
        SELECT  current,
                GET_BYTE(buffer, current) AS intro
        FROM    (
                SELECT  current AS previous,
                        intro AS previous_intro
                FROM    blocks
                ) b
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  previous + GET_BYTE(buffer, previous + 2) + 4 AS current
                ) q
        WHERE   previous_intro = x'21'::INT
        ),
        image_offset AS
        (
        SELECT  current AS image_offset
        FROM    blocks
        WHERE   intro = x'2C'::INT
        ),
        image_header AS
        (
        SELECT  q.*, l.*, l2.*
        FROM    image_offset
        CROSS JOIN
                bin
        CROSS JOIN
                header h
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, image_offset + 9)::BIT(8) AS flags
                ) f
        CROSS JOIN LATERAL
                (
                SELECT  (GET_BYTE(buffer, image_offset + 2)::BIT(8) || GET_BYTE(buffer, image_offset + 1)::BIT(8))::BIT(16)::INT AS left,
                        (GET_BYTE(buffer, image_offset + 4)::BIT(8) || GET_BYTE(buffer, image_offset + 3)::BIT(8))::BIT(16)::INT AS top,
                        (GET_BYTE(buffer, image_offset + 6)::BIT(8) || GET_BYTE(buffer, image_offset + 5)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, image_offset + 8)::BIT(8) || GET_BYTE(buffer, image_offset + 7)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = b'1' AS has_lct,
                        (flags << 1)::BIT(1) = b'1' AS interlace,
                        (flags << 2)::BIT(1) = b'1' AS sort
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CASE WHEN has_lct THEN 2 << (flags << 5)::BIT(3)::INT ELSE 0 END AS lct_size
                ) l
        CROSS JOIN LATERAL
                (
                SELECT  image_offset + 10 + lct_size AS image_data_offset
                ) l2
        ),
        image_data AS
        (
        SELECT  GET_BYTE(buffer, image_data_offset) AS code_size,
                image_data_offset + 1 AS image_first_block_offset
        FROM    image_header
        CROSS JOIN
                bin
        ),
        image_blocks AS
        (
        SELECT  image_first_block_offset AS block_offset,
                block_size,
                SUBSTR(buffer, image_first_block_offset + 2, block_size) AS block_data
        FROM    image_data
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, image_first_block_offset) AS block_size
                ) l
        UNION ALL
        SELECT  new_offset AS block_offset,
                new_block_size AS block_size,
                SUBSTR(buffer, new_offset + 2, new_block_size) AS block_data
        FROM    image_blocks
        CROSS JOIN LATERAL
                (
                SELECT  block_offset + block_size + 1 AS new_offset
                ) no
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, new_offset) AS new_block_size
                ) nbs
        WHERE   new_block_size > 0
        ),
        lzw_data AS
        (
        SELECT  code_size,
                (1 << code_size) AS clear_code,
                (1 << code_size) + 1 AS eof_code,
                compressed
        FROM    (
                SELECT  STRING_AGG(block_data, '' ORDER BY block_offset) compressed
                FROM    image_blocks
                ) i
        CROSS JOIN
                image_data
        ),
        lzw_bits AS
        (
        SELECT  current_code_size,
                clear_code AS code,
                ARRAY[]::INT[] AS output_chunk,
                0 AS next_bit_offset,
                NULL::HSTORE AS codes,
                0 AS next_table_key,
                0 AS next_index
        FROM    lzw_data
        CROSS JOIN LATERAL
                (
                SELECT  code_size + 1 AS current_code_size
                ) cc
        UNION ALL
        SELECT  next_code_size,
                code,
                output_chunk,
                bit_offset + current_code_size,
                new_codes AS codes,
                next_table_key,
                next_index
        FROM    (
                SELECT  code AS previous_code, current_code_size, next_bit_offset AS bit_offset, codes, next_table_key AS current_table_key,
                        next_index + COALESCE(ARRAY_UPPER(output_chunk, 1), 0) AS next_index,
                        ld.*
                FROM    lzw_bits
                CROSS JOIN
                        (
                        SELECT  code_size AS initial_code_size,
                                clear_code,
                                eof_code,
                                compressed
                        FROM    lzw_data
                        ) ld
                ) lb
        CROSS JOIN LATERAL
                (
                SELECT  bit_offset / 8 AS byte_offset
                ) bo
        CROSS JOIN LATERAL
                (
                SELECT  (
                        CASE WHEN byte_offset < LENGTH(compressed) - 2 THEN GET_BYTE(compressed, byte_offset + 2) ELSE 0 END::BIT(8) ||
                        CASE WHEN byte_offset < LENGTH(compressed) - 1 THEN GET_BYTE(compressed, byte_offset + 1) ELSE 0 END::BIT(8) ||
                        GET_BYTE(compressed, byte_offset)::BIT(8)
                        )::BIT(24) AS cut
                ) cc
        CROSS JOIN LATERAL
                (
                SELECT  SUBSTRING(cut, 25 - current_code_size - bit_offset % 8, current_code_size)::INT AS code
                ) l
        CROSS JOIN LATERAL
                (
                SELECT  *
                FROM    (
                        SELECT  ARRAY[]::INT[] AS output_chunk,
                                HSTORE(ARRAY[]::TEXT[][]) AS new_codes,
                                eof_code + 1 AS next_table_key,
                                initial_code_size + 1 AS next_code_size
                        WHERE   code = clear_code
                        UNION ALL
                        SELECT  CASE WHEN code < clear_code THEN ARRAY ELSE (codes->(code::TEXT))::INT[] END AS output_chunk,
                                codes AS new_codes,
                                current_table_key AS next_table_key,
                                current_code_size AS next_code_size
                        WHERE   previous_code = clear_code
                        UNION ALL
                        SELECT  output_chunk,
                                CASE current_table_key WHEN 4095 THEN codes ELSE codes || HSTORE(current_table_key::TEXT, next_table_chunk::TEXT) END AS new_codes,
                                next_table_key,
                                CASE next_table_key WHEN (1 << current_code_size) THEN current_code_size + 1 ELSE current_code_size END AS next_code_size
                        FROM    (
                                SELECT  CASE WHEN previous_code < clear_code THEN ARRAY[previous_code] ELSE (codes->(previous_code::TEXT))::INT[] END AS previous_chunk,
                                        LEAST(current_table_key + 1, 4095) AS next_table_key,
                                        code < clear_code OR codes ? (code::TEXT) AS code_in_table
                                ) pc
                        CROSS JOIN LATERAL
                                (
                                SELECT  output_chunk,
                                        previous_chunk || output_chunk[1] AS next_table_chunk
                                FROM    (
                                        SELECT  CASE WHEN code < clear_code THEN ARRAY ELSE (codes->(code::TEXT))::INT[] END AS output_chunk
                                        ) q
                                WHERE   code_in_table
                                UNION ALL
                                SELECT  output_chunk, output_chunk AS next_table_chunk
                                FROM    (
                                        SELECT  previous_chunk || previous_chunk[1] AS output_chunk
                                        ) q
                                WHERE   NOT code_in_table
                                        AND code <> eof_code
                                ) q
                        ) q
                LIMIT 1
                ) ns
        WHERE   previous_code IS DISTINCT FROM eof_code
                AND bit_offset < LENGTH(compressed) * 8 
        ),
        indices AS
        (
        SELECT  idx, next_index + position - 1 AS rn
        FROM    lzw_bits
        CROSS JOIN LATERAL
                UNNEST(output_chunk) WITH ORDINALITY q (idx, position)
        ),
        pixels AS
        (
        SELECT  idx,
                rn % width AS x,
                rn / width AS y,
                luma
        FROM    indices
        CROSS JOIN
                bin
        CROSS JOIN
                header
        CROSS JOIN LATERAL
                (
                SELECT  255 max_value,
                        2.2 gamma,
                        GET_BYTE(buffer, 13 + idx * 3 + 0) r,
                        GET_BYTE(buffer, 13 + idx * 3 + 1) g,
                        GET_BYTE(buffer, 13 + idx * 3 + 2) b,
                        .2126 rw,
                        .7152 gw,
                        .0722 bw
                ) c
        CROSS JOIN LATERAL
                (
                SELECT  ((r::FLOAT / max_value) ^ gamma) * rw +
                        ((g::FLOAT / max_value) ^ gamma) * gw +
                        ((b::FLOAT / max_value) ^ gamma) * bw AS luma
                ) l
        )
SELECT  *
FROM    pixels;
idx x y luma
1 0 0 0.2126
1 1 0 0.2126
1 2 0 0.2126
1 3 0 0.2126
1 4 0 0.2126
2 5 0 0.0722
2 4 9 0.0722
1 5 9 0.2126
1 6 9 0.2126
1 7 9 0.2126
1 8 9 0.2126
1 9 9 0.2126

Rendering the result

Finally we need to render our picture on the screen. We do that as usual:

  • Cross join two GENERATE_SERIES for width and height
  • Left join it with the rowset of pixels on (x, y)
  • Look up the ASCII art character depending on the pixel luminance
  • Group by y and STRING_AGG the ASCII art characters without the separator, ordering by x

gif.sql

WITH    RECURSIVE
        bin AS
        (
        SELECT  DECODE(data, 'HEX') AS buffer
        FROM    input
        ),
        header AS
        (
        SELECT  h.*, io.*
        FROM    bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, 10)::BIT(8) AS flags
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CONVERT_FROM(SUBSTR(buffer, 1, 6), 'LATIN1') AS version,
                        (GET_BYTE(buffer, 7)::BIT(8) || GET_BYTE(buffer, 6)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, 9)::BIT(8) || GET_BYTE(buffer, 8)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = B'1' AS gct,
                        (flags << 1)::BIT(3)::INT + 1 AS depth,
                        (flags << 4) = B'1' AS color_sort,
                        2 << (flags << 5)::BIT(3)::INT AS gct_size,
                        GET_BYTE(buffer, 11)::INT AS bci,
                        GET_BYTE(buffer, 12)::INT AS aspect
                ) h
        CROSS JOIN LATERAL
                (
                SELECT  13 + 3 * gct_size AS blocks_offset
                ) io
        ),
        blocks AS
        (
        SELECT  blocks_offset AS current,
                GET_BYTE(buffer, blocks_offset) AS intro
        FROM    bin
        CROSS JOIN
                header
        UNION ALL
        SELECT  current,
                GET_BYTE(buffer, current) AS intro
        FROM    (
                SELECT  current AS previous,
                        intro AS previous_intro
                FROM    blocks
                ) b
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  previous + GET_BYTE(buffer, previous + 2) + 4 AS current
                ) q
        WHERE   previous_intro = x'21'::INT
        ),
        image_offset AS
        (
        SELECT  current AS image_offset
        FROM    blocks
        WHERE   intro = x'2C'::INT
        ),
        image_header AS
        (
        SELECT  q.*, l.*, l2.*
        FROM    image_offset
        CROSS JOIN
                bin
        CROSS JOIN
                header h
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, image_offset + 9)::BIT(8) AS flags
                ) f
        CROSS JOIN LATERAL
                (
                SELECT  (GET_BYTE(buffer, image_offset + 2)::BIT(8) || GET_BYTE(buffer, image_offset + 1)::BIT(8))::BIT(16)::INT AS left,
                        (GET_BYTE(buffer, image_offset + 4)::BIT(8) || GET_BYTE(buffer, image_offset + 3)::BIT(8))::BIT(16)::INT AS top,
                        (GET_BYTE(buffer, image_offset + 6)::BIT(8) || GET_BYTE(buffer, image_offset + 5)::BIT(8))::BIT(16)::INT AS width,
                        (GET_BYTE(buffer, image_offset + 8)::BIT(8) || GET_BYTE(buffer, image_offset + 7)::BIT(8))::BIT(16)::INT AS height,
                        flags::BIT(1) = b'1' AS has_lct,
                        (flags << 1)::BIT(1) = b'1' AS interlace,
                        (flags << 2)::BIT(1) = b'1' AS sort
                ) q
        CROSS JOIN LATERAL
                (
                SELECT  CASE WHEN has_lct THEN 2 << (flags << 5)::BIT(3)::INT ELSE 0 END AS lct_size
                ) l
        CROSS JOIN LATERAL
                (
                SELECT  image_offset + 10 + lct_size AS image_data_offset
                ) l2
        ),
        image_data AS
        (
        SELECT  GET_BYTE(buffer, image_data_offset) AS code_size,
                image_data_offset + 1 AS image_first_block_offset
        FROM    image_header
        CROSS JOIN
                bin
        ),
        image_blocks AS
        (
        SELECT  image_first_block_offset AS block_offset,
                block_size,
                SUBSTR(buffer, image_first_block_offset + 2, block_size) AS block_data
        FROM    image_data
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, image_first_block_offset) AS block_size
                ) l
        UNION ALL
        SELECT  new_offset AS block_offset,
                new_block_size AS block_size,
                SUBSTR(buffer, new_offset + 2, new_block_size) AS block_data
        FROM    image_blocks
        CROSS JOIN LATERAL
                (
                SELECT  block_offset + block_size + 1 AS new_offset
                ) no
        CROSS JOIN
                bin
        CROSS JOIN LATERAL
                (
                SELECT  GET_BYTE(buffer, new_offset) AS new_block_size
                ) nbs
        WHERE   new_block_size > 0
        ),
        lzw_data AS
        (
        SELECT  code_size,
                (1 << code_size) AS clear_code,
                (1 << code_size) + 1 AS eof_code,
                compressed
        FROM    (
                SELECT  STRING_AGG(block_data, '' ORDER BY block_offset) compressed
                FROM    image_blocks
                ) i
        CROSS JOIN
                image_data
        ),
        lzw_bits AS
        (
        SELECT  current_code_size,
                clear_code AS code,
                ARRAY[]::INT[] AS output_chunk,
                0 AS next_bit_offset,
                NULL::HSTORE AS codes,
                0 AS next_table_key,
                0 AS next_index
        FROM    lzw_data
        CROSS JOIN LATERAL
                (
                SELECT  code_size + 1 AS current_code_size
                ) cc
        UNION ALL
        SELECT  next_code_size,
                code,
                output_chunk,
                bit_offset + current_code_size,
                new_codes AS codes,
                next_table_key,
                next_index
        FROM    (
                SELECT  code AS previous_code, current_code_size, next_bit_offset AS bit_offset, codes, next_table_key AS current_table_key,
                        next_index + COALESCE(ARRAY_UPPER(output_chunk, 1), 0) AS next_index,
                        ld.*
                FROM    lzw_bits
                CROSS JOIN
                        (
                        SELECT  code_size AS initial_code_size,
                                clear_code,
                                eof_code,
                                compressed
                        FROM    lzw_data
                        ) ld
                ) lb
        CROSS JOIN LATERAL
                (
                SELECT  bit_offset / 8 AS byte_offset
                ) bo
        CROSS JOIN LATERAL
                (
                SELECT  (
                        CASE WHEN byte_offset < LENGTH(compressed) - 2 THEN GET_BYTE(compressed, byte_offset + 2) ELSE 0 END::BIT(8) ||
                        CASE WHEN byte_offset < LENGTH(compressed) - 1 THEN GET_BYTE(compressed, byte_offset + 1) ELSE 0 END::BIT(8) ||
                        GET_BYTE(compressed, byte_offset)::BIT(8)
                        )::BIT(24) AS cut
                ) cc
        CROSS JOIN LATERAL
                (
                SELECT  SUBSTRING(cut, 25 - current_code_size - bit_offset % 8, current_code_size)::INT AS code
                ) l
        CROSS JOIN LATERAL
                (
                SELECT  *
                FROM    (
                        SELECT  ARRAY[]::INT[] AS output_chunk,
                                HSTORE(ARRAY[]::TEXT[][]) AS new_codes,
                                eof_code + 1 AS next_table_key,
                                initial_code_size + 1 AS next_code_size
                        WHERE   code = clear_code
                        UNION ALL
                        SELECT  CASE WHEN code < clear_code THEN ARRAY ELSE (codes->(code::TEXT))::INT[] END AS output_chunk,
                                codes AS new_codes,
                                current_table_key AS next_table_key,
                                current_code_size AS next_code_size
                        WHERE   previous_code = clear_code
                        UNION ALL
                        SELECT  output_chunk,
                                CASE current_table_key WHEN 4095 THEN codes ELSE codes || HSTORE(current_table_key::TEXT, next_table_chunk::TEXT) END AS new_codes,
                                next_table_key,
                                CASE next_table_key WHEN (1 << current_code_size) THEN current_code_size + 1 ELSE current_code_size END AS next_code_size
                        FROM    (
                                SELECT  CASE WHEN previous_code < clear_code THEN ARRAY[previous_code] ELSE (codes->(previous_code::TEXT))::INT[] END AS previous_chunk,
                                        LEAST(current_table_key + 1, 4095) AS next_table_key,
                                        code < clear_code OR codes ? (code::TEXT) AS code_in_table
                                ) pc
                        CROSS JOIN LATERAL
                                (
                                SELECT  output_chunk,
                                        previous_chunk || output_chunk[1] AS next_table_chunk
                                FROM    (
                                        SELECT  CASE WHEN code < clear_code THEN ARRAY ELSE (codes->(code::TEXT))::INT[] END AS output_chunk
                                        ) q
                                WHERE   code_in_table
                                UNION ALL
                                SELECT  output_chunk, output_chunk AS next_table_chunk
                                FROM    (
                                        SELECT  previous_chunk || previous_chunk[1] AS output_chunk
                                        ) q
                                WHERE   NOT code_in_table
                                        AND code <> eof_code
                                ) q
                        ) q
                LIMIT 1
                ) ns
        WHERE   previous_code IS DISTINCT FROM eof_code
                AND bit_offset < LENGTH(compressed) * 8 
        ),
        indices AS
        (
        SELECT  idx, next_index + position - 1 AS rn
        FROM    lzw_bits
        CROSS JOIN LATERAL
                UNNEST(output_chunk) WITH ORDINALITY q (idx, position)
        ),
        pixels AS
        (
        SELECT  idx,
                rn % width AS x,
                rn / width AS y,
                luma
        FROM    indices
        CROSS JOIN
                bin
        CROSS JOIN
                header
        CROSS JOIN LATERAL
                (
                SELECT  255 max_value,
                        2.2 gamma,
                        GET_BYTE(buffer, 13 + idx * 3 + 0) r,
                        GET_BYTE(buffer, 13 + idx * 3 + 1) g,
                        GET_BYTE(buffer, 13 + idx * 3 + 2) b,
                        .2126 rw,
                        .7152 gw,
                        .0722 bw
                ) c
        CROSS JOIN LATERAL
                (
                SELECT  ((r::FLOAT / max_value) ^ gamma) * rw +
                        ((g::FLOAT / max_value) ^ gamma) * gw +
                        ((b::FLOAT / max_value) ^ gamma) * bw AS luma
                ) l
        ),
        picture AS
        (
        SELECT  STRING_AGG(SUBSTRING(palette, LEAST(FLOOR(COALESCE(luma, 0) * LENGTH(palette))::INT + 1, LENGTH(palette)), 1), '' ORDER BY x)
        FROM    (
                SELECT  *,
                        ' .*:o&8#' AS palette
                FROM    header
                ) h
        CROSS JOIN LATERAL
                GENERATE_SERIES(0, width - 1) x
        CROSS JOIN LATERAL
                GENERATE_SERIES(0, height - 1) y
        LEFT JOIN
                pixels
        USING   (x, y)
        GROUP BY
                y
        ORDER BY
                y
        )
SELECT  *
FROM    picture;
string_agg
.....
.....
.....
...####
...####
####...
####...
.....
.....
.....

Here we are!

More pictures

So let's do a cat:

string_agg
.. . *#&&: :::
.. . :#&::
. . .. .. &#&:* . .
.. . . . . &&&:. . .. .
.. : . &#::. . .
. . .. .&&:: . *
. . * . . *&&&:
* . . .. .. :&&:: . . ..
. : . . . . . &&&:: . . .... ....*... .* .
. . . . &#&:: . ..* ..... . ....*
. . ...*. * .* * * ..&o:: ...... .. ... ... . .....
. . : . .. ***.*.*:*: .*... * . . .. . ... . .* *
. . .. ...**.*.*.*..:: .... ..... ..... .
.*..:. . .*... *.. . . . .. . . . ..
. ... ......* .*. . .. . . ... .
. . * .. *. .. . * .*.. ..
. . . :* * .. . .
* . . . . * . . .
. . . .. . . * .. .
. . .. *.. . .
. . . . .* . * . . . . . . . . .
.* . ...* . . . . . .. . . *......* ... *.. . .
. * . . . . . . . . ... .*. . . . ... . . . . .
. . . . * . . . .. . ** * ..* . .. . . . . . . .
. .. . . . . . ....* .*... . .* . . . . . ...* . .
. . * .. * . . . .. *.*..* . . .. .. .. . .
.. .. *. .. .* *: **...*. .. . .* .:... . .. ....
. . . . . . .* .. . . .. :. . . . . .
. . . . . . ..* .... :* . .. .
* . *. .. . .* . ....... . . . *
. . . . . . ...* . . .....*. . . .
. . .. . . ...* ..*.:*.*** ...
. ** * * . . .o****.**. . ..
. . . . . .. ..*... . ...***.*.*. .. .*
* .. . ...*o*....** ... .
. * . .**..***. .. * .
.* ....*..... . *.
. . . ......... .
. . . .....*
: . . . . .... .
: . ... . . . ....
.. . * . . . .
.o ... *. . . . .
..:: ... * .. .
...*. * *. .. . . .*
..:: ..... .
.:.:: . ... .*. . .
:..: . . .. ..
.. .. . * .* .
::... : ... . .
::::.: * .: . .. .. . .
.::::::*.* :&: . ...
. :::::::*:: :&:& . . . .
&::::&::*.* :::&& . .. . .
. ::::::::&.*.: &:&&& . . . .
&::::::::&.:* :&&&:& . . . .
. &:::::*::&: :&&:&&& .
. #::::*:*::& #:&&&:: . . .
&::::::::&:# &&&&&&&: .
&:.:.::&::::& .:&#&&: .&&&&:&&:
::.:*::::::&:#&&&&&&:&##&:&&&::: .
::....::.::::::&:&&&&&&&&&&&:&:: . .
::...*.:.:::::::::&&&&&&&#&&&&:# .
.:. .:.::.::::::::&&&:&&#&&&&::&
.:. .:..::.::::&:.:&&&&&&#&#&&::. .
.: .:...*.*.:::&*:&&&&&#&&8&&:::
::. ..:.*.:.*:&&&#&:&&#8#####&o: .
.:. .:..:...::. :&&:&&&##:::#&&&
.:. ...:.::::. &::&&#& :&#&#
:.*.:.:.:::::: . :&::&#& .:#&# .
...:.:.....::#:. .:::&&:.:.#&#
*.:.:..:....::#:::. .:::&#&&:&#&# .
...:.*...::::::&&:.*...:::&#&#&&#
.:......:.:::::::&::. .o&:#&#&&#
*...:...:::.::::::::&:.*:&&&&#&# . .
.... ....*.. ....... ..:....:::.:....:::::&&::&&&&&&# .
......* ..*... :.*.*. ...*... ..... . . ..... . :.....*....*.::::::&::*:&&&#&&# . .
..**.... . ..... .*: .:**.....*.. ....... ... . . .... ...*......:.::::::&::::.:&&&&&: .
. ....... . ... . .*...................... ... . .:.........*.::::&::&:&.&&&&# .
.....***. ...*.* . . ... . .. . :...*.**.**.*..**....... :.... .:.*:*:.:.:::.:::::&&#:&&&#. .
.... ..*.* ........ . .. :.. ..*.. ..* .............*.... ****. . .:.:..*.*::::::::&:#&&#&8#& : .
***.**.*. ....... ...*...*. .. ..:.. ......*.......... . ..::::.:*::::::.:::&#&&#&& .
*.* ..*:**.. .....*. . . ....:o&&&#o....:. . o:: ................ ....::::::*:::::::&&&#&&# .
***.**:*. **::***. ...... .. ....::&&&&&o. ** . :::. ............. . ..... :*:*::::::::::::&&&&&&# .
...*.. *.**:**. ....... **:::&&#&#. .. :..: .*......... .*:::: ...:::::::::::&&&&8#&# :.*.... . ..
*:.*.**... *:.*:o&&&: ...* *..o ..:& * . .... .:..*:.. .. *...**::::&:&&&&&&&#&&: . :.*:. .*..
***.. o:..::&: ..*:*:.. *..*:*:&o&. .*. . . ...*.:**. *::::. ...:.::::&:&&&&#8##8#&#: :. .. .
. . ...*.*...&#..::..*:*:. *:*o ...*.:&&&: *:o:o:o::o::..*::o::::*:::*. * :....:::::&&&&##8##&&#&##. ....*:*::*... .....
. * ..... ..*.**::.*:::..*:*:o&&: .*::*.:*:*::*. .*..:& o .:*..:o:o*oo:o:o:*..*:::*::*..* ::..:::::&&:&#&8##&##&#&&# .:::. . .:.:.. ::::*::..:. ...
.**. .*.*:*:*. .***:.****:*.:&.*# **:*.:*:**.:*o. **::&:.: *o:oo:o..::o::*::*:::*. .::::: :::::::&&&&:&&&##&##&&#&&&#& :::: :::: *::. ..... .....
.*.*.*.*. *.:*:*:. .*:*..*:o::&#. .*:::*:*..:*:. *..##&#. o:..::::o::*..::::.::::::*. : :::::&&&&&::&&#&#&#&##&&&&&#. .:&:. ::::. .::: .: . . .. . .....:*.
*..**..***.*:*.. .*::*:*...:.:#&&&o.:*:. .::*:*:*: ::&&&#. :::o:o* .:::::o:. ::::::::*::: ::::&&&&&&&&&##&#&##&#&&&&###8 *#&. &&:: ::* . :. .. . ..
* ***. .***..*:.:***. .*:*::*:&:..#...*::o*:* .*::. *.#&::: .oo:o:::o:oo&oo&o&ooooo:oo:o::: .:.:::&&:&:&&&&&&##&&&&&&&#&&&#: .&&:.::&:&. .:: ...**. .: . . ......
*.:.**:*.:. .:.*..:*:*:*:. .:*&&:&&:.:::*..::o:o:o. *:&:::& . . &o&o&&o&&&:**&o&:o::* .&:::::: ::::::&&&&&&&&#&&&&&&&&#&#&&#&## &&&: &:&* :: :: ..:.. .* . ..... ....
.....:. .*.::**...****.:*::::*:&#&##*:::* .o:o::o:o.. .....*o##&&&........*. o&o&:::o. ::o:o::. .:::::.. :::.:::::&#&&&&&&&&&&&&&&#&&&&&&8& :&&& :&:* .:. ::: :. . ... .. . ... .*:
....*.*..*:.. .:*:**:* .::*::::&&&##:oo:&o&:&:o:o&&........*:#&&#:. ........ .:::o&::..::::::::.o****... &:::::::o:&:#&&&&&#&&&&&&&&&&&&&&# .&&&. .8::. .:. : .:: :&.. *...... .... .
*..*:.**:*:..::::::::o&o:o:o:o::#&:&&.:&::&::o .:o:....... .:8#&#&.........**::::::::::::::::::::***:**. ::.::::::&:&&&&&#&&&&&&&&:&:&&&&&&&& *&::. :::: :: .::. ...:#8&. *.*:....... .
..*. ::::::::::::*:*:o::::&. ::::#:::& *:::::o*. *::. . .:#&&&:......... ::::::::::*::.:.:.*****..... ::::::::&&:&:&:&:&&:&:::::::&:&&:&:&&: .&&&...&:: .: ...*::: *#&#..:###&.:& .....
....:::...:::::::* .::::::: . :::#&&&#.*:::::*::*:::. *&&&&:.:.........:::.*.:.::**: :::*:*:. *. &:.....:::::::&:&::&&&:::&:&:&&&&&&:&& :&::. :::: .. .::..:.:* . *. . .#&.:&#8#...&....
..* .*:. :.:*::*...:::::.:*.::*.:#&&&:::::::::*:*::. .:&&&&:.*....... ..:: .:*:*:*. .:**:.:. *..::..*...:.::.::::::::&&&&&::&&&&&#&:&&* ..* .:&: .&:: . ::&...:::. : ..:oo. *.:###&..:& ..*.
*.. ...* ***.*.*.:.**.:*.:*.**.:*:&&&#:.:..:..:.*:.: *&&##&.:. . .. . .:*. .:**:.::.*:*:**:*:..*.:*............:::.:::&&&&#&&:::*. &&....* :o&. .::. . :::*..&::: ... o&&: .:#8#:.:&:...*..
... .......*.:.....*.****.*.**.:.*&&:&&.:: :.:.*.:* .&&&&:.*. .. . ..::*:*:***:*:*:*:*:.:**.*.:.........:*.*....*::&&::*. ........&& .... ..&&:... . ... .::: .:8:: . : .: .:&o.. :&#&. &::... ..
... *........*..*..**....*.:..**. :&::&... .***:**.. :&&&&:..: . ..*:*:***.:*:*:*:.****.*................:...... . ............#*. .... .:&:. . .: .:::...:. . :. .8 .:8*.* :&o.:&&..*.*:*
.. ......... ...*...* *..**.* o::*&..:*.:.:.*.:*. ..&&&8& :.*:.... ....*:*:*..**:*:*:* ::**.*.. ... ....*.... ..................:& .....* ::.#* ..*. .:::. . .* .: :&: ..&..* .:..#::.. . .
.. ....... ...*.... *.*.****:.:#::&*.*.*.*.**:.:. :&##&: .:.:.*.:* *:.*:*:.. *:*:*:*...:.:*:*.......::. ......................&: . ... .*.* .. .::: .::::. . ....: .8&&. .*:..&: .:::..*.*...
.. .........* .......:....**.**.**:&&&&:..:**:..*.**. .&&:#o .:*:*.... **:***:..o:*:*:*:o*:*:**.... o&o&o .......................*& .... ...*.*.:.*.:. ***. .:: .#::..:8::. . *..:. &&o&. .:..*&o . ..&::
.. .......................*.:.*.**.&&&&: *.**.:. .**. ::&:&: .*.:**:*:.*:.*.:*::*:*:*:*::*o*.. .... o&:&.................... ...o. . . .....*...*... &:* ::&* ::&& .:&::. o...: :8&#... *...:#8..... .::&..
. .*.................:.**** *.*.*:&&&& .**.:. .*:. *&::&:.:.****.:.:..****:*::::***... .......... o&:&o . . ..................* .. . .. * :.*.*.* . ::o ..: .:8&:. o&8..:#:&.* .:*... :8&8:..# .:8&8.**.... &::.
. ..... ........ *...... .:.**:&::&....:.*:..:*. .&:&:: .:..***:*.:.:...::*.. . .................:*:& ....................: . .. . .. **.:... . :&. ::&#:..&&8. .#8& &:&:&. ::... &&8#. :8: .8&&:.....*...::*. ....
. ..... ........ ...:..*......*.::..:..*..:*.:***. .:&:&: *.** :*****.. .................. .. . &::&... ............ ....:. .... . ....o#&:&* . :*. *.#8&o .&8#::.&&&. :&&&&& **.. .#8#:..#&&. . .#:& ......*...: .........
. *.... ........*...*....*..*.:.**::..&. .*.*.:.:.: *&:::& ***. *:*:. ................. . . .. ... *&:&: ... .......... ...:* . . . .. ..*...**. . :. .:.:&&##..:#8: :8&&..&&&&:: ...... :&&&..:8&8* 8&: .o....*...*. ....::
.................*....*.**......:&:::. . .::.:.** .::::: **.:.*:*:. *............ . .. ...... ... . . o&&& ...... ..... ....oo ... .. . .. ....... . . 8..*#&8:.&8&&. *8&8..:8&&&&. ...*.*:.&&#:..#&#&.. . .::.&&.....*........*::..
.*.................. :...**** *::&:: *.**.:.. *::*:o .*..:**: ..... . . . ..... .. . . .. ......*&&#.. .. ..... ... .:o.. . .. . . ......... .. :.#8#&. :&&: .&&#:..##&&&&. ..***..::&:o..:#&8*.* ...#&... .. ... .**:o&
.. *....... ..*..... .**...*. ..:.... *.*.******* ::::&:.*.:.:.: ... . .. ... . . . .. ... .. . . ..&:#....... .... . :o*. .. .. .. .. ..*... . .&#&#. *#8&..&8#&. :#8#&8&& ..*****...:&&:..##8&. :o .#&&:. :.:::*....
....... .......*..:..:.:.*..*.***.::..:.*****. .::::& **.:.: .. ... . . . ..... ... ... ...... o:&& . . .. . .. oo:. . . . . .. .......... :#8: .#8#&.*&#&: .#8#:#&&&&.**... .o:..&#&#*..#& .#&#: ...::&:&:&
............*........:.......*:.**.:*..:.*.:...:*. *::::: .:.:*: ... . . . .. . . ..... .. .. . ...*:&& ....... . .&::*... . ... .. . . *... 8& &##: .##8#..&##:.&&&&&o **..&8#&..&# &#8. *:::o:
................*..*...:...**..:.*. ::...*.. :** *::::&..:.**.:. . .... . ... .. ... .. ... .. .... ..o:#. . .. . .:o.:. . .. . ..*:o&&.. .. . :##&..:###:..###..&#&&&#: ....... ..:::#*..#: #:.. ......
*....... ...... ...*.*..* *.**.** ..:...*. ..:. *::.*:.:.*:.:.* ..... .. .. .. .... .. ... .. . . .::#..... . .o&:*.*:&o:. ...::o:&:&:&#:. :# .*##&:.*#&#&..##8::&#8&&&&8*....... *&:&:.:#8 . *:o: . ...
..... ........ .*...*. .**.**.*.***.***.:.*:.: *::..:.**..*..* ... .. .... ... . ... .. .. . ....::&: . . .o::o:::&:&&&#::&&::&::o:&::&# . ::#& &##&..:###*.:###..&#&&&&&&#. .&:...#. . . ..&. . .. . .::::*
*.... ....... ...*....:.***.:..:..*.*.**..*.:.:. *.::::.*.:* *: ........ . .. . ... . .. .. ... .. .#:o.. .:8:.:&:&&:::#8#&:::::::&::&:&:&. :.###. ##: :###:..###:.:##&&8&&&&&. .:..#* .:: #:* ... .. . .::&:
. ....... .......*......**...*.*..:..:.***.*.*.*.*.*. ..::::..*.* ... .... . .... ... . .... . .. . . .. o&& .&&:*::::&&&&:#::.:::::::&:&&o:*:. .&:::. .#..8###.:&#8#..&#8#&#&&#8&&* .:. . .::& . . .... .. ::#:
. *.................*....**.*.*.. **..:.*. *.:.*.* .**:::.**.*..*.* ... ... . .. ... . . ..... ..... ...#o..o:*....::&&&&:....::&::&:*... ... .::::..# ..:###:.####:.:###:&&&8&...&:. *:#.. . . ..
............. .......* :...*.*. ..:..*.. ..:..**.: *...*..*..**.*** ... . .... . . .. .. . . . . ..**:o..*.......o*&&:..:::::*:::& .. ....* .::&: .:#: .###&..&#8#..&##:..&##.. &&::. ::8&.:. .. .
* .. ........ ........ ..*...*. ....***.:.**..*.*:* .:.:.*.*.*..*... ....... . ............ ..*oo::o&&&&:. :oo***.*.*:::o*. ..:&* .... . .:. .::& ..8##. .#8#..:###*.:###..&##:..:#8&: .:. .:*: *:8#:. .. .
... ...... ..............*....*.:.*.**.**.*.*..*. .:.:.* *.*..**. . .... . ..... .. . ..*:&o::&::o*::::: ..::::::..... .. . .&*. .. .. .... ..:&. &8&&. :#:..###& .###:.:###...###8 .:::: :.*. &8&. . .. .
. ......................*...*.***.*...*.**.:.**. .*.: .:..:. .*.*..*.. ........ . . ..*o&&&&&::.::o::::o.*: * .:... * ...... .** . . .. .. .. .::. :###... o. ####:&:###..###:.*#8##...:::: *.: .&. . .
...................................**. ****.** *.: .:.:.:*.::.***.*: ... . .*:o&&&&&&::&:&:*:&:o::o.*:o ** .*. . . ..*:. . . .. .....: .::.:*#8#:..&* .:###:..###:.:#8#...###:.*#&&&: ::#*. .
.. ..... ......... ........ .:.:.**. ..:.**.* *..* .**.:.*:.:.::**.: . ...:o:&&::&&&::&:**:&:&::o:::::.:&o .o ..::::*.:o . . . .. . . . ..:. .###&. :#& *###:..###&..&##:.:#8#& ..#8#&..&#&. .
.. ..... ........ .....*.. .*...**.*.:.:.:..:.**.** .*:..:.*:.:...***. *&&:&:&:&:&&&:*::&**::&oo**.. .*::&o *&.. ..:*.*. . . . . . .. .........:##8...#&#: :##..:###:.:###..&#8#...&&##..:: *.
.. ..... ...........*.*..*.*.****..*:.******..**... .. *:.::*** .**.* .&:&&:::&&&&&&:&.::&::o:. ....::::#* *::.* . . . . . . . .. ........ :&#8: .&##:.. #:..###&..###:.:###:..&###&.. :...# .
. ...... .................*..*..*..:..:.:...***.**.. . ::.**.*. .*.***&:.&#:*:o&.:::&::*&::. . ........:&:#* .&o *... . .. .. . . . . . . .. ..... .&:##..:#&#.:8. *.&###*.:###..:##&...###: *#&...: :
. *.. ........ ............**.** :..:..*. ...*..* .*.:*.:.*:.*.*:.:.:&..&&::&&::.:&&:o:. .... .....o.&&&#. .&& ..:o&o.. . . . . . ... . . ..... * :&#8..*###. .#8 .&###:..#8#:..###...#8. ##&...&* .* .
.. .. ....... .......* *....... .**.*.*. ..**.*..* .:*:.*.:.*****..:..&o*&&:.. .::&o:. .. . .. ...: o:&&# .:& :. :::&:* . . . . .. . . . .. . .... &:#:..#8#&..:##o &#8#..:###.*:##&.:. .*..&8#:.&:&: ..
.. ...... . ....... .*....**..*...*.**...**.:..: ..*.:...*******: **&o. .... o:. ..... ... .....o &&&&o ..:&*o&.:&:::&... . . . . . ... ..... . ::&#..&##&..*###& ##..*###:..###* .o##:.*#8&...&:* ...
.. . . ..... .......*.....*........**..*.*.:..*.*.* *:..: .:**.**. .:* .. ... .. . ...... ....*..:#&#*. :#:&:&::&:&:: .. . .. . . .. .. . . .* .*&8*.*###.. 8##&.& .&..&###..:#: .####&.:#8#:..:::. . .
. ...... ....................*...:.....:..*.... .:.* ...:.. .****.*:..** .... ..... ... . .....& :&&#8 . :&::&::&::::&* .. . . . .. . .... ... :.&:.:&##:.:&###...& ..&###:.&: .*.:####..:##&...::: .. .
* ... ..... .............. *.*..... .***.:.. .** :*..:*.:**.:.*..*.. .. .. . .. . .... ....*& ::&&& . ::&:::o:::&::: .. .. ... ..... . .. *...&*.:##8.::###:..&#* :###&. .8&..#&##:..#8#:..:::
. ..... ........ *....... ..*.*.*. ..*..*.**.:.** .*.:..:..***.*..*..* .... .. . ... .. .....#: :&&&: . *::&:&o:&:&:&: . ... ... .. ...... .... .&&&###:..&##&..&### .##: :8##.*&###&. &###:..o:.
* .. . .. ... .........*...*..*.**.***.*.*..*:. .*.**.*..: .:.*... .. . . . . .. . . ...*#*.&:&&.. .:&:&::o:&::::o: ..... .. .... . ... . ...:###&&####:*.###&. . .**:###:..####:..#&#:..::*
. ... . .. ...................*..*..*..**..*.:.**.. .. .**.*.*. ......* .... .. .. ... . ... &#..:&&& . . &:.o&::::&o&:&o.. ........ ... . . ::&&..:###&#####... *#:..###&..:##8&..&##&...&: .
. ...... . .... ...........*......*.*.**.. :.....*. ....**.*.......... ... . . .. . . ....:#.::&&: . :: ::&:&::&::::& .. .. . . . . .::: ..&8&..&##&&&#.... *###..&8##:..####:..###:..::. .
. ... ........ ........ .......* *.....:. ..*.*... *............*...... .... .. . .. . ... oo#.*:&# .. :::.:oo:&o:&. &::. . . : .::. .:::..:&:: .#8 ..***... .&###.*.###&*.&#8#:.:##8&..::: .
. ... . .. ........ ........ ........ ........: *.....*........ ..* .. . . . . . . ... &&#**:&o . .:..*:&o::&:&.:::. .. :::. .:::. :& .*.* .&::###&..&###:..####..&&##:..::.
. . . . . ...... ............................. ....* ........ ... ... . . .. . .....#&#*.:#. .. .:.:..&:o:oo:&:&:. . :::... o#&&####:::###&.*##8#:..###&..::: . . ... .
. . ... .. .................................. ... ..... ..... *... .. . . . .. .#&8:*o: . .. :..: :::&::&:o&:*. .. ...... .& .:8&#:&####&:.####..:###:..::: . .. . .
. . . . ...... .. .. .. . . . . ....... ... .... . . .. .....#&&&** . . :. . &::&:.:::&:& .... ......: ....... ..*::: .:#8#:&####*.*###&.*::: . . .. ....... .
. . . . ... . . ... ... . . . . *. . . . . . .. o.. .. . o&::. &&:o:: ........ ......* *.**.......... . ::: .::::..:8&8::####&*.o:: . . ....... ..
. ................. .... . .. . .. . :::&::::&.:&: .......*......*.*....:*. .*.*............ .::. .::: ..:&::.:&&&::.:: .. . ............

And a pig!

string_agg
.##*
##&#
&&o#
:&
&#
###&
#&&#
.&&:
.
:##:
#&#&
&&o#
::
##
##&#
#&&#
&&.
*.
:##&
#&&&
o&&&
**
##
###&
#&o#
&&
*:
&##&
#&&&
:&&&
..
##.
##&#
#&&#
&&
::
###&
#&o#
*#&&
.
.##.
##&#
&&o#
&&
&#
##&#
#&o#
.#&:
:##:
##&#
o&o#
:o
##
###&
##o#
&&.
..
&##:
#&&#
o&&#
*:
##
####
#&:#
&&*
..
:##:
#o&#
o#o#
*:
##
##&#
#&o#
&&.
*.
&##&
#&&&
:&&&
..
##.
##&#
#&&#
#o
:o
###&
#&o#
:&&&
.
.##.
##&#
&&o#
o:
.
*o&&&#&#o:
.&&##&&o&##&#:
o&##. . *o#&#
:##* .. .#&#.
&&&* . ..o#&
:o#** *.o##
:#:: :*&&*
:&#: *:#&
:#:* .*&#.
.o#: *o#:
::&: *:#o
:o#* .*&&
*o&* :&o
*o&* .*#:
*:#: .*#*
*.&o *oo:
*&&. *&:.
**&o *oo:
..&&o .*#**
**o&* .*&o.
.*&&o *:&&.
.*...**o*o:oo:o:*****..
.:o***:*::o::&oo&o&o&&&&o::::*:*
***.****:*::o&o&&o&&&&o&&o&o:**.*o
**.**:.*:*ooo&&&&:&o&&&o&:&*o:***:
:***.o***:::&:&&::&&&&o&&&::&:*:**
*.*.*o.**::o&&o&&*#o&&&&&o&*o:***:.
*****o***::oo&&&::&&&&o&&&::::*:*.*
.*:...&.*:::o&&&&&:&&&&&o&oo::o*:**:
*..*:.&*:**::oo&&:*&&&&&&&&o:oo:.*::
:**.:.&..:::o&&&&&:&&&&&&&:::::::*.o
*.:*.*o.***:ooo&&:*&&o&&&o&:*oo***::
:..:..&.**:::&o&#:*&&&&&&&::::::*:.*
.****.&*.*.:oo&o&:.&&&&&&o::*:o****:
:..:.*..***:::oo&..oo&o&o::*:.::**.:
.**.** .****:o&&.. .o&&o&:o:. :**.*:
..**. .....* ... ****... :***.
. ........ .. . ...
. : . .................
.. 8 ...........*........o:..
...: o ....................&**.
.:..:*: . . . . .
. o : o .* :&..:. .. . .. ... .. . ...
:*. o :.8 : ::.o*:.#. .... .... .. . . :*:. .*
.o:* .:: :.&..o .*:o . ...... ..:..... . .o. .*.
. .8**... o&::* ..o#&.. ........#:.&# .. ..... . .. . ..
*. . ..o&: o::&:*:.:#........ ..o:**...... .... . . .. .
.. .::.***. :o*o:::..................&&&o#8...... ... .. ... .
**.. . .o8:*. &o.o&....:&&...........##&.&:*....... ..... .... . . .
*. . .&:. & .::* #:..:ooo&#.............:&.#8............. .. ... . . *
. .. . . .*.**# & o. .#.# # .o...............&#..::................ . .. . ..o:
. .* .. . .#*8.:.8.8..:.& #.##..&#................................. ...... .. .
:* . . ...8* &.&:&.:. o#.o*.:.............................. ....... ... . .. ..
*. o:. . . .. ... ..&&8&.8#.:.##.:#&#.................................... ... .... * .
:.. . . . . ........ &.*.*o:#*....... .........oo&#*............................ ....8o. .
.. . .. . . .. ...*#.o&.*..................& # ##:............................ ....8: *:
. . .. ..:.... ......:..8....................#o&...o..##o#......................... ...*#. :
. . .. .. *...... ......##.....................o&....&..&.::#.......................... . .o* :*:o
.. . ..*..... ..... ............8.....*....:#:..o:..&.#.....#...................... ....*:&o:o:::o.
. . ....: ...*..*................:#........*o#.oo.*o..#.&:...o#.........................:#&ooo&&.o .
. .. .. ..*...:.................##.&#........:&.o..#..#.o.#..*.*#..........................*#o:..8..: .#
. . . *.*..::*..................&&:&.*##o....:&.....#.o:&#.oo##.*................................8..# . .*
. . . ....::.::...*...............:###o&*.....o.o....*:o...&*.....:............................ ..&& .. #
. . . . .**:.:..:.::**............*##:#&:.........#.*.#o#&*...:#:#&&:#&...........o...................# ...*&&*
.. . .. .. .....**.&:...:.............#&*.o*o#o.......o#.:....#...:.:....*#...........o#....&.............#.. .**.&*
.o:o . . .. ....*:o....*.................&#..##...........o&##.#oo#*#......#.....**......*..&.................. . &.& .
.::*:8 . .. . .....*...o....*................##............o&&.:..#.&.#.:&:...#&......oo...&&.o.*................. ...&& & :
:*:*:o . . . ...*.......*.o.:.................&.............#o.....#.&o*#..&*.o*:*......*:*:.o..#&............... ..&#&&&: . .
.o::o*:..# . . . . ...:....:*..................................:o:...o&.#...#..::#:o.........:#..o.:..*................#8:.:o& .
o.::.*.o:. . . .. .. ..........................................8*o.*.o*:*o...&..#...............*oo..&.::#..................#.#. .
.: 88 .8 . . ... .......:........................................&#.#.:..o.....#*.............&:*.&.&o:&.:..*............ .. #*.# #
.o **o &** . . .. . .. .................*................. ......::o###.*&...oo#...........:o&.&*:*#::..*...... ........... .*8 .#. ..
*: o&.o . . . :&. :................:..&.................................###.#.o:...........*.*.#..&.**o&o.....................#.. #. o
.o.&:8* . . . ..#. &# . ..... .........:....................................oo#&#....*............*&o.o..:*.....................#&.. .
.ooo..8.. . . .:.#8:o&.. .......*...&.*..o*......*..........................&.o....................&.:#.o.#*....................:. ..
. :o:&88: . ..*#**8&&8#:..........*::.:.8.......................... ..............................::*.&.o....&...................... ...
8&::8 ** . *. & o#:&&8&#... .........o:......................... *: .............................*o....:....................... .... .. .
8. &8 ... .*# #.#..#&o#&&...........#.*..&:..........*......... &&8: .............................#....*o.......................... ... .
. * .o&.8: .&o*.** o#&8#&&:.........*..&.8...*................. &&o. .. ..............*...................:*............:.............. ...
* *.&o&&8. .:.&8.##:#&8&&&#8..............*..................... &8oo& .......................................*............*..*........ .... . ..
* .. :::o: . .&: #8&&&&8###8:... .........&...:................. .&oo&o& ... .........*....................................o.o*............ . . . .
. * &&&& . &* .&&&&8#&...#&.............*.................... &o&:&oo& ....................................................***............... .. .
* *:. 8:o.8. .#&*#o&&8o .##8&# ......................... ...... &oo:o:o:& ....... ...........................................*:*o*.*.............. ..
*: 8 ..:8.. .# 8&&&&&8# ##.:#8&......................... ...... #oo:ooo:&o& .............. ...................... .............*.*:........... .. . .
**o : . .&: #&&&o&#&#*#&*.&............................ ...... #o&*oo&o&ooo# *.......*........ . ................. ........ ...o.:.o.............. . .
*:***:. .&&:: ..&o&#&..o .:8#8 .... . ................... ... .. &o:o.o&ooo&o:o# ....... o8&&88 ........... ...............*.*.............. ... . . .
****:*. :.&:&8.#&o&8# &# ..:.##*. .......................... .. &oo& 8o:&:o&o&& &:&&88&: .*. #oooo&o&&& ...... ............................... ... . . .
**.:*&.&8.#:&.8:#&. &##o8#. . ......*........................ . .o&o# #&&o&oo&. &. .&oo&o&8. . #ooo&o&oooo&&. .............. ....................... .... ..
.* 8*:::::&:&:8&:.# #o .#.:..... ....:.*..................... .. 8oo: .&:& ::&: .&o&o# &&: &oo&&o&:&&o&:o&&&: .............................. ... .
. o:&ooo:&&& & #&. #.. . .. ...*.*..**.................... .. &:&o: &ooo # &.o :##.&.. & &..&o&o:o&ooo&oo&o&oo&8&8&&&8# .............................. . . .
8..: . :::&&:.# . . . *. .... ... ...*o........................ :oo&: ooo:oo :&o ####..###.:o&:o&o&oo&o&o& *.:o&o:oo&oo#:& ................................. . . .
: . : &::&&:&:#& . .. ... ... ....:.: o....................... &&o: oo::ooo. & ## .# ####.&o&o&:&o&oo&:: .:::. :.&:&8: .............................. . .. . .
. .&:&:&o&&&.. . .*. . ...... ...*.:..................... .. #o &:oooo:oo: :# & #####:o:o&o&o&o&:&& #ooooo&&oo& ...............*...................... . .
:::&:&&&&& ## .:: *. . . . ................................ . &.ooooo:oooo& : :## #:&o&:o&o:&oo&..o&&:&&&o&o& ... ..............*..**.............. . . .
.: &::&:# &&&:**.###8.......... ..:..*........................ :oooo:*ooooo:oooo ## : :.&o&o&o&&o&: &o:o&o:oo&& .... ............&o.**.:.*.........*.*.. ... :
& o.&:.&::&*.o&&&&8* .&##. . . ................................ &:oo& oo *oo:ooo :.: &:o&o&o&o:&o# #:o&o&o&oo*...................**.&.*.*..........*.*. . . . :.
.:o.::8.8.8:&#*8:oo&&. &#* . .#.. . ...................... .... &:o:o. &*& oooo:o.. &. oo&:o&o&:&& # &o&o:o&:&& ..................o..**.**:...........****.. . .
.. .:o: &:.::: .8o&:8. # ##:##.. ..... ....................... o:o:oo o.:o* oo:ooo:.# .#.&&o&:&o&ooo& &o&o&o&o&o& ...................*:.*o..**o..........*..... . .
o .o:.. &&& #8:o&8o##o#:..#o::.......... ............... . o:o::o:&o:.o& &oo:ooo.&o& #oo&oo&:&o&&& &#oo&:&:oo:..................:o.***::..o*............* . . . . *
...o:*8 &&. .# .#:ooo&oo.& & .... . ....................... .. &*oo:oo:ooo:oo*o:ooooo.o&o8 &:&:o&:&oo8 # &:&oo&o& ...................&::*o:oo:..:o.......*...... . &
.**o* . ..&o.* *#.&ooo&# &8# #: ....... ..................... . oo::o:oo:oooo:ooo:o:::.&o&o8::&o&o&o&o&:.o&# &o&:&:& ...................*..o***&o....o.......... . . . :.:
*:* 8 .&:o*8& . #:. 8o&&o# .8&:o#.. .. .......................... :ooo:ooo:o:ooo:oooo& &&oo&o&oo:&&o&o&: &&oo&o:&oo&:.................*.&...*::..*o...:......... ...... & .
.** .. ::o: ..*8 :&oo&&o#o:oo&:. ............................... .. .oooo::ooo:oo :oo&:&o&:# oo&:&o:&o&o:&oo&:o& ....................:*:.o&.o:*.:............. . . .
.**: . *:o:o* & 8&:*8#oo&oooo:o ..... ... ..:.................... . *&. ooooo: oo&o&o&o&o& *&oo&o&&o:&&o&oo&&& ...................::..**o::o:&.o......... .*... . . .
*..**. 8:::oo8* *.o8 & .#&o:&::o*. . ..... ..#.................... :&&# oo&&:o& ..8&&oo&o&o&& .8:&:&o&:&o&:o&o&oo# ........... ......*.....::**:...:*.........:&:. . . . .
*. : .*..oo* o:. .:* 8:*.8o&oo::& ... ..:*..:#:................... &ooo& &:oooo&o#o8o&oo:&o&:o# :&oo&o:oo&:&o&:&oo&:....... ..........*...*o:. **o.**......... .#..8* .. ..
.... : .:8 8 . . 8..8:8..&::::8.... . ..##.*&....................*o:oooo #oo:ooo&o&o&o&o&:o&& 8&o&o&8 #oo&o&o&:&& .. ....................:o..:.o*..:..........**..o.. . ..
.. :. 8o* :.& . . .# 8#.8:::o.. . .....#8.o*.#8#............. . :o:oo:&. &:&:&o:oo&:o&:&o#o &o:&:&o& :#8&o&&&: ... ......................*...*.:..........:.*. .. 8:.. .
. * 8:&:*o8:. . . .*oo.8.o::#*. ... .. ...8#::#&.............. . ::o:&o:o #oo:o&o:&o&o&&: :&o&o&o:&& ... ..........*.................o..*&o.o:......::&:.o..&...* .
*. o .:o:o :o . . :.8:o*o8.. .. .....**o&:o................. .. &:ooo:&o: .&#&&&&&&&:: .. &&o&o&o&o# o8 *................................:o ...........*** o.*.* ...:
* ::o*oo: . . .o: o::* . .... .. &##.&:&................. .. &o::oo:o8 ........ #oo&o&oo&o ## *...... .....................................*# ..:.:&.. *
o oo 8 . . . *: .8... ... ... **:..o.*#8............ . .. :oo&ooo:# ....... ..........&o:&o:o&&o& ##o ..... ........................*..............#.......*... *.
: &**:..:. . . . .. . ... ... .. ...#8..#8............. ..... 8:oo:ooo ...... .......... 8o&o&&oo&o# *### ..............................................8*...:&.:.*:*. .
:*. o:.o o* . . . . .. . ... ........8&................. .... &o:&oo# . .............. 8o&o&oo&o&o: o###...............................................o.**::. .:. .
.* :.:o 8 *o* . . . . . . .*.. . . . ...#. ... .................. 8 &ooo:o:. ............. #o&o&:&:&o&8 #### ..............................................o.:..o..*.:*:.
. *.o:*.*: * . . . . . .... ....... ........................... # &:o&:o: . ....... . &&:&o&o&o&:8 #### .: .... . ..........................................::..*. . *
*****::.* . . .. . .....: .. .. ..... ... ................... : *# #o:o:o& ....... o&o&oo&o&o&&& *#### :&o: ... ........................................oo:..o**:. ..
o:.**.**. . . . . *...:. ... .. ......... ............... :&o8 &* &:o&oo&& .:&&o&:oo&o:&8: ##### :ooo&o& ..........................................o ..*..* :. :
...*.**o . . . ....... . .. .. .. ........... ....... &o:o:& &*:oo:o:oo::&&&o&oo&oo&:&&# *###.## :oo&&ooo&& .........................................::..:.o.*.. ..
:*:. . . . . . ..* . :. .. ........... ....... ... .. &o:o:oo: & o:oo&:ooooo:oo:o&o:&o& .##:.o&* &o&o:o&o:oo# ..................................... .o. .*.: .. *:
. . . . .:... .. .. . . .. .... .. ..... .. #:oo:o::oo # 8:ooo:o&oo:&o&o&oo&8 ##*.##8 #o&:o:o&o&o:o&* .......................................#..*.:. * : .
. . . . . .. . . . ...................... ... ooo:oo:oo:oo # :&:o&:oo:o&o:o:&& ##o.##o 8&oo:oo&oo:o:&ooo* .*................................. ..**:: . . : .
. . . . ...*. ... . . .. . ............ .. &::o:o:o:oo:o& # #o:o:oo:oo:8&. *##.#:*. :oooo&oo&o:&oo&:o&o&. ..........................................:: *o .
:.. . . . . .. . . ... .... .. ......... 8o:oo:o:oo:o:o:& o# &&&&&&&#: ### *:* .&o&&:oo&o:&o:o:oo:oo:8 ..........................#........ ..... o.:. .
: 8. . . .. . . ..... . ... ... ...... ...... :o:o::o:o::o:o:oo& #. o ##: :&oo&oo:&:oooo:&o:&o&ooo:& ... .................#...&*......... .... .:.. . .
. :: * . . . . . . #*:o . .......... ....... . o:o*o:oo:oo:o:o:o:8. ##########.# .&&oo&ooo:ooo:&:&o:o&o:o::ooo. ... ................:o*::.... ...... . .. . . . : .
. ..**.#: . . . . . .:.:** .. .. .. .... ... .. oo:o:o:o:o:o:oo:oo:o& ##########. o&ooo:oo:&o&oo&oo:oo&oo:&oo&:o&o& ....................o..*....... ..... . . . . ..
. . . ..*.. . . .#* . *:.*.. . .. ...... . . .. #:o:o:o*o:o:o:o:o::o:oo ##8&#8&### *#oo:o&oo&ooo:oo&o:&o:ooo:oo:oo:::o# ..*.................*oo*o&*..... . ..... . . ..
.. .o.** . . ..*... 8*... . .. .. .. . ... .. &:o:::o:o:o:o:oo:ooo:o:& *#&#8#:.# o&o::oo:o:o:&oo&& o&oo:o:&oo&oo:ooooo: .................&.o.:.o... ....... . . . :.
. *. . o. . .8.*.o ....:.. . .. ............ .o:::o:o*o:o:o:o:o::o:oo& 8#&o# # &o:oooo&o:&o:o:oo .o:oo&:oo:oo:o&o&::o& ................&::.:**......... .. ... . ... . .
. . . .: :. . . ......* .&*. ..:# . . .. .. ... oo::o:::o:o:o:o:o:oooo:o:o& ##** o:o:o:o&oo&&&&&&# #oo:oo:&o:oo::o:o:o:o:.. .. ...........*..*.o& .... ..... .. . . . .
: . . o..:.:.. .# **.. ... . .... .o::o:*o:o:oo*o:oooo::o:o:o:o# #&&o&&&&*. :&&:oo:o:o:&o:o:ooo& ..................oo.o:... ... . .. .. . .
. : . . . . *:.:.* *:.:.** ... ...... .&*oo::::o:o.:o::: .:oo:ooo:o:o #. o###########. .&&o:&o:o:o:o&:o:. . ................o:...:.... ...... . . . .
. . . *..o**.:.*.& .. ..... &*o:o::o*:o:.oo:oo&o::o:o::o:o.o&8o *#o&###&#oo##&##&#######* &oo&:oo&o:oo:o: . ................o*............ . . . . .
. . . *.o...*:: *:*.:.*.. .... ... &::*:::*o:oo &::oo ...oo:oo:oo 8##### *o&o:o###o#8#o#o#o&#8###o &: :oo:o:oo:& .. ..................... .. .. .. ... . . .
. *: * :.*.*... .. .. . .. . . ::*o:o:o::o.. oo:o:o* ::&o:o #&##&###&##o#o####&#8&##o#&##### ..o:o:oo:o& .................... ....... .. ... .. . .
. . . ..*.*.:..::**. . . .. . ... oo:*:*o::::& o&. ...... :&: *#o&8##o8####&:oo:*###o##&8#o#### #o:o:oo::o:: ........................ ...... .. . . .
*. . . *. .. *:..*: & .. ... . .. o*::*o*::*o::..* ............. .o ##::o##o*. .:*####o#&#&8&# o:o:o:oooooo ...................... .... . .... .. .
. .* . . o*.....:. *.o..... .. ... o:*o::::*o:*o: ................. : ....... *.#8&##8#o&#8# o:o:o:::. : ................. ...... ..... . . ..8: ...
. . .. * . . . ....* .: :*. ... . . . o:::*o:*o.o& ...................... :: .......... .. .8&8#o#&#&###.oo.*:ooo.: ................. . .. ..... ... . . o:8o: .
. . ** . ..*. ...*.& . . . .. :*:*o:.o oo ......................... .......... .. . :##&#&8#8&&8# & &:o:o& ........................ ...... ... :.:.: .o::
.. . . . . . . ..o.* . :... . .... . &.o:::..&. ........................................ .... .#&8&8&&#8#&# :.oo:o:& ......................... . . . . .8.**: : .
. . . .. .: .:... . ..o. . . . ... *:*:*:o: ............................................. *#&##o#88&##&o o:o:o:& ............. ... . .. ........ .. *:..::*.:
. . . .* . . ****. . . . . .. .. ::.o*o:o ............................................... :##o#o#&#o#o## &o:o:oo . ......................:: ... . .. ..*:.::.:..
. . .....: . * . .. . . . .. . & o:**o................................................ .8##&#8&#o#&#o# o:o:o:o .................. ... &o#..8. .. . * # *. *.o
. . . . . . . . . .. . ... o:*o:o ............................................... *#o8&#8&8&##&8# :o*o:o& ............. .. ......:::o #.. .. . . .:o..:
..*.. ...*.* . . . . . . . . . .. . o:*:**: ............................................... ###&8&#o#&8&8&#*.o:o:o: ............ . ..... .:::*#.: ... . :. .:o: .
. . : . .. . . . . . . . . o*:::o ................................................ #&&o#o#&#o#&8&#o o:o:o ........... ........ .*o&.# :#. .. ..# .: o
.. * .* : . . . . . . . . .. o:*:*:o ............................................... o#8#&8&#o#&8o#&8# oo::& ............... ... &.8.8.*# .. * .. .o#o. o :
. . .. : * . . . . .. . . .. o:**o*o ............................................ .. ##o&#&#o#o#&8&#&# o:o& ..... ... ... ..... . #..oo &8. .o. . :..::::8
. . * . * . . . . .. . . . . .*o**:*o ................................................#o#8&8&#&8&#&8o## &oo ................ ....#:..&:# o .:: . . :o8 o ..
. . . .... . . . . . . .. o**::*o:* ........................................ .. . #o#o#&#o8&#o#o#&8# o .. ..... .. .. .. .::::#&o.o& .8o ...:. : : ..
. . . * .. . . . . . . . . :**:***:& ........................................ . .. ##&#o8&#o#o#o#&8&# &: ..... .............#::. o:.::&::o*.& .:88o 8 . .
.*. . . . . . . . . .::**::**& ....................................... . ...#o8o#&8&#o#&8&8&8# oo ... ......... ... :&::#:.#8:&*:&. .: # o:.8& : *
.. .* . . . . . . . o*::*::**: .......... ........................... . *##&8&8&8&8&8&8&8&# . o:. .. .... . ... .. .#:.8&.8& #. .:.. *oo * ... .
* *. . . . . . . .. :**:***:*oo ....... :#&. ... :o. ... . . ... :. . *8&#o#o&#o&8&&#o&8# :oo. .. . . ..... .... .:..#..8 o8.:8 .*88 #. . ....
... . . :o# . . . . . .. o*:*:::***o. ..... #88##....:####* . ####: . ##8## .. #o#o#o#o##o#o8&&&o o:o. .. ... .. .. ..:**8. .8&8#.#.... .:* *# . **.
:* : . . . .. . ::**:**o:*:o ....*8& *8# ..#8..8# . .*8#.. #8#.&*o.. #&&o#&o#&o&8&o#o# . o:o ... . . ...... .....8.#* . #*# o*.8* ..o&. o.: *
.o* # ** *o . . .. . . o:***:*:::o ...:oo .## .#8. ## ....#8 ..##. .:& . o&#8&o#o8&&8&8&# .. o:o ... ..... . ... . .. . .. o8 .#..8oo. .8: .: o* ..
o .: #o ..:....: . . . .. . .o::o*ooo ....#8.#o& *8& . #8. . #8* .:8* o:o .. #o&o#o&#o&#o&#* . *::o ...... ...... .... .. . .#o*. 8o#.8. #ooo :*o *:
. * :. .*: *: . . . . . .. ..... .o8& ## ..&8 ...#8 ..:&# *&# ...*&8#o&8o#oo##* .. &oo ... .... .. . . . .. . :&o8. :..8 .8:88 .*8 .:.:
. . .:..*: o o :. ::8: . . . .. . : .......... 8#& ## . &*& . o:: .. #ooo&* .... 8&o#o#&&o##. ... o:. .... . .. .... .... .. . .8*.*8:8.8: 8.&. :. *
. .. . : o o : o:. o . . . ... :*o .......... 88& ...o: . #8 .. ## ... :o:8 ...... #&o&oo#8# .... &o. ... .. ..... . .. .... . ...:#. 8:.o8 .8 &: o:o . *
. . ..o :..... :o. # . . . . .*o ......... #o: .o# #oo ..*oo ..... ## .... .. #&8&o#& .. . .& .. ... ... . .. .. . . .. . .. . #: : *oo8 .o :..
. .:.*o.. 8 * 8 #: . . . . . .**o ....... #oo#8## #:##o# .. ## ..... #&: .... . .. #o##. .. .. # . .. ...... .. .. . . ... . .. . .8..oo:: ... :
. . o : o 8 *o.o . . . :**. ....... ::o&&: ###o .. #8 ..... ## ...... . .. #* .. .. : ... ... . . .. .. . . .. . . . . . ::8oo*:o:8
.. :8 *o :* 8: 8 .&&. . . . o**o ....... .. .... ...... . ....... . . ... ... ... .. .. .. . . .... . .. . . # . :. 8o.&:oooo&
. ... * 8:. *8 o* .o 8. :::*: o* . .. .*: & ................................... .................... ... . .. . ... . ... . . . . &:. .#..8* : o::o ..: .
...... .* *8. ::.:*o 8 8. o # . . .. o** o ...................................................... ... .. .. . .. . ..... .. 8#& # :.:88 8o8. #.: & .
.. . * . .o 8:.o.**: :. .*.: . . .*** o .................................................... .oooo ... .. . ... . .. *8#:# ..8*..& 8o.:o &: &..*o*. *
. . o..*8. .*. o.. .8o::o8 . . . .. :*** * ....................................................o:*::o: ... . .. . .. .. . .o 8 :88o :o&o8 &o:: o. .
. .. o * *. *::.:*.8&. :8 . . . . *o*:.: ................................................. o*o*o:*:o . . .. .. ... .:&#..: ..8:#. .o8 8 : 8. :o **
. . :. .oo o*..:*.8 .88..8. . . . . :**** .............................................. o*:*o oo*oo .. . . . . .#o:*&#8. 8*oo .#*o8 &o& #.o *..
. . o* : # o :: .8. .:o . . . . . .ooo ........................................... o::*o o& . ... . ... . #:#8. &8. .8*8 &* 8o o :* *
. .. .: 8 8 * o .8* . . . . . ......................................... .o*:*:::ooo. .. . . . . ..8. .##o8 #& :8. &. :: 8 : *
o* 8 *o o* . . . . *: ....................................... .:*:*:****:*:o. .. .. . . . . . 8#8&: .o8:o 8 #:: :8 : oo
& . o * oo8:*:. . . . . . o:*.o .................................... o****:*:**o*::*:: .. . . . . . * # .:::#: :&. .. o:&. . * .
*..::* & 8 & .o... . . . . :*****o* ................................. :oo:*.:::*:****o& . . . . . . # .88. 8# .. . .**. . o .
.8o* :.. ...o : . . . . o.***: oo .............................. .:...:.oo: .. . . . . . 8.#. *#. 8. * &#.:.:o# * *
: .8 : &.o.:.:: . . . :**** .****o ........................... ..... .. . . . . .. .:8..#o *: : o : ..
* :.*.: ..*.. o . . . .**: *o*******: ....................... .. . . ............. . . .. . . . ..:&o. :* : : . .
:* * *8 8: &. & . o..********o. ..................... . . . . . . .. . . . . . . . ...8:o:&* & 88 :. .
. .. . *&& . . . .. .******.******o* .................. .. . . . . .. . . . .. . . . . . :o& : 8o&8 o:8.
.8: . . ***********:* ............... . . . . . . .. . . . . . . .: # . #. 8 : .
:. . o. .* o :. . ............. . .. . . . .. . . . . . . .. . . . :.#
. .o . ... . . ........... . . . .. . . . . . . . . . . 8:. .
. * o . . . ....... . . ......... . . . ... . . . . . . . . . . . & #..o .*
. o *. . . . ........ . . . . . .. . & . . : . . . . . . . o. : .* o
* * .. . . . . . . . ...... .. . . .. . . .. . *.. . . . . &..8 8o *..
. o o . . . . . . . . ..... . . . . . . .... . : . . . . . .* 8 :.o
:*.:**: . . . ... . .. . . . . .* : . . . . 8** **.
. *.:** . . . . . . . . . *:. .&.. ... . ::ooo***
. * . . . . . .. . . ..&... ..* : :. . *.
. .* . .. . . . . . . . . .o. . *: . .
o..: *. . *. .. . . . . . . . *. * *: . . . . &.:*
. ** ** . . . . . . : * . .. ..*.
. * .* . . .. . . . . . . .&.. . .#. . * ..
* .. : o . . . . ... : . . . . . . :. * * * * :.. ..* .*. *.
* * . o .:* * . ....... . :. *8. ... . .# :* **: * .*. . .
.* . .*:****: .... .. . *8: *o . . ... * *... *. ..*.*
.....***..:*. . ... . *:.. .* . . ... : . .*:..*.: :
..**.:..:. . ... ... &o:.88: . . .. . . &.. ... .
.. * *& * ... ... :8&.:o*. .. .. . . ..**. .
.* .. o. : . . *. .... ... .. . . . . :....:.. . . .
*. *o . .. 8. o#. . . . .. .*:... .* *..* . .
. 8 . . .o. : .. . * . . .. ... . .. .
.* . . . . . . ... ..... . . .* . .. .. ...
*. *....... . * ...
... ... *... . :.:. .. .
.*.***o . . ..... . . .
... .. . . .* . .
... . . . . * .
. :. . . . . ..
*. *.o... . *.*
.*. .: ..: .. . . : *
* . * . ..* :. .o ... o
... ... .: . .. . * . o *.: .*. .
.. . ... .. .. .. : .. ... **.*.: * o o. . ....o
.. .. .* . * . o .. * .*. o : . ..*.
* * .. ..* * .... ... o * * ... ...
.. .. . * .* ...*. .. ... ** ..... .. .. ..*.
.. ....* ...... . .. ... .. ...... *... ...
.... . . .. .... . : * . .....* ..... .
.. ... : . * .. .. * . ... . .. .
.. . . . .. . . . .. .. .. ... .. . .
. ... ...: :. . .. .. .. ...
*. *. .. . .... .
. . . ..
. .

And a winter scenery!

string_agg
.::.::::.:::::::::::::::::::::::o::::&::&::o::&::&::&:&::&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&::o:&:o::&::&::&::&::::&::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
..:.:::::::::::::o::o:o:o:o:::&:o::&::&:o::&:o:&::&:&::o::&:&:&:&:&:&:&:&:&:&:&:o:&o:&o:o&:o&:o&:o&:&o:&o:o:o&:&o:o:o:o:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&::&:&::&::&:&:o::o::o::&:&::&:&:&:&:&:&:&::o::o::::o::::::::::::::::::::::::::::::::::.:::*::.::.::.:
... :..::::::::::o::::::::::::&:&:::&::&:::&:o::&::&::&:&:o:&::&:&:&:&:&:&:&:&:&:&&:&:o:&:&:&:&:&:&:&:&:&:&:&&:&:&:&&:&&:o&:&&:&:&:&:&:&:&:&:&:&:&:&::&::&:o:o:&::&:&:::&::o::&:o:::&:::::::::::::::::::::&::::::&::::::::::::::::::::::::.::*:::*::::::::::::
:::::: .:::::::o::::::&:&:&:&:&::::&:&::o:o:&:::o::&:&:&:&::&:&:&::o:o:o:o:o:o:&:&:&:&:&:&:&:&:o:&:&:o&:&:&:&o:o:o:&:&:&:o:&:o:o:&o:o&:&:&:&:&:&:&::o:&:&:o:o::&:&:o::o:o::o::o::::o:::o:&:&:&:&::o:o::o:::::::o::::::o:::::::::::::::::*::::::::::::*::*:.::*::
:: ...:::::::::::&::o::::::::::&:o::&::o:::&::o:&:&:&::o::&:&:o:o:&:&:&:&:&:&:o:&&:&:&&:&:&&:&&:&:&o:&:&:&o:o:&:&:o:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&::&:o::&:o:&::&:&::&:&::::::::::::::::::::o:::::&:::::::::::::::::::*:::::*::.:.::.::::::::::.:
: . :::::::::.:::::::&::&:&:::o::o::&:o:o:&:&::&:::o:&:o:&::&:&:&:&:&:&:&:&:&:o:o:o:&:&o:&:&:&&:&:&:&:o:&:&:o&:&&:o:o&:&:&&:&&:&:o:o:o:o:&:o:&:o:o:&:&:&::&:&::o:o::o::o:::o::&::o::::o:o:o::&:&::o::&:::::&::::::&::::::::::::::::::::::::::::::::*:.::.:::::
:...::::::.: ::::::::::&:::::o:::o::&:::&:::o::o::o:&:&::&::&:o:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&:&o:&:o&:&:&o:&:o:o:&:&:&&:o:&:o:o:&:&:&:&:&:&:o:&:&:o::&::o:&::&:o::o:o::&:o:o::o:::o:&:::::&::::::::::::&::::::::::::::::::::::::::::::*:::*:::*:.::::::::.:*:
:. ...:::.&:::::&::::::&:&::&:&::&::&::&:&::&:o:&::&:&:&:o:o::o:o:o:o:o:&o:&&:&&:&:&:&:&:&:o:&:&&:&:&o:o:&:&:&:&:o&:o:o:&:o:&:&&:&:&:&:&:&:o:&:&:o:&:&:&:o:&:&::&:&::o::&:::o:::o::::&:&::::&::o::&::o::::::&:::::::o:::::::::::::::::::::::.:::::.::.:::::::
..:.:..:.:::.::::::::o::&:::::&::::o::&:&::o::&:::&::&:::&:o:&:&:&::&:&:&:&:::::::::::::::::::::::::&:&::&:&:o:&&:o&:&:&:&:o&:&&:o:&:&&:&:o:o:&:&:&::&:&:&:&::&:&:&:&:o:o:&::&::o:&:::&:::::o::::::::::::::::o:::::o::::::::::::::::::.:::.::*::::::::*::::*::.:
. .... :.::: :::&:::::::::&:o:::&:&::o:::&::o::&:o::o:&:&::&::&:&:&:&:&::::::o:::::o::&::o::&:&::o::::::&::::::::&::&:&:&:o::&:::::::::::::::::::::o:::::::::::::::::::::::o::&::::o:&::o:::::o::&::o::&::o::::::o::::::::::::::::::::::::::::::*:*:::::*:.:::::
.. :.: .:::::&:&::::::o:::o:::o::&::&:o::o:&::&:o:&:&::o:&:&::::::::::::&::&:::::o::::::::&::&::::&:&:&::o::::::::::::::&:&:&::o:o:o:o::o::::o::&::o:o::&:&::o::&:::::::&::::::::o:::::::::::::::::::::::::::::::::::::::::::::::::*:::::::.:*:::::.:.:
...:..:::::.:::::::::::::o:o:&::o::&:o::o::&:::&::&:&::&:o:o:&:&:o::::::&:&::o:::::::o:o:::o:o:o::::&::o:::::&::::o:o:o:o:o:o:o:::::::o::::::::::::o:::::::::::o::::::::::o:::&::::::::::::::::::::::::::::::::::::::::::::::::::.::::*::::::*:::.:::::*:::::::
. .:.:.:::::::o:::::o::&:::::::::o::::o::&::&:o::&:::o:&::&::o:o:&:::::::::::::&::&:::::::::::::::o::::::&:&::::o::::::::::::::::&::o:::::o:::o::o::::&::&::&:::::::&::&:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.::::::.::::*:::::.::::*::.:
.:.::.:::::::::::::o::::::&:&:&:&::&:o::o:::&:::&:&:o::o:&::o:&::&::::::::::::::::::::::::::::::::::::::::::::::::::::::::&:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*::*:.:.::::*::.::::.:::*::*::*:*:::::
.. .:::::::::::::::::::o:&:::::::o::::&:::o::o:&::o::o:&::&:&:&:&:::::::::::::::::::::::::::::::::::::&:::::::::::::::::::::::::&:::::::o::::o::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.::.:.:*:*:.:.:*::*::*::.:.::.:::::::::::*::::::::*:*:
:: :::::::::::::&:::o:::::o:o::&::&:&::o::&:::&:o::&::&:o:o::&:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*::.:.:.:::.::::.:::*::.::.::.::.:.:*:.:.:*::.::*::::.:.:*:::::.
.::. .. ::::::::::o:::::o:::::::o::::&::&::&:&::::&::&::&::&:&::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.:::.::*:.::.::::.::.::.:.::*:.::*::*:.:.:*::*::*:.:*:.::.::::*:::::::.:*::
::::::::::::::::::::::&::::&::o.::o:&:::&::o:::&:&::&:&:o:&::&::.::::::*::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.::::::::::.::*::.:.:::.::.:::::::.:.:::.::::::*::*:*:.:.:.::.:.:.:.:.::.::*::::.:::.:*:*:::.::*
::::::::::::::::o:::::::::::o:::::::::o::&::o:o::o:&:&::&::o:&.:::*:.:*::*::.::.:*:*:.::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*:::::::::::::::::::*:.:.::::::::.:..:.::.:::::::*::*:.:*:*::.:.:*:.:*::::*:::::::*::.::
:::::::::::::&::::::&:::o::::::.::&:::::&::::&::&::::&:&::&:o:::.::*:::.::.::*::::::::.::.:.::::::::.:*:::::::::.::.::::::::::.::::::::::::::.:::::::.:::::.:::::::*:*::::.:*:.:*:::::::.::::::.:*:.:*:*:::*:*:..:.:..:..::.:.:.:.:.:.:..:.:.::.:::*:*:*:*::::.:
.:::::::::::::::::::::::::.::::::::::::::&::o::&::&:&:::&:o:&:..::*::.:.:*:.::.:..:..::.::*::.:.:.:::::.::.::.::::.:::::*:::::::::.::::.:::.:::::.:.::::.:.:::.:.:*::::*:.::::.:::.:::*::::::.:::.:.:::.:*:.:::::.:.:*:.:..::.:.:**.*:*:.:..:..::.:::::::::*:*::
::::::::::::::o::::::::.::::::.:::: :&:::::&:::o:&:&:&:::o:&:.:.:*.:.:.:.:.:.:::.::.:.:.::*:.::::*:.:::*:::::*:*:.:.:.::.::.:.:.::*:::.:.:.:.:::*::.:::::.:.::::::*:*:::.::.:.:.::*::::*::::.:.:.::*::.:.:.:*..:.:.:.*.::..:...:..*....:.:*:.:::.:*:*:.:.:::.:
:..::::::::o:::::::..&:.o.:..:.:..:::::.:::o::::o::::o::&:&:o:.:...:.:.:.:....:...:.:*:.:.:.:.:.:*:.::::.::.:.:*::.:.:::::::.:.:::::::.::*:.::::*::.:::.:*::*:....*::::*:.:.:.:.::.:::*:.:::.:.:.:.:.:.:.:.:*:..:.:.**.....:....:....:.:.....:.:.::::::.:::*:.::
::... :::::::::::::::.::::.: :.:...: ::::::::&:::o:&::o::&:::o::....*.......: .:.:.:.::.:.:.:.:.:.::..::.:.:.::*::::.:..::::*:*...:.::.::::.:*::*:.:..:.:.:.:.::::.:.::.:.:.:.:..:.:.:.::.:.:.:.:.:*:.*:..:..::...:..*.*....*....:...:..*.*.:::::.:*:::*:::.:.:
.:: *:.:.:::::::o::.::::.:.& :.&. :: .::::::::::::&::o::&:&:o:&:.. . :::: . .............:..:.::...:.::..::.:.::.::.:*::::::.::.::*:.:::*:::.:.::.::.:::.:*.:.:.:.::.::*:.*.........*:*::.*....::.:..*:....:..:...:.:......... .......:::.:*:::.::.:*::::
:o.::.:::.:::::::::*::..:: :::. ...:::: ::.::::::&::&:::o:::& ::::& .. ...........:..:.:......:..:*::.:..:...:..:..:..: ..:.:......... .. . . . .....:..:.......:..........:.......... . . .:::::..... .*:::::*:*::*:::.:..
:.:. .::::.:::&:::::..:: ::: .. .: ::::.:::::::&::&::&:o:&:&& ..:.:. . . :.*:*:. . .::*::::*::.:*:.::
..: .::.:::::::&::. ...:.: .... & . &:. :..:::::o:::::::&::& :::..:: . :.:..: .:.::*:::*::::::.:
.. .:.:...:.o::::::: : . : :.. . :: :.:::.:.....:::&::&::&:&:& . . ... .: ::::::*:::.:*:.::
.:. ::.&.:.:::::::: ... :. . . .:: : ..:::.::::::::&::&. . : .::*:*::*:*::.::.:
:: &. :..::::&:...: : . ... . : : ..: : . :....::.:&:::&::& . ::*::::.:::*::.:*:
::: .:. .::.::o..::.:: . . : .. . . :..: ::..:.:&::::o:. ::::*:*::*::*:::.:
..: .::. :: : .::.... : .:. ..*: .. .. :... :..:.:::.:&::&&:&:::& .:.. ::::.::::*::.::*:*:
: :: & . .: :.. . . : :. :... ..... :.. .:. :::&.:.:&:&::o:&:&:. &::::::*::.:*::.::*:.::.
: :.:..:: :: ....:::.. . :. . : .. :: .:... : ::..:::::::o:&::o::8 .:.::::::*:::::.::.:.::*:
:.:. ::*.::...:.:...: . . . : : ... ... ..:..::.::&:::o::o::o:: .::::.:.::::*:.::*:::*:*:.
..::.. :..::: . .:: : . . ::: .: .. .:.: . ..:: ::.::&:::o::o::& ::::::::::*:*:::.::*:.::*::
.. . . :. . ::. .: . : . . .:... . : : .: : ::::.::::::::::&::o:o& *::::*:.::*::::.:.::*::*:.:.:
:. :. *:. . ..... . . . .. .: : :.:. : .::..::.:&:.:&::&:o::& .:::::::::::::*:.::::*::*:.:::.
.... . .:..... ... .. : . :.. : . . :..:..:..:::::o::&:::o:&. ::::::*::*:*:*::::.:*::*:::*:.:.
:..:.: &. : . . . . ..: ..: : .. ..: ::.:...:::::::::o:o:::# .::::::::.:::::::.:*:::*::*:*:.:*:
:: ::.. .: . . .: :: :. .::: . . : :. . . .* &::.::...:::&::::o::& : :::*:*::::.:*::.:::.:.::.:.::.::.
.: ::.:. :.:..::. .. : . . &::: : :.. : . . :::..::.::::::&:o::&:& ::::::::::::*::::.::::.:::*::*::.::.:
::: .::: .. .. . ::.:. *.. :.:::: :. ..:. :.::. . :...:&::&:&:::&::&& ::::::.::*:::::.::::*:.::*::*::.:.:.:*
. . ::::: :::..:. .::.:: : . .. : . . ...:::::::::o::o::& .:::::.::::::*:*:::*:*::::.::.::.:.:.:.:
. ....:...: .. . . . ::.:.:::. : : :.: :.:*:.:&:::o:::&:: .. .... . . ... . . ....:.:.....:.::.:.... ::::::::.::.:::::.::::*:*::.:.:*:::: .:.
:. :::: .:. :.. . .. : ... ...:. .: .. ..*::.:&::&::o:o:::&:& .&:&:&&&:&&:&:&&:&&:&&&:&:&&&:&&&:&:&&&:&:&&:&&:&&:&:&&:&:&:&&&:&:&:::&:&:&::::::::::&:&::::::::::::::::::::::: .::::.:::.:::::.::::.::::*::. :...:....:
::. :. . .: : . .: . . : .. : ..:.... . . :.. . :.::&::::::&:&::& .:&::::::::::::::::::::::::::::::::::::::::::::::::::::::: :. ::::::&:::::::. :.::::::::::::::::::::::::::::::* ::.::::*:::.:.:::*:.::.:*::. . . .:.
..::: : .:. : . . . . .: : :. : ::. . :. . .: .:.....:::::&:o::::&:& &::o:&:o:o:o:o:&::&:o::&:: .:&::o:&::&::o::&::o::&::o::&:. &:&:::&::::. ....:::::::::::::::::::::::::::: ::::*::::::::::.:::.::*::. . . ..
.:. ..: . : . :... :: . .: .*.::.: :...:::::::o:&:::& ::o:::::::::::::::::::::. :::o:::::::o:::::::::::o::::: ::::&: :o.:. . :.::::&::& &:::::::::::::::& ::*:::.:.:.:.::::.:::.:.. . . .:
.: . :.. . .. . : :: .:: ::... .: ..:::::o:o:::::&: .&:::o:o:o:o:o:&:::::::.: ::&:::::&:&::::o::&:&::::::o:& .:&:: .: . ..:::::::: :::::::::::::::: .::::*:::::::::*:.:::.::: .
.... .:. . : . :. . ::: :: .:....:::::..:::::::&:&::& .:o:::::::o:::::::.::..: :::&::::::::::::::::. ::::::: . : : . ::::::::: ..:::::::::::::::.. . ::::::.:.:.:.::.:.:*::.. .
. . .. : . ..... .. .. .... .::::::o:&:&:::::& .&::&:&:&::::o:::.::: . &:::&::o::&:&::o:: .::&: .. .:: .::::::: :o:::::::::::::. :: ::.:.:::::::::. ::.:.:. .
. . . . . : . .:.. . .: . :.:..:::::::::::o:o:& .::::::::&::::::::.::. .::::::::::::&::: . &: :... .: :. .::.:::: .:::::.::.::: . .. ::::::*:*:.:: . . . .
. . .. . .: : :::...:.. :...::.:..:.*:::&::o:::::: .o:o:&:&:::&::::.:.::: : .:o:o::::.:&. :&. &. : ::: . :..:::: ..::. . :... : ... :.:*::.::::*: . . :
. . . . ..... ..: . ...:..::::::::::o::&& :::::::::&:::::::. :.: . . &:::: . ::.: :&: ::. . ::::.:: :: :::: ...::.....::: ..
.. . . . : .. : . .: .: :.::. :.:::::&:.&:::::& .o:&:&:o::::::::.:.:.. : :&:. . ::&:.: :. .. . : : . ::.::: &:.. .. : . ... .:....... . . :
.:. . :. . .. . .. .: : :.. . ::.:&::::::.:::*::&:::& ::::: &:::o::::::....: : .. :::. ..:.. . .. :...: . :.:.: :: :: . . . ... . . .. . :::
. :. . ..: .. : .: ..: :::::..::::.::::o:: :::: .::::::::...:.. . :.:. . ... . . . :.:. . .::. :. . . : . . .
. . . . : .: : .. .:::. .:::::...: :::&::* .:& ..&::::::*::.. :.. &. . :.. . .:.. : . . . :. ..
. . . : ..: .&::: .:::::.:: :&:::&. .:. :.:... ::..:.. : : : . ::. .. ... . . . . .
.. . . . . ..::::. ::::::.. ::::: .: . .:::...::. :: . . . . . . . . . . .
. ... . .. .::::: .::::::..::&. . .:.:: : :. .:: . . :. :.. .
.. .::::. ..:::::: ::. .: . : . .. . :.. . . . .:
. . . . . ::::.. .::::::: :. .: . . . .::. . . . . . . .
:: . . . . . ..:::. .:::::. . . : . :: . :. :. .. . .. . . .
: .. . . . . . .::::. .:::: . . . . ..: .: . .. :. .:.
... . . . . .. .::... .:. . . . . . . ... . .
. . . . ::*:.. . . .: . : . . : : .
. . :.:. . . . . :. . . .
. . . .. . ... :&:: .
.. . .. . . . . .
. . . & . . . .
. . :& . . .. .. .
. . . .:::. . . . .
. . ::: . . ...
. . . ::& : : . :
. . :: .. . ::.: :. ..: . ..
. . . :& . .
. . .&. .. :. :&::: . :. .
. . . .:. . : .. :. :. .
. . . :. : . .:
:::.. ..... :. ::. :.& . . .
:::::::::::::::::::::&:o::::.::......:..: ::::&:&:&:::.. . ... . .. ..... . . . . ... ... . .. . ::::::&: &
:..:.:.::.:::::::::::::::::::::::::::::::::::::::::::::::::::. . . . . :
.::.:.:..:.:.:.::.:.:.:.:*:.:.:*::.::*:::.:.:..:.:.:::::::::::::::. . . ..: : ...
..:*:..::.:.:.:.::.::.:.:.::*:.::*::.:*:.:::::::.::.:..:.::*:.:::::::::o . . . .. . . : . . . :
.....*...*.:.:.:..:*.:.:.:*:.:*:.:.:.::*:....:.:*:.::::.:.:::::.:::::::.:::: . . : . . : . : . . : : .:.:. .:.:&:&. .
.::.:..:..:**.*.::.:.:.:.::.:.:*:.:.:*.:.::.:.:.:.:.*:.:*:.:..:.:.:.:.::::::::::. . . . . .. . . .: ........ . . . ...& ::&. . .
........:...........*.:*.:.....:..:..::.........:...:.:...:..:..:.:.:..:..:*:..:*: . ::. . . . . .... . .. . :: .::::::. . .::: :::& . .
. . .. .....:.. .*:....:...:..*....:. ::: :.....:..*.....:.......:..:...:.:.: : : ...: . ..:..::..........::&:. ::::: . :.. . ...::::.::. . .. .*:::.:: . ::&: .
. . :::. .. .. : :::::::::::::. &::::::::&::&::&:&::&:::&:&::::&&&::::&:&:::::::::::.:.:::::::::::::. .: : ::. ..: . .
:::.: . . . :::::::::::::..::::::o:::&::::::::::::::::::o::::::&::::::&::&::::::::::::::::::::::::.. ..: :. :: . & : ..:::: :... . . .
::.: ...:.:: ::::::::::::::::::o::::::::::o::::&::&:&:o:o::o:&::::o:&:::::::&:::o::::::::::::::o:::o:&:&:&*::::. :.... .::::: :*:..::*:.::..:.::*:.:.: :.. . .
. :..::..:::::::::::&::::::o:::o:::&:&::&:::::::&:::::&:&:::::&:&::::&:::o:o::::::&:&:::&::::::::::::::&::o::o:::::: :::::::::::::::::::::::::::.:::::::
::::.:: .::.:::.. : ...:. ........ ::.:.::::::::::::::::o::::&:::o:::::o::&:&:o:::o:o::::o:&:&::::o:::o::::::&::o:::::::::::&:::::&:&::::::::::::::& ::::::::::::::::::::::::.::.:::::::
........ :.......:.:::..:..... .. .... .:.::.::::::::::::o::::o::::&::::&::&:&::::&::o:::&::::&:&::::::&:&::&::o:o:o:::::::::::o::o:::::&::::::&::::o::::::: :::::::::::::::::::::::::::::::::::
.:.:.:.::.:.:.:.:.:*.:..*.::.::.::.... . . :.::::::.:.::::::::::::o:::o:&::&:&:::o:::o:&:::&::&::&:o::::&:&:&:::::o::&:::::&:o:o:o:&:&::::&:&::::o::&::::::::::o::& .:::::::::::::::::::::::::::::.:*:.
.:.:..:.**.**.:.:..:.:.:.:.:..::.:.:::::::::::::::&:..:.::::::&&&&&:::.... ..: &:::::::::::::::::o:o:&::&::::&::o:::o::&::::&:&:::o:o:::o:&::&:::o:o:&:::::&:o::::::::::::::&::::::o::::::::o:o::o:::::. .:::::::::::::::::::.::*:::::::::::
.:..::.*.::::.::::::*::.::::.::::::::..:.:.::.:::::::::::::::::::::::::::::o::. :::::::&::::o:::&:::::::&::o:::::::&:::o::o::::o:o::::&:o:::::::o::::o::&:&::::o:o:o:o:o:o:::::o::o:::o::o::o::::::::o:: :::::::::::::::::::::::::.::::::..
:*::.::.:.:*:::*:.:::.::::::.:::*::.:::..:::::::::::::::::::::::::::::::::::::. :::::o::.:::::o::::&::o:::::&::o:&:::&:::o::&:o:::&:&:&:::&:&:o:::o:o::&::::o::::::::::::::o:o:::::::::o::::::::&::&:::&. &:::::::::::::::::::::::::::::::::
.:..:.: ::::.::::.:*::::.:.:.:::::::::::::::::::::::::::::::::::::::::::::::.& :.:::::::::&::::o:::&:::&:o:::&:::&:o::&::&::&::&::&::::&::::::&:::&::&:::&:::&:&::&:&::o::::::&::&:o:::::o:::::::::::::. ::::::::::::::.::::::::::::::.::::
.:.::: :.:*::.::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :::::::::o::::::::&:::o::::::&:::&::::::&::&::o::&:::&:o:::o:o:::&::o:::o:::o:::::::::::::&::o::&:::::::o::::&:&::::::::. ::::::::::::::.:::::::::::.::::::.
:*:.: .:*::.::.::::.:.::::::*::::::::::::::::::::::::::::::::::::::o::o::::& o:::o::&:::&:&::o:::o:::o::&:::&::&:&:o:::&:&:::::&:o::::&:::::&::::::&:::::::&:&::o:o::::::::::::o:::&:::::::::::&::::&: ::::::::::::::*:::::::*:::::::.:::
:.::: . .::::::::::.:::::::::::::::::::::::::::::::::::::::::::::::::::::::::& .::::::::::::::::::::::&:::::::&::&::::&:&:&::&::o:&::::o::o::&::::o:o::&:&::&::::o::::&::::&:&::&:::&:::::::&:::::::::::: ::::::::::::::.:::::::::::::.:::::
.....:::..........:.:..:.....:.:...:..:.:.:::.:::*::::::::::::::::o:&::&::&::: .&::o::&::o:o::&:&:o::::&:o:&:&::&::o:::::::&:::o:::&:&::::::o::&::::::::::o::::::::o:::o::&:::::::::::&::o::::&:::::::::: ::::::::::::::::::::::::*::.::.:*:
:.*.:....:**.:.::.:.:*:*::*:::.:.::.::*:.:..:*:..:.:..:*:*::::::::.:.::.:::::* :::::o:::::::o::::::::o:::::::::::o:::&&:&:&:::::o:&:::::&:&::::::o::&:&:&::::o:::o:::o::::::o::&:&:&::::::::::::&::o::::: :::::::::::::::::::::*::::::::::::
...:*.::.:.:*:*:**.:.:.*.::.*:.:.:.:.::.::*:.:*::.:.::*:::*:*:*:*:::.:*::..:.:: . ::o::::&:&:::::&:&:&:::o:o::&:&::::o::o::&:::&:&::::o::::::::&::o::::::::::&:&::::::::::o::::::o::::::::o::&:o:::::::::::: ::::::::::::::::::::::::::::::::::
.........:.:..:..:..:..:..:.::.*.::*:..::.:.:.:.:.:.:.::.*:.::.::.:*::::.::*:.::: :::o::::::::&::::::::&:::::&:::::&:::&:::::o::::::o::&:o:::o::::::&::o::o:::::::&::::&:::&::o::::::::&::::::::::::::::::o: ::::::::::::::::::.:::::.::*::*:*:
.*...:..........*..:*.*..:.:*.:.:..:.:...:.:.:..:.:.:...::.:.:.::*:.:...*..:.:.:. :::::&::::o::::&::::::::&::::o:o::::&::&:&:::o:o::::::&::::::&:::::::::::o::&:::::&::::o::::::::::&:::::::&::::::o::o::::: ::::::::::::::::::::::::::::::::::
.............:......*..:.....:......: ........*:......:..............:.:*.:.:...: o:&:::::&::::::::::o::o::::::::::&:::&:::::&:::::::&:&:::o::::::&:::::&:::::::&:&:::::::::::o::o::::&:::::::::::::::::o::: ::::::::::::::::::::::.::::::*::::
. .... ..............:............. ..:........ .::.:. :.*.......... . ::::::::::::&::o:o:::o:::o:o:o:::::o::::&:&:::&:::::::::&:::o:::::&:::::::::::::::::&::o::&:::::::::::::::::o::::::::::::& ::::::::::::::::::::.:::*::.:::*:.
.:.:. . :. :. ::::o::&::o::&:::::::::::::::::::&::::&:::&:::::::o:::o::::::::o:::::o::::o::o::&:&::::::::::::::o:::&:::::::::::::::::::: ::::::::::::::::::::::::::::::::::
. &::::::::::::::::::::::o::&::::o:::o::::o::::&::o:::::::::::::::::::::::&:::::::::::::::::::::::::::::&::::::::::::::::::: *:::::::::::::::*::::::::*:::.:::.
&::::&:::&::::::::o::&::::::::::::::::::::o:::::::::&:::::o::::::::o::::::::::::::o::o::::o::::::::&:::::::::::::::::::::& ::::::::::::::::::::.::*:::::::.::
. .... . .. . . :::o:::::::::::::::::::::::&::o:::&:&::o:::::::::::::::&::::&::::::::::::::::o:::::::::::::::::::::::::::::::::::::::::::: ::::::::::::::.:::::::::::*:::::::
. .....::::::..::::.:: .:::::::&&:&&:&::&::&:&:&:&:&&::&:o:o::&::::&::o::::&:: &:::::::::::&:::o::::::::&:::::::::::::::o::::o::o::::::::::::::::::::::::::::::&::::::::::::::::::::::::::::::::::::::::: :::::::::::::::::::*::::.::::*:*::
::::::::.:.:*::::*:::::::::::.:::::::::::::::::::::::::::::::::::::::::::::::. ::::::::&::::::::::o::o::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :::::::::::::::::::::.::::*:::::.:
:.:*:*:.:.:::::*::::.::::::::.:::::::::::::::::::::::::::::::::::::::::::::::. &::::::::::::::::::::::::::::o::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::. .:::::::::::.::*::.:::::.:::.:.:::
:.:*:*::.::.:.::*:::::*::.::.::::::::.:::::::::::::::::::::::::::::::::::.:::. ::::::::::::::::::::::::::::::::::o::::o:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.:..:.:.::.:.::::.:::. .::.:::::.::::::::::*:::::::::::.:
:.::*::.::.:.:*:::.:.::::::. .:::::*::::::::.:::::::::::::::::::::::::.::.:::. :::::::::::::::::::::::::::::::::::::::::o:::::::::::::::::&:::::::::::::::::::::::::::::::::::::::::::::::::.:.::::*:::::: .:::::*:::::::*::.::::*:*::*::*:::
.:.:.:*:.::.:::.:::::.:*:.:::::.::::::*:*:::::*::.::::::::::::::::::::.::.:::. ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.:*::*::::::::*::::*::. ::::::::::::::::::*:::::::.:::::*:
::*:*:.:*:*::.::::*:...:::::.:::::.:*::::::*:::::::::.:.::::::::::*: :::: :::. :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.:::::::::::::::::::*::::::*::.::::*::.:::*::::::::::::.:: .:*:*::::*::*:::*:.:.:*::.::.:.:::
.*:.:*:*:.:.::..:*:..:.:.::.::.:*:::::.::.:::*::.::*:::::*:::::::: :.:.::.::: ::::::::::::::::::::::::::::::::::::::.::::::::::::::::.:::.:.:::::::::::::::::::::::.:::.::::::::::::::.:.::*:*:*:.:.::::. ..:.:.:.:::.::*:::::.::.:::.::::*:
.:.:*:.::*::.*::.:.*.:.::.:::::::.:.:.::.::.::.::*::::.::::.::*:::.::..:: ::: :::::::::.::::.:::::::::::::::::::::::::::::.::::::::::::::::::::::.::*::::*::::::::::*:::::*:*:.:.::..*:::.::::::::::::*:: ..:.:..:.:.:::.::*:::.:.:*:.:.:.::
:*:*:.:..:.:.:..:.:*:*::.:*:..:*:.:.::.::.::.::.::*:.::.::.:::::.....: :..:.: ::.:*:.:::::::::.::::::::::::::::::::::::::::::::::*::::::::.::::*::::::*:::::*::::*::::::.::::::::.:.::::.::*:*:.:*:.:.::* .::*:::*::.:.:*:::.:::.::.:::.::.:
..:..:..:*:.*:*:.:.:.:.:.::.::.:.:.::.:.:.:*::.::*:::.:::.::.:.:.:.. :. ::... :.::::::::::*:::::.::*::::::::::::::::::::::*::::.:::.::*:::::.:::::::::::::::::*:::::::::::*:*:*:..:*:*.::.:..:*:*:::.::.: .::::.:::*::::.:.:*:.*:.::.:.:*:*:
.**.*.*:....:..:..:*:.:.:.:*:.:*::.:.:.:::.:.:.:.:*:.:.:*:....:*:..::. :. . .:::.:.:.:.::::::::::::::::::::::::::.::::::::::*::::::::::::::::::*:.:::.::*:::::::.::*:.:::::::::::*::::.::.*.....:..:..:* ::*:...:.:.*::*::.:::.:.:.::.::.:
...:*.*..:.:..*..*.:.:..:.:.:.:.:.:.:.:.:*:*:.:.:.:.::.:.:...:.:.:.:..:... . :.::.:::::::.::.::::::::*::::*:::::.:::::::.::::::::*:::::*:::::*:::::*::::::*::*:::.::::::*::.:....:..:.::.:*:.*.:..:..:.: .:.....::*:.:::.:.:.:.:*:*:.:.:.:*:
:.:..:.*:.*:.:.:.:*.:.:.:*:.:.:*:.:.::.:.:.:.:.:.::.:.:.:.:::*:*:*:..:... .:. :.:::*:.:::.::::.:.::*::::*:::.:::.::::::::::.:::*::::.:::::*::::::::::.:.::::::::*:::*::.:::*:::....:.:::.:::::.::.:.::...: ......:.:.:*:*.:.:.:.:.:.::*:*:.:*:.
...*:..:..:..*.*..*.:..:.*.:.:.:.::.:.::.:.::.:.::.:*:.::.:.:.::.::. ..: ..: ...:*::.:::::*::::*::::::::.:::::::::::.:::*:::.::::::::::::*::::::*::.::.::::.::*::.:::*::*:::*:.:..::.::::*:.:*:.*:.:..:.......:. :....:..:**:.::.:*:.:*:.:*:.::.:.:*:
:.:..:..:.:**:..:.:.:.:.:.:.:.:.:.::*:*::.:.:.::.:.::*:*::.:::*::.*:.::....:.: ..::.:::*:*:*:::::::::::*::::::::::.:::::::::::::.::*:.::*::*::::.::*::::::::.:::::::*:::.:::::*:.:.:.::::::*:*:::.:.::.:..:*:.:.:....::.. .....:.*.:*:***:.:*:.:*:.::.::*:.:*
...:.:.:.:.:...:.:*:*:*:*:.:.::.::.::.::.::*::.::.::*:::.::.:.:*::.:.: ::.:..:.::.:.:.:*:.::::::*:.:.::*::::.::::::::::::::::*:::::*:::::::::::::::::::.:.::.::::*::.:::*::::*:*:::*::::::*::.:::.:*::.::.::.:.:.:..: ..:.: .:.:....:.:.:.:.*.:.:.:.:.:..:.::.:
:.:.:.:.:..:*::..:.:.:.::*::.:.:.:.:.:.:.:.::.::.:.::*:*::*::.::.::.:.: .:.:..:..:.:*:.:::.:.:*:::::::.::::::::*::*::::::.:::::::::.::::.:*:.:.::*::*:::::*:::.:::::::::::.::::::*:::.:.:.::*:.:*:::.::*::. ::*:.:.:....:.:.:.....:.*.:*::.::.:.*.:*.:*.::*:*:*:
*:.*..:.:.:.:.*.:.:*:.::*:.::.:::::.::.:::::.::*:.::*:::.:.:*:.:.:*:.:::..:.:*.:.:..:.::.::.::::.:.::.:::::.:::::::::.:::::*::::.::.:.:::::::::*:::.::::*:::*:::.:.:.:.:.:::*.:*:::*:::::::::::.::.::*:*:.:::.:*::.:....:..:..:.:.....*.:.:.:*:*:*.*.:.:.:.:.:.:
:.*:*:*:.:.:.:.:.:*:.:.::*:*::.:*:.:.::*:.::.:.:.:..:..::.:.:*:*:.:.:.:. :.:.*:.:.::*:.:.:::.:.:::::.::.:.:::::.::::::::*::::.::::::::::::::.::::.:::.::::*:::.::*:::::::.:.:::::.:::*:*:.:*:.:.:.::.::.::*:.::.:.:.:::.:.:.:.:....:.:*:.:.:.:.:.::.:.:.:.:.:.:*
..:..*..:..:...:..:.::.:.:.:.:.:..:.:.:.::..:..:.:.:.::.*:.:.:.:.:*::. : :.:.:.::.:*:::::.:*::::::*::::::::::.:::::.:::::::.:::::.:.::.:.:*::::.:::.::::.:::::::::.::.:*::.::*:*::::.::::::::::.:.:*::.:.:.:*:*::.::.:*:.:::.:.:.:.:.:..:.:.*.:.:..:.:.:*:.:.:.:
:.:.:..*..:..:..:.....:...:....:.:*:.:....:.:.:*:*:*:..::.:.:.:.:*:..::.:..:::*:::::.:.:::::..:.:.:::*::.::*:::*:.::::::::.::*:*::::::::::::.::::::::*::::*::*::.:::::::::::.:::*:.:::*:*:*:*:.:.::.:.:::.:::*::*:.::.:::.:.::*:..*...:*:.:.:*:.:*:*.:.:..:.:.:.
.:..**........*...*.*..:.:.*.::.:.:.:.*:::*:.:*:.::.:*::.:.:.::.::.:::: :::.:.::*:*.::.:*.:*:::*:::::::*:::::::::::.:::.::::::::::..:*:.::*:::*::*:*:::.:::::::::.:*::*:.*:.::*::::::.:::.::::*:::.::::*::.:.::.:::*::*:*::.:.:*::..:.:.:.:*:.:*:.:.:..:.:.:.:.:
..:.:...*..*...:.:.:.:..:..:.:.:.*.*:.:*:*::*::*:*:*::.:.::.:.::.::.:..::*::::*:.:::.::.:::::.:::.:*:::::.::*:::::::::::::::*::*:.:::::::*::::::.::::::::*::.::.:::::::::.:.:.:::*:.:::*::*:.::.:::*:.::*:::.:.:*:.:*::.:*:*:.:.:*::.:.:.*.:.:..:..:*:.:.:..:.:.
:.....:..:..:.:...*..:.:.:.:.:..:*.:*.::.::*:.:.:.:.:*::*:*::.:.:..:*:.:.::.:*:.::*::.:::::*:::.:::::::*::::::*::*::::::.:.:::::::.:*::*:::.::*:::::::*::::::::::.:::.:*:*:..::*:::::*::.::::.:::*::*:*::.:.::.::.::.:.:.:.:::.::.:.:.:.:*:*:.:*:.:.:*:..:.:.:.:
.*.:.:..:..*..:.:..:.:.:.::.*.:.:.:.:.:*:*:.:*::*::.:.:.:.:*:.::*::::::::.:::::::.::::*:.:.:::.::.:.:::::.:::::::::::::.:::::.::.::::::::.:::.:::*::.::::*::*:.::::.:::::.:.:::::*:*::::::.::::.:::.::.:.:.:.::.::*::*:.:*:*:.:.::.::.:.:.:.:.:.:*:.:..:.:*:.:*.
..:.*.*:.:.:.:.:.:*.:.*.:...:.:.:*::.:::*::*::.:.:*::.:*::.:::*:::.::.:.::*:*:*:.:::*:::::::::::::::*:.:::::*:::::::.:::::.::::::*:.:.:*:::.:::*:::::::*::::::::.::::*:.::*:*:.:.:::*:*:*::.:*:::*:::.:.:::::.::.:::*::::::.:.::.::.:.:*:*:*:.:.:.:.:.:.:.:.*:.:
:..*.:...:..:*:.*.*:.:.:.::.::.::.:.:*:*:.:.:.:::*:.:::*:::.::::*::*:::.:::.::::::.:::::::::.:::::::::::*::::::.::*:::::::::::::.::.:::::::::::::::::::::.:.::::::::.:::.:::::::::*:::::::::::*:::.:.:::.:.::::*::*:::.:.:.:::*:.:*::.:.:.:.:*:.:*:*:*:.:*:.:*:*

You can view the queries here: https://github.com/quassnoi/explain-extended-2019

I wish that this New Year brings more bright colors and pretty pictures in your life!

Happy New Year!
Previous New Year posts:

Written by Quassnoi

December 31st, 2018 at 11:00 pm

Posted in PostgreSQL

Tagged with , , ,

Leave a Reply