[vte] lib: Add new SIXEL context and test



commit 93d10c5db61cc286373e7fa75c8856a411f5d469
Author: Christian Persch <chpe src gnome org>
Date:   Mon Oct 19 00:16:36 2020 +0200

    lib: Add new SIXEL context and test

 COPYING.XTERM        |  30 +++
 src/fwd.hh           |   1 +
 src/meson.build      |   9 +-
 src/sixel-context.cc | 505 ++++++++++++++++++++++++++++++++++++++
 src/sixel-context.hh | 668 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/sixel-test.cc    | 551 ++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 1762 insertions(+), 2 deletions(-)
---
diff --git a/COPYING.XTERM b/COPYING.XTERM
new file mode 100644
index 00000000..c34c6216
--- /dev/null
+++ b/COPYING.XTERM
@@ -0,0 +1,30 @@
+Parts of code copied from xterm, under this licence:
+
+Copyright 2013-2019,2020 by Ross Combs
+Copyright 2013-2019,2020 by Thomas E. Dickey
+
+                        All Rights Reserved
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE ABOVE LISTED COPYRIGHT HOLDER(S) BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the name(s) of the above copyright
+holders shall not be used in advertising or otherwise to promote the
+sale, use or other dealings in this Software without prior written
+authorization.
diff --git a/src/fwd.hh b/src/fwd.hh
index f84b76e1..2b7114c4 100644
--- a/src/fwd.hh
+++ b/src/fwd.hh
@@ -36,6 +36,7 @@ class Widget;
 
 namespace sixel {
 
+class Context;
 class Parser;
 class Sequence;
 
diff --git a/src/meson.build b/src/meson.build
index 3700768f..1b6e8b27 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -103,7 +103,12 @@ sixel_parser_sources = files(
   'sixel-parser.hh',
 )
 
-sixel_sources = sixel_parser_sources + files(
+sixel_context_sources = files(
+  'sixel-context.cc',
+  'sixel-context.hh',
+)
+
+sixel_sources = sixel_parser_sources + sixel_context_sources + files(
   'image.cc',
   'image.hh',
   'sixelparser.cc',
@@ -504,7 +509,7 @@ if get_option('sixel')
     install: false,
   )
 
-  test_sixel_sources = glib_glue_sources + sixel_parser_sources + files(
+  test_sixel_sources = glib_glue_sources + sixel_parser_sources + sixel_context_sources + files(
     'cairo-glue.hh',
     'sixel-test.cc',
     'vtedefines.hh',
diff --git a/src/sixel-context.cc b/src/sixel-context.cc
new file mode 100644
index 00000000..7a8826a3
--- /dev/null
+++ b/src/sixel-context.cc
@@ -0,0 +1,505 @@
+/*
+ * Copyright © 2020 Christian Persch
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "sixel-context.hh"
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+
+#ifdef VTE_DEBUG
+#include "debug.h"
+#include "libc-glue.hh"
+#endif
+
+namespace vte::sixel {
+
+/* BEGIN */
+
+/* The following code is copied from xterm/graphics.c where it is under the
+ * licence below; and modified and used here under the GNU Lesser General Public
+ * Licence, version 3 (or, at your option), any later version.
+ */
+
+/*
+ * Copyright 2013-2019,2020 by Ross Combs
+ * Copyright 2013-2019,2020 by Thomas E. Dickey
+ *
+ *                         All Rights Reserved
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE ABOVE LISTED COPYRIGHT HOLDER(S) BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * Except as contained in this notice, the name(s) of the above copyright
+ * holders shall not be used in advertising or otherwise to promote the
+ * sale, use or other dealings in this Software without prior written
+ * authorization.
+ */
+
+/*
+ * Context::make_color_hls:
+ * @h: hue
+ * @l: luminosity
+ * @s: saturation
+ *
+ * Returns the colour specified by (h, l, s) as RGB, 8 bit per component.
+ *
+ * Primary color hues are blue: 0 degrees, red: 120 degrees, and green: 240 degrees.
+ */
+Context::color_t
+Context::make_color_hls(int h,
+                        int l,
+                        int s) noexcept
+{
+        auto const c2p = std::abs(2 * l - 100);
+        auto const cp = ((100 - c2p) * s) << 1;
+        auto const hs = ((h + 240) / 60) % 6;
+        auto const xp = (hs & 1) ? cp : 0;
+        auto const mp = 200 * l - (cp >> 1);
+
+        int r1p, g1p, b1p;
+        switch (hs) {
+        case 0:
+                r1p = cp;
+                g1p = xp;
+                b1p = 0;
+                break;
+        case 1:
+                r1p = xp;
+                g1p = cp;
+                b1p = 0;
+                break;
+        case 2:
+                r1p = 0;
+                g1p = cp;
+                b1p = xp;
+                break;
+        case 3:
+                r1p = 0;
+                g1p = xp;
+                b1p = cp;
+                break;
+        case 4:
+                r1p = xp;
+                g1p = 0;
+                b1p = cp;
+                break;
+        case 5:
+                r1p = cp;
+                g1p = 0;
+                b1p = xp;
+                break;
+        default:
+                __builtin_unreachable();
+        }
+
+        auto const r = ((r1p + mp) * 255 + 10000) / 20000;
+        auto const g = ((g1p + mp) * 255 + 10000) / 20000;
+        auto const b = ((b1p + mp) * 255 + 10000) / 20000;
+
+        return make_color(r, g, b);
+}
+
+/* END */
+
+/* This is called when resetting the Terminal which is currently using
+ * DataSyntax::DECSIXEL syntax. Clean up buffers, but don't reset colours
+ * etc since they will be re-initialised anyway when the context is
+ * used the next time.
+ */
+void
+Context::reset() noexcept
+{
+        m_scanlines_data.reset();
+        m_scanlines_data_capacity = 0;
+        m_scanline_begin = m_scanline_pos = m_scanline_end = nullptr;
+}
+
+/*
+ * Ensure that the scanlines buffer has space for the image (as specified
+ * by the raster and actual dimensions) and at least one full k_max_width
+ * scanline.
+ *
+ * The scanline offsets must be up-to-date before calling this function.
+ *
+ * On success, m_scanline_begin and m_scanline_pos will point to the start
+ * of the current scanline (that is, m_scanline_data + *m_scanlines_offsets_pos),
+ * and m_scanline_end will point to the end of the scanline of k_max_width sixels,
+ * and %true returned.
+ *
+ * On failure, all of m_scanline_begin/pos/end will be set to nullptr, and
+ * %false returned.
+ */
+ bool
+ Context::ensure_scanlines_capacity() noexcept
+ {
+         auto const width = std::max(m_raster_width, m_width);
+         auto const height = std::max(m_raster_height, m_height);
+
+         /* This is guaranteed not to overflow since width and height
+          * are limited by k_max_{width,height}.
+          */
+         auto const needed_capacity = capacity(width, height);
+         auto const old_capacity = m_scanlines_data_capacity;
+
+         if (needed_capacity <= old_capacity)
+                 return true;
+
+         /* Not enought space, so we need to enlarge the buffer. Don't
+          * overallocate, but also don't reallocate too often; so try
+          * doubling but use an upper limit.
+          */
+         auto const new_capacity = std::min(std::max({minimum_capacity(),
+                                                      needed_capacity,
+                                                      old_capacity * 2}),
+                 capacity(k_max_width, k_max_height));
+
+         m_scanlines_data = 
vte::glib::take_free_ptr(reinterpret_cast<color_index_t*>(g_try_realloc_n(m_scanlines_data.release(),
+                                                                                                      
new_capacity,
+                                                                                                      
sizeof(color_index_t))));
+         if (!m_scanlines_data) {
+                 m_scanlines_data_capacity = 0;
+                 m_scanline_pos = m_scanline_begin = m_scanline_end = nullptr;
+                 return false;
+         }
+
+         /* Clear newly allocated capacity */
+         std::memset(m_scanlines_data.get() + old_capacity, 0,
+                     (new_capacity - old_capacity) * sizeof(*m_scanlines_data.get()));
+
+         m_scanlines_data_capacity = new_capacity;
+
+         /* Relocate the buffer pointers. The update_scanline_offsets() above
+          * made sure that m_scanlines_offsets is up to date.
+          */
+         auto const old_scanline_pos = m_scanline_pos - m_scanline_begin;
+         m_scanline_begin = m_scanlines_data.get() + m_scanlines_offsets_pos[0];
+         m_scanline_end = m_scanlines_data.get() + m_scanlines_offsets_pos[1];
+         m_scanline_pos = m_scanline_begin + old_scanline_pos;
+
+         assert(m_scanline_begin <= scanlines_data_end());
+         assert(m_scanline_pos <= scanlines_data_end());
+         assert(m_scanline_end <= scanlines_data_end());
+
+         return true;
+}
+
+void
+Context::reset_colors() noexcept
+{
+        /* DECPPLV2 says that on startup, and after DECSTR, DECSCL and RIS,
+         * all colours are assigned to Black, *not* to a palette.
+         * Instead, it says that devices may have 8- or 16-colour palettes,
+         * and which HLS and RGB values used in DECGCI will result in which
+         * of these 8 or 64 colours being actually used.
+         *
+         * It also says that between DECSIXEL invocations, colour registers
+         * are preserved; in xterm, whether colours are kept or cleared,
+         * is controlled by the XTERM_SIXEL_PRIVATE_COLOR_REGISTERS private
+         * mode.
+         */
+
+        /* Background fill colour, fully transparent by default */
+        m_colors[0] = 0u;
+
+        /* This is the VT340 default colour palette of 16 colours.
+         * PPLV2 defines 8- and 64-colour palettes; not sure
+         * why everyone seems to use the VT340 one?
+         *
+         * Colours 9..14 (name marked with '*') are less saturated
+         * versions of colours 1..6.
+         */
+        m_colors[0 + 1]  = make_color_rgb( 0,  0,  0); /* HLS(  0,  0,  0) */ /* Black    */
+        m_colors[1 + 1]  = make_color_rgb(20, 20, 80); /* HLS(  0, 50, 60) */ /* Blue     */
+        m_colors[2 + 1]  = make_color_rgb(80, 13, 13); /* HLS(120, 46, 72) */ /* Red      */
+        m_colors[3 + 1]  = make_color_rgb(20, 80, 20); /* HLS(240, 50, 60) */ /* Green    */
+        m_colors[4 + 1]  = make_color_rgb(80, 20, 80); /* HLS( 60, 50, 60) */ /* Magenta  */
+        m_colors[5 + 1]  = make_color_rgb(20, 80, 80); /* HLS(300, 50, 60) */ /* Cyan     */
+        m_colors[6 + 1]  = make_color_rgb(80, 80, 20); /* HLS(180, 50, 60) */ /* Yellow   */
+        m_colors[7 + 1]  = make_color_rgb(53, 53, 53); /* HLS(  0, 53,  0) */ /* Grey 50% */
+        m_colors[8 + 1]  = make_color_rgb(26, 26, 26); /* HLS(  0, 26,  0) */ /* Grey 25% */
+        m_colors[9 + 1]  = make_color_rgb(33, 33, 60); /* HLS(  0, 46, 29) */ /* Blue*    */
+        m_colors[10 + 1] = make_color_rgb(60, 26, 26); /* HLS(120, 43, 39) */ /* Red*     */
+        m_colors[11 + 1] = make_color_rgb(33, 60, 33); /* HLS(240, 46, 29) */ /* Green*   */
+        m_colors[12 + 1] = make_color_rgb(60, 33, 60); /* HLS( 60, 46, 29) */ /* Magenta* */
+        m_colors[13 + 1] = make_color_rgb(33, 60, 60); /* HLS(300, 46, 29) */ /* Cyan*    */
+        m_colors[14 + 1] = make_color_rgb(60, 60, 33); /* HLS(180, 46, 29) */ /* Yellow*  */
+        m_colors[15 + 1] = make_color_rgb(80, 80, 80); /* HLS(  0, 80,  0) */ /* Grey 75% */
+
+        /* Devices may use the same colour palette for DECSIXEL as for
+         * text mode, so initialise colours 16..255 to the standard 256-colour
+         * palette. I haven't seen any documentation from DEC that says
+         * this is what they actually did, but this is what all the libsixel
+         * related terminal emulator patches did, so let's copy that. Except
+         * that they use a variant of the 666 colour cube which
+         * uses make_color_rgb(r * 51, g * 51, b * 51) instead of the formula
+         * below which is the same as for the text 256-colour palette's 666
+         * colour cube, and make_color_rgb(i * 11, i * 11, i * 11) instead of
+         * the formula below which is the same as for the text 256-colour palette
+         * greyscale ramp.
+         */
+        /* 666-colour cube */
+        auto make_cube_color = [&](unsigned r,
+                                   unsigned g,
+                                   unsigned b) constexpr noexcept -> auto
+        {
+                return make_color(r ? r * 40u + 55u : 0,
+                                  g ? g * 40u + 55u : 0,
+                                  b ? b * 40u + 55u : 0);
+        };
+
+        for (auto n = 0; n < 216; ++n)
+                m_colors[n + 16 + 1] = make_cube_color(n / 36, (n / 6) % 6, n % 6);
+
+        /* 24-colour greyscale ramp */
+        for (auto n = 0; n < 24; ++n)
+                m_colors[n + 16 + 216 + 1] = make_color(8 + n * 10, 8 + n * 10, 8 + n * 10);
+
+        /* Set all other colours to black */
+        for (auto n = 256 + 1; n < k_num_colors + 1; ++n)
+                m_colors[n] = make_color(0, 0, 0);
+}
+
+void
+Context::prepare(uint32_t introducer,
+                 color_t fg,
+                 color_t bg,
+                 bool private_color_registers,
+                 double pixel_aspect) noexcept
+{
+        m_introducer = introducer;
+        m_st = 0;
+        m_width = m_height = 0;
+        m_raster_width = m_raster_height = 0;
+
+        if (private_color_registers)
+                reset_colors();
+
+        /* FIXMEchpe: this all seems bogus. */
+        set_color(0, bg);
+        if (private_color_registers)
+                set_color(param_to_color_register(0), fg);
+
+        /*
+         * DEC PPLV2 says that on entering DECSIXEL mode, the active colour
+         * is set to colour to colour register 0.
+         * Xterm defaults to register 3.
+         */
+        set_current_color(param_to_color_register(0));
+
+        /* Clear bufer, and scanline offsets */
+        std::memset(m_scanlines_offsets, 0, sizeof(m_scanlines_offsets));
+
+        if (m_scanlines_data)
+                std::memset(m_scanlines_data.get(), 0,
+                            m_scanlines_data_capacity * sizeof(color_index_t));
+
+        m_scanlines_offsets_pos = scanlines_offsets_begin();
+        m_scanlines_offsets[0] = 0;
+
+        ensure_scanline();
+}
+
+template<typename C,
+         typename P>
+inline C*
+Context::image_data(size_t* size,
+                    unsigned stride,
+                    P pen) noexcept
+{
+        auto const height = image_height();
+        auto const width = image_width();
+        if (height == 0 || width == 0 || !m_scanlines_data)
+                return nullptr;
+
+        if (size)
+                *size = height * stride;
+
+        auto wdata = vte::glib::take_free_ptr(reinterpret_cast<C*>(g_try_malloc_n(height, stride)));
+        if (!wdata)
+                return nullptr;
+
+        /* FIXMEchpe: this can surely be optimised, perhaps using SIMD, and
+         * being more cache-friendly.
+         */
+
+        assert((stride % sizeof(C)) == 0);
+        auto wstride = stride / sizeof(C);
+        assert(wstride >= width);
+        // auto wdata_end = wdata + wstride * height;
+
+        /* There may be one scanline at the bottom that extends below the image's height,
+         * and needs to be handled specially. First convert all the full scanlines, then
+         * the last partial one.
+         *
+         * FIXMEchpe: colour data needs byteswapping for big endian?
+         */
+        auto scanlines_offsets_pos = scanlines_offsets_begin();
+        auto wdata_pos = wdata.get();
+        auto y = 0u;
+        for (;
+             (scanlines_offsets_pos + 1) < scanlines_offsets_end() && (y + 6) <= height;
+             ++scanlines_offsets_pos, wdata_pos += 6 * wstride, y += 6) {
+                auto const scanline_begin = m_scanlines_data.get() + scanlines_offsets_pos[0];
+                auto const scanline_end = m_scanlines_data.get() + scanlines_offsets_pos[1];
+                auto x = 0u;
+                for (auto scanline_pos = scanline_begin; scanline_pos < scanline_end; ++x) {
+                        for (auto n = 0; n < 6; ++n) {
+                                wdata_pos[n * wstride + x] = pen(*scanline_pos++);
+                        }
+                }
+
+                /* Clear leftover space */
+                if (x < wstride) {
+                        auto const bg = pen(0);
+                        for (auto n = 0; n < 6; ++n) {
+                                std::fill(&wdata_pos[n * wstride + x],
+                                          &wdata_pos[(n + 1) * wstride],
+                                          bg);
+                        }
+                }
+        }
+
+        if (y < height && (y + 6) > height &&
+            (scanlines_offsets_pos + 1) < scanlines_offsets_end()) {
+                auto const h = height - y;
+                auto const scanline_begin = m_scanlines_data.get() + scanlines_offsets_pos[0];
+                auto const scanline_end = m_scanlines_data.get() + scanlines_offsets_pos[1];
+                auto x = 0u;
+                for (auto scanline_pos = scanline_begin; scanline_pos < scanline_end; ++x) {
+                        for (auto n = 0u; n < h; ++n) {
+                                wdata_pos[n * wstride + x] = pen(*scanline_pos++);
+                        }
+
+                        scanline_pos += 6 - h;
+                }
+
+                /* Clear leftover space */
+                if (x < wstride) {
+                        auto const bg = pen(0);
+                        for (auto n = 0u; n < h; ++n) {
+                                std::fill(&wdata_pos[n * wstride + x],
+                                          &wdata_pos[(n + 1) * wstride],
+                                          bg);
+                        }
+                }
+        }
+
+        /* We drop the scanlines buffer here if it's bigger than the default buffer size,
+         * so that parsing a big image doesn't retain the large buffer forever.
+         */
+        if (m_scanlines_data_capacity > minimum_capacity())
+                m_scanlines_data.reset();
+
+        return wdata.release();
+}
+
+// This is only used in the test suite
+Context::color_index_t*
+Context::image_data_indexed(size_t* size,
+                            unsigned extra_width_stride) noexcept
+{
+        return image_data<color_index_t>(size,
+                                         (image_width() + extra_width_stride) * sizeof(color_index_t),
+                                         [](color_index_t pen) noexcept -> color_index_t { return pen; });
+}
+
+#ifdef VTE_COMPILATION
+
+uint8_t*
+Context::image_data() noexcept
+{
+        return reinterpret_cast<uint8_t*>(image_data<color_t>(nullptr,
+                                                              
cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, image_width()),
+                                                              [&](color_index_t pen) noexcept -> color_t { 
return m_colors[pen]; }));
+}
+
+vte::cairo::Surface
+Context::image_cairo() noexcept
+{
+        static cairo_user_data_key_t s_data_key;
+
+        auto data = image_data();
+        if (!data)
+                return nullptr;
+
+        auto const stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, image_width());
+        auto surface = vte::cairo::Surface{cairo_image_surface_create_for_data(data,
+                                                                               CAIRO_FORMAT_ARGB32,
+                                                                               image_width(),
+                                                                               image_height(),
+                                                                               stride)};
+
+#ifdef VTE_DEBUG
+        _VTE_DEBUG_IF(VTE_DEBUG_IMAGE) {
+                static auto num = 0;
+
+                auto tmpl = vte::glib::take_string(g_strdup_printf("vte-image-sixel-%05d-XXXXXX.png",
+                                                                   ++num));
+                auto err = vte::glib::Error{};
+                char* path = nullptr;
+                auto fd = vte::libc::FD{g_file_open_tmp(tmpl.get(), &path, err)};
+                if (fd) {
+                        auto rv = cairo_surface_write_to_png(surface.get(), path);
+                        if (rv == CAIRO_STATUS_SUCCESS)
+                                g_printerr("SIXEL Image written to '%s'\n", path);
+                        else
+                                g_printerr("Failed to write SIXEL image to '%s': %m\n", path);
+                } else {
+                        g_printerr("Failed to create tempfile for SIXEL image: %s\n", err.message());
+                }
+                g_free(path);
+        }
+#endif /* VTE_DEBUG */
+
+        if (cairo_surface_set_user_data(surface.get(),
+                                        &s_data_key,
+                                        data,
+                                        (cairo_destroy_func_t)&g_free) != CAIRO_STATUS_SUCCESS) {
+                /* When this fails, it's not documented whether the destroy func
+                 * will have been called; reading cairo code, it appears it is *not*.
+                 */
+                cairo_surface_finish(surface.get()); // drop data buffer
+                g_free(data);
+
+                return nullptr;
+        }
+
+        return surface;
+}
+
+#endif /* VTE_COMPILATION */
+
+} // namespace vte::sixel
diff --git a/src/sixel-context.hh b/src/sixel-context.hh
new file mode 100644
index 00000000..db9b2e36
--- /dev/null
+++ b/src/sixel-context.hh
@@ -0,0 +1,668 @@
+/*
+ * Copyright © 2020 Christian Persch
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <bit>
+#include <cstdint>
+#include <iterator>
+#include <utility>
+
+#ifdef VTE_COMPILATION
+#include <cairo.h>
+#include "cairo-glue.hh"
+#endif
+
+#include "glib-glue.hh"
+//#include "parser-glue.hh"
+#include "sixel-parser.hh"
+#include "vtedefines.hh"
+
+#if __cplusplus <= 201703L
+// FIXMEchpe remove this and just upgrade to C++20
+namespace std {
+enum class endian
+{
+    little = __ORDER_LITTLE_ENDIAN__,
+    big    = __ORDER_BIG_ENDIAN__,
+    native = __BYTE_ORDER__
+};
+} // namespace std
+#endif
+
+namespace vte::sixel {
+
+class Context {
+
+        friend class Parser;
+
+public:
+        Context() = default;
+        ~Context() = default;
+
+        Context(Context const&) = delete;
+        Context(Context&&) noexcept = delete;
+
+        Context& operator=(Context const&) = delete;
+        Context& operator=(Context&&) noexcept = delete;
+
+        /* Packed colour, RGBA 8 bits per component */
+        using color_t = uint32_t;
+
+        /* Indexed colour */
+        using color_index_t = uint16_t;
+
+private:
+
+        uint32_t m_introducer{0};
+        uint32_t m_st{0};
+
+        static inline constexpr unsigned const k_max_width = 2048u;
+
+        static inline constexpr unsigned const k_max_height = 2052u;
+        static_assert((k_max_height % 6) == 0, "k_max_height not divisible by 6");
+
+        static inline constexpr int const k_num_colors = VTE_SIXEL_NUM_COLOR_REGISTERS;
+        static_assert((k_num_colors & (k_num_colors - 1)) == 0, "k_num_colors not a power of 2");
+
+        /* The width and height as set per DECGRA */
+        unsigned m_raster_width{0};
+        unsigned m_raster_height{0};
+
+        /* The width and height as per the SIXEL data received */
+        unsigned m_width{0};
+        unsigned m_height{0};
+
+public:
+
+        constexpr auto max_width()  const noexcept { return k_max_width;  }
+        constexpr auto max_height() const noexcept { return k_max_height; }
+        constexpr auto num_colors() const noexcept { return k_num_colors;  }
+
+        constexpr auto image_width() const noexcept
+        {
+                return std::max(m_width, m_raster_width);
+        }
+
+        constexpr auto image_height() const noexcept
+        {
+                return std::max(m_height, m_raster_height);
+        }
+
+private:
+
+        color_t m_colors[1 + k_num_colors];
+        bool m_palette_modified{false};
+
+        color_index_t m_current_color{0};
+
+        Parser m_sixel_parser{};
+
+        /* All sixels on the current scanline OR'd together */
+        uint8_t m_scanline_mask{0};
+
+        int m_repeat_count{1};
+
+        /*
+         * m_scanlines_data stores the pixel data in indexed colours (not resolved
+         * RGBA colours).
+         *
+         * Pixels are stored interleaved in scan lines of six vertical pixels.
+         * This makes writing them cache-efficient, and allows to easily write
+         * more pixels in one scanline than the previous scanlines without having
+         * to copy and pad already-written data. The buffer is created at the
+         * start, and enlarged (if necessary) when starting a new scanline.
+         *
+         * m_scanlines_data is allocated/re-allocated as needed, and stores
+         * m_scanlines_data_capacity color_index_t items.
+         *
+         * The offsets of the scanlines in m_scanlines_data are stored in
+         * m_scanlines_offsets; scanline N occupies
+         * [m_scanlines_offsets[N], m_scanlines_offsets[N+1]).
+         *
+         * m_scanlines_offsets_pos points to the offset in m_scanlines_offsets of the
+         * current scanline, and is never nullptr. When in a valid scanline, there is
+         * space to write to m_scanlines_offsets_pos[1] to store the scanline end
+         * position.
+         *
+         * m_scanline_begin is a pointer to the current scanline being written;
+         * m_scanline_pos is a pointer to the current write position, and
+         * m_scanline_end is a pointer to the end of the scanline. All scanlines
+         * have space to write up to k_max_width sixels (i.e. have 6 * k_max_width
+         * items), regardless of m_width.
+         * If allocation fails, or height limits are exceeded, all three pointers
+         * are set to nullptr.
+         *
+         * [FIXME: This could be further improved (e.g. wrt. memory fragmentation) by
+         * using a tempfile to store the pixel data, having only a fixed buffer
+         * of N * k_max_width * 6 size, and writing out the scanline data on DECGNL,
+         * instead of re-/allocating memory for the whole buffer.]
+         */
+
+        size_t m_scanlines_data_capacity{0};
+        vte::glib::FreePtr<color_index_t> m_scanlines_data{};
+
+        color_index_t* m_scanline_begin{nullptr};
+        color_index_t* m_scanline_end{nullptr};
+        color_index_t* m_scanline_pos{nullptr};
+        unsigned m_scanlines_offsets[(k_max_height + 5) / 6 + 1]; // one more than the maximum
+                                                                  // number of scanlines since
+                                                                  // we need to store begin and
+                                                                  // end offsets for each scanline
+        unsigned* m_scanlines_offsets_pos{nullptr};
+
+        inline auto scanlines_data_begin() const noexcept
+        {
+                return m_scanlines_data.get();
+        }
+
+        inline auto scanlines_data_end() const noexcept
+        {
+                return m_scanlines_data.get() + m_scanlines_data_capacity;
+        }
+
+        inline auto scanlines_offsets_begin() noexcept
+        {
+                return std::begin(m_scanlines_offsets);
+        }
+
+        inline constexpr auto scanlines_offsets_end() const noexcept
+        {
+                return std::end(m_scanlines_offsets);
+        }
+
+        inline constexpr auto scanline_capacity() const noexcept
+        {
+                return k_max_width * 6;
+        }
+
+        inline constexpr auto scanlines_count() const noexcept
+        {
+                return unsigned(m_scanlines_offsets_pos - std::begin(m_scanlines_offsets));
+        }
+
+        /* Returns the capacity needed to storage an image of width×height
+         * dimensions, plus one max-sized scanline.
+         */
+        inline constexpr auto
+        capacity(size_t const width,
+                 size_t const height) noexcept
+        {
+                auto const scanlines = (height + 5) / 6;
+                return (width * scanlines + k_max_width) * 6;
+        }
+
+        inline constexpr auto minimum_capacity() noexcept { return capacity(k_max_width, 64); }
+
+        bool ensure_scanlines_capacity() noexcept;
+
+        void
+        ensure_scanline() noexcept
+        {
+                if (!ensure_scanlines_capacity()) {
+                        m_scanline_pos = m_scanline_begin = m_scanline_end = nullptr;
+                        return;
+                }
+
+                m_scanlines_offsets_pos[1] = m_scanlines_offsets_pos[0];
+                m_scanline_pos = m_scanline_begin = scanlines_data_begin() + m_scanlines_offsets_pos[0];
+                m_scanline_end = m_scanline_begin + scanline_capacity();
+        }
+
+        void
+        update_scanline_offsets() noexcept
+        {
+                /* Update the scanline end offset and the line width */
+                auto const width = unsigned(m_scanline_pos - m_scanline_begin);
+                assert((width % 6) == 0);
+                m_width = std::min(std::max(m_width, width / 6), k_max_width);
+
+                auto const pos = unsigned(m_scanline_pos - m_scanlines_data.get());
+                assert((pos % 6) == 0);
+                m_scanlines_offsets_pos[1] = std::max(m_scanlines_offsets_pos[1], pos);
+        }
+
+        bool
+        finish_scanline()
+        {
+                if (m_scanline_begin == m_scanline_end)
+                        return false;
+
+                auto msb = [](unsigned v) constexpr noexcept -> unsigned
+                {
+                        return 8 * sizeof(unsigned) - __builtin_clz(v);
+                };
+
+                static_assert(msb(0b1u) == 1, "wrong");
+                static_assert(msb(0b10u) == 2, "wrong");
+                static_assert(msb(0b100u) == 3, "wrong");
+                static_assert(msb(0b1000u) == 4, "wrong");
+                static_assert(msb(0b1'0000u) == 5, "wrong");
+                static_assert(msb(0b10'0000u) == 6, "wrong");
+                static_assert(msb(0b11'1111u) == 6, "wrong");
+
+                /* Update the image height if there was any pixel set in the current scanline. */
+                m_height = m_scanline_mask ? std::min(scanlines_count() * 6 + msb(m_scanline_mask), 
k_max_height) : m_height;
+
+                m_scanline_mask = 0;
+                m_repeat_count = 1;
+
+                update_scanline_offsets();
+
+                return true;
+        }
+
+        inline constexpr auto
+        param_to_color_register(int param) noexcept
+        {
+                /* Colour registers are wrapped, as per DEC documentation.
+                 *
+                 * We internally reserve a register for fully transparent
+                 * colour, and use register 0 for it since that makes it easier
+                 * to initialise the buffer. Therefore the user-provided
+                 * registers are stored at + 1 their public number.
+                 */
+                return (param & (k_num_colors - 1)) + 1;
+        }
+
+        inline constexpr color_t
+        make_color(unsigned r,
+                   unsigned g,
+                   unsigned b) noexcept
+        {
+                if constexpr (std::endian::native == std::endian::little) {
+                        return b | g << 8 | r << 16 | 0xffu << 24 /* opaque */;
+                } else if constexpr (std::endian::native == std::endian::big) {
+                        return 0xffu /* opaque */ | r << 8 | g << 16 | b << 24;
+                } else {
+                        __builtin_unreachable();
+                }
+        }
+
+        color_t
+        make_color_hls(int h,
+                       int l,
+                       int s) noexcept;
+
+        inline constexpr color_t
+        make_color_rgb(unsigned r,
+                       unsigned g,
+                       unsigned b) noexcept
+        {
+                auto scale = [](unsigned value) constexpr noexcept -> auto
+                {
+                        return (value * 255u + 50u) / 100u;
+                };
+
+                return make_color(scale(r), scale(g), scale(b));
+        }
+
+        void
+        set_color(color_index_t reg,
+                  color_t color) noexcept
+        {
+                m_colors[m_current_color = reg] = color;
+                m_palette_modified = true;
+        }
+
+        void
+        set_color_hls(unsigned reg,
+                      unsigned h,
+                      unsigned l,
+                      unsigned s) noexcept
+        {
+                set_color(reg, make_color_hls(h, l, s));
+        }
+
+        void
+        set_color_rgb(unsigned reg,
+                      unsigned r,
+                      unsigned g,
+                      unsigned b) noexcept
+        {
+                set_color(reg, make_color_rgb(r, g, b));
+        }
+
+        void
+        set_current_color(unsigned reg) noexcept
+        {
+                m_current_color = reg;
+        }
+
+        template<typename C,
+                 typename P>
+        inline C* image_data(size_t* size,
+                             unsigned stride,
+                             P pen) noexcept;
+
+        void
+        DECGCI(vte::sixel::Sequence const& seq) noexcept
+        {
+                /*
+                 * DECGCI - DEC Graphics Color Introducer
+                 * Selects and defines the current colour.
+                 *
+                 * Arguments:
+                 *   args[0]: colour register
+                 *   args[1]: colour coordinate system
+                 *     1: HLS
+                 *     2: RGB
+                 *   args[2..4]: colour components
+                 *     args[2]: 0..360 for HLS or 0..100 for RGB
+                 *     args[3]: 0..100 for HSL and RGB
+                 *     args[4]: 0..100 for HSL and RGB
+                 *
+                 * Defaults:
+                 *   args[0]: 0
+                 *   args[2]: no default
+                 *   args[3..5]: 0
+                 *
+                 * If only one parameter is specified, selects the colour register
+                 * for the following SIXELs to use. If more parameters are specified,
+                 * additionally re-defines that colour register with the colour
+                 * specified by the parameters.
+                 *
+                 * If the colour values exceed the ranges specified above, the DEC
+                 * documentation says that the sequence is ignored.
+                 * [FIXMEchpe: alternatively, we could just clamp to the range]
+                 * [FIXMEchpe: check whether we need to set the current colour
+                 *  register even in that case]
+                 *
+                 * References: DEC PPLV2 § 5.8
+                 */
+
+                m_repeat_count = 1;
+
+                auto const reg = param_to_color_register(seq.param(0, 0));
+
+                switch (seq.size()) {
+                case 0: /* no param means param 0 has default value */
+                case 1:
+                        /* Switch to colour register */
+                        set_current_color(reg);
+                        break;
+
+                case 2 ... 5:
+                        switch (seq.param(1)) {
+                        case -1: /* this parameter admits no default */
+                        default:
+                                break;
+
+                        case 1: /* HLS */ {
+                                auto const h = seq.param(2, 0);
+                                auto const l = seq.param(3, 0);
+                                auto const s = seq.param(4, 0);
+                                if (G_UNLIKELY(h > 360 || l > 100 || s > 100))
+                                        break;
+
+                                set_color_hls(reg, h, l, s);
+                                break;
+                        }
+
+                        case 2: /* RGB */ {
+                                auto const r = seq.param(2, 0);
+                                auto const g = seq.param(3, 0);
+                                auto const b = seq.param(4, 0);
+                                if (G_UNLIKELY(r > 100 || g > 100 || b > 100))
+                                        break;
+
+                                set_color_rgb(reg, r, g, b);
+                                break;
+                        }
+                        }
+                        break;
+
+                default:
+                        break;
+                }
+        }
+
+        void
+        DECGCR(vte::sixel::Sequence const& seq) noexcept
+        {
+                /* DECGCR - DEC Graphics Carriage Return
+                 * Moves the active position to the left margin.
+                 *
+                 * (Note: DECCRNLM mode does not apply here.)
+                 *
+                 * References: DEC PPLV2 § 5.8
+                 */
+
+                /* Failed already, or exceeded limits */
+                if (m_scanline_begin == m_scanline_end)
+                        return;
+
+                /* Update the scanline end offset of the current scanline, and return
+                 * position to the start of the scanline.
+                 */
+                update_scanline_offsets();
+
+                m_repeat_count = 1;
+                m_scanline_pos = m_scanline_begin;
+        }
+
+        void
+        DECGNL(vte::sixel::Sequence const& seq) noexcept
+        {
+                /* DECGNL - DEC Graphics Next Line
+                 * Moves the active position to the left margin and
+                 * down by one scanline (6 pixels).
+                 *
+                 * References: DEC PPLV2 § 5.8
+                 */
+
+                /* Failed already, or exceeded limits */
+                if (!finish_scanline())
+                        return;
+
+                /* Go to next scanline. If the number of scanlines exceeds the maximum
+                 * (as defined by k_max_height), set the scanline pointers to nullptr.
+                 */
+                ++m_scanlines_offsets_pos;
+                if (m_scanlines_offsets_pos + 1 >= scanlines_offsets_end()) {
+                        m_scanline_pos = m_scanline_begin = m_scanline_end = nullptr;
+                        return;
+                }
+
+                ensure_scanline();
+        }
+
+        void
+        DECGRA(vte::sixel::Sequence const& seq) noexcept
+        {
+                /*
+                 * DECGRA - DEC Graphics Raster Attributes
+                 * Selects the raster attributes for the SIXEL data following.
+                 *
+                 * Arguments:
+                 *   args[0]: pixel aspect ratio numerator (max: 32k)
+                 *   args[1]: pixel aspect ratio denominator (max: 32k)
+                 *   args[2]: horizontal size (in px) of the image
+                 *   args[3]: vertical size (in px) of the image
+                 *
+                 * Defaults:
+                 *   args[0]: 1
+                 *   args[1]: 1
+                 *   args[2]: no default
+                 *   args[3]: no default
+
+                 * Note that the image will not be clipped to the provided
+                 * size.
+                 *
+                 * References: DEC PPLV2 § 5.8
+                 */
+
+                /* If any SIXEL data, or positioning command (DECGCR, DECGNL) has
+                 * been received prior to this command, then DECGRA should be ignored.
+                 * This check only approximates that condition, but that's good enough.
+                 */
+                if (m_scanlines_offsets_pos != scanlines_offsets_begin() ||
+                    m_scanline_begin != m_scanlines_data.get() ||
+                    m_scanline_pos != m_scanline_begin ||
+                    m_scanlines_offsets[1] != 0 ||
+                    m_scanlines_offsets[1] != m_scanlines_offsets[0])
+                        return;
+
+                #if 0
+                /* VTE doesn't currently use the pixel aspect ratio */
+                auto const aspect_num = seq.param(0, 1, 1, 1 << 15 /* 32Ki */);
+                auto const aspect_den = seq.param(1, 1, 1, 1 << 15 /* 32Ki */);
+                auto const pixel_aspect = std::clamp(aspect_num / aspect_den, 0.1, 10.0);
+                #endif
+
+                m_raster_width = seq.param(2, 0, 0, k_max_width);
+                m_raster_height = seq.param(3, 0, 0, k_max_height);
+
+                /* Nothing else needs to be done here right now; the current
+                 * scanline has enough space for k_max_width sixels, and the
+                 * new raster width and height will be taken into account when
+                 * resizing the m_scanlines_data buffer next.
+                 */
+        }
+
+        void
+        DECGRI(vte::sixel::Sequence const& seq) noexcept
+        {
+                /* DECGRI - DEC Graphics Repeat Introducer
+                 * Specifies the repeat count for the following SIXEL.
+                 *
+                 * Arguments:
+                 *   args[0]: the repeat count
+                 *
+                 * Defaults:
+                 *   args[0]: 1
+                 *
+                 * References: DEC PPLV2 § 5.8
+                 */
+
+                /* DEC terminals limited the repetition count to 255, but the SIXEL
+                 * test data includes repeat counts much greater. Since we limit to
+                 * k_max_width anyway when executing the repeat on the next sixel,
+                 * don't limit here.
+                 */
+                m_repeat_count = seq.param(0, 1);
+        }
+
+        /* FIXMEchpe: should also set
+         *
+         *      m_repeat_count = 1;
+         *
+         * for all the unused RESERVED_* sixel commands.
+         */
+
+        void
+        SIXEL(uint8_t sixel) noexcept
+        {
+                /* SIXEL data
+                 * Data encodes a scanline of six pixels in the integer range
+                 * 0x00 .. 0x3f, with the LSB representing the top pixel
+                 * and the MSB representing the bottom pixel.
+                 *
+                 * References: DEC PPLV2 § 5.5.1
+                 */
+
+                if (sixel) {
+                        auto const color = m_current_color;
+                        auto const scanline_end = m_scanline_end;
+                        auto scanline_pos = m_scanline_pos;
+
+                        for (auto n = m_repeat_count;
+                             n > 0 && G_LIKELY(scanline_pos < scanline_end);
+                             --n) {
+                                /* Note that the scanline has space for at least 6 pixels, wo we
+                                 * don't need to check scanline_pos < scanline_end in this inner loop.
+                                 *
+                                 * FIXMEchpe: this can likely be optimised with some SIMD?
+                                 */
+                                for (auto mask = 0b1u; mask < 0b100'0000u; mask <<= 1) {
+                                        auto const old_color = *scanline_pos;
+                                        *scanline_pos++ = sixel & mask ? color : old_color;
+                                }
+
+                                assert(scanline_pos <= scanline_end);
+                        }
+
+                        m_scanline_pos = scanline_pos;
+                        m_scanline_mask |= sixel;
+
+                } else {
+                        /* If there are no bits to set, just advance the position */
+                        m_scanline_pos = std::min(m_scanline_end,
+                                                  m_scanline_pos + m_repeat_count * 6);
+                }
+
+                m_repeat_count = 1;
+        }
+
+        void
+        SIXEL_ST(char32_t st) noexcept
+        {
+                m_st = st;
+
+                /* Still need to finish the current scanline. */
+                finish_scanline();
+        }
+
+public:
+
+        void prepare(uint32_t introducer,
+                     color_t fg,
+                     color_t bg,
+                     bool private_color_registers,
+                     double pixel_aspect = 1.0) noexcept;
+
+        void reset_colors() noexcept;
+
+        void reset() noexcept;
+
+        uint8_t* image_data() noexcept;
+
+        // These are only used in the test suite
+        color_index_t* image_data_indexed(size_t* size = nullptr,
+                                          unsigned extra_width_stride = 0) noexcept;
+        auto color(unsigned idx) const noexcept { return m_colors[idx]; }
+
+#ifdef VTE_COMPILATION
+        vte::cairo::Surface image_cairo() noexcept;
+#endif
+
+        void
+        set_mode(Parser::Mode mode)
+        {
+                m_sixel_parser.set_mode(mode);
+        }
+
+        auto
+        parse(uint8_t const* const bufstart,
+              uint8_t const* const bufend,
+              bool eos) noexcept -> auto
+        {
+                return m_sixel_parser.parse(bufstart, bufend, eos, *this);
+        }
+
+        constexpr auto introducer() const noexcept { return m_introducer; }
+        constexpr auto st() const noexcept { return m_st; }
+
+        constexpr bool
+        is_matching_controls() const noexcept
+        {
+                return ((introducer() ^ st()) & 0x80) == 0;
+        }
+
+}; // class Context
+
+} // namespace vte::sixel
diff --git a/src/sixel-test.cc b/src/sixel-test.cc
index 05a9a206..d6d852d6 100644
--- a/src/sixel-test.cc
+++ b/src/sixel-test.cc
@@ -29,10 +29,12 @@
 #include <glib.h>
 
 #include "sixel-parser.hh"
+#include "sixel-context.hh"
 
 using namespace std::literals;
 
 using Command = vte::sixel::Command;
+using Context = vte::sixel::Context;
 using Mode = vte::sixel::Parser::Mode;
 using ParseStatus = vte::sixel::Parser::ParseStatus;
 
@@ -991,6 +993,545 @@ test_parser_controls_c1(void)
         }
 }
 
+// Context tests
+
+class TestContext: public Context {
+public:
+        using base_type = Context;
+        using base_type::base_type;
+
+        auto parse(std::string_view const& str)
+        {
+                auto const beginptr = reinterpret_cast<uint8_t const*>(str.data());
+                auto const endptr = reinterpret_cast<uint8_t const*>(beginptr + str.size());
+                return Context::parse(beginptr, endptr, true);
+        }
+
+}; // class TestContext
+
+template<class C>
+static void
+parse_image(C& context,
+            std::string_view const& str,
+            Context::color_t fg,
+            Context::color_t bg,
+            bool private_color_registers = true,
+            int line = __builtin_LINE())
+{
+        context.reset();
+        context.prepare(0x50 /* C0 DCS */, fg, bg, private_color_registers);
+
+        auto str_st = std::string{str};
+        str_st.append(ST(StType::C0));
+        auto [status, ip] = context.parse(str_st);
+        g_assert_cmpint(int(status), ==, int(ParseStatus::COMPLETE));
+}
+
+template<class C>
+static void
+parse_image(C& context,
+            ItemList const& items,
+            Context::color_t fg,
+            Context::color_t bg,
+            bool private_color_registers = true,
+            int line = __builtin_LINE())
+{
+        parse_image(context, ItemStringifier(items).string(), fg, bg, private_color_registers, line);
+}
+
+template<class C>
+static void
+parse_image(C& context,
+            std::string_view const& str,
+            int line = __builtin_LINE())
+{
+        parse_image(context, str, 0xffffffffu, 0xff000000u, true, line);
+}
+
+template<class C>
+static void
+parse_image(C& context,
+            ItemList const& items,
+            int line = __builtin_LINE())
+{
+        parse_image(context, ItemStringifier{items, Mode::UTF8}.string_view(), line);
+}
+
+template<class C>
+static auto
+parse_pixels(C& context,
+             std::string_view const& str,
+             unsigned extra_width_stride = 0,
+             int line = __builtin_LINE())
+{
+        parse_image(context, str, line);
+        auto size = size_t{};
+        auto ptr = vte::glib::take_free_ptr(context.image_data_indexed(&size, extra_width_stride));
+        return std::pair{std::move(ptr), size};
+}
+
+/* BEGIN */
+
+/* The following code is copied from xterm/graphics.c where it is under the
+ * licence below; and modified and used here under the GNU Lesser General Public
+ * Licence, version 3 (or, at your option), any later version.
+ */
+
+/*
+ * Copyright 2013-2019,2020 by Ross Combs
+ * Copyright 2013-2019,2020 by Thomas E. Dickey
+ *
+ *                         All Rights Reserved
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE ABOVE LISTED COPYRIGHT HOLDER(S) BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * Except as contained in this notice, the name(s) of the above copyright
+ * holders shall not be used in advertising or otherwise to promote the
+ * sale, use or other dealings in this Software without prior written
+ * authorization.
+ */
+
+static void
+hls2rgb_double(int
+               h,
+               int l,
+               int s,
+               int* r,
+               int* g,
+               int* b) noexcept
+{
+    const int hs = ((h + 240) / 60) % 6;
+    const double lv = l / 100.0;
+    const double sv = s / 100.0;
+    double c, x, m, c2;
+    double r1, g1, b1;
+
+    if (s == 0) {
+            *r = *g = *b = (short) (lv * 255. + 0.5);
+        return;
+    }
+
+    c2 = (2.0 * lv) - 1.0;
+    if (c2 < 0.0)
+        c2 = -c2;
+    c = (1.0 - c2) * sv;
+    x = (hs & 1) ? c : 0.0;
+    m = lv - 0.5 * c;
+
+    switch (hs) {
+    case 0:
+        r1 = c;
+        g1 = x;
+        b1 = 0.0;
+        break;
+    case 1:
+        r1 = x;
+        g1 = c;
+        b1 = 0.0;
+        break;
+    case 2:
+        r1 = 0.0;
+        g1 = c;
+        b1 = x;
+        break;
+    case 3:
+        r1 = 0.0;
+        g1 = x;
+        b1 = c;
+        break;
+    case 4:
+        r1 = x;
+        g1 = 0.0;
+        b1 = c;
+        break;
+    case 5:
+        r1 = c;
+        g1 = 0.0;
+        b1 = x;
+        break;
+    default:
+        *r = (short) 255;
+        *g = (short) 255;
+        *b = (short) 255;
+        return;
+    }
+
+    *r = (short) ((r1 + m) * 255.0 + 0.5);
+    *g = (short) ((g1 + m) * 255.0 + 0.5);
+    *b = (short) ((b1 + m) * 255.0 + 0.5);
+
+    if (*r < 0)
+        *r = 0;
+    else if (*r > 255)
+        *r = 255;
+    if (*g < 0)
+        *g = 0;
+    else if (*g > 255)
+        *g = 255;
+    if (*b < 0)
+        *b = 0;
+    else if (*b > 255)
+        *b = 255;
+}
+
+/* This is essentially Context::make_color_hls from sixel-context.cc,
+ * only changed to return the colour components separately.
+ */
+static void
+hls2rgb_int(int h,
+            int l,
+            int s,
+            int* r,
+            int* g,
+            int* b) noexcept
+{
+        auto const c2p = std::abs(2 * l - 100);
+        auto const cp = ((100 - c2p) * s) << 1;
+        auto const hs = ((h + 240) / 60) % 6;
+        auto const xp = (hs & 1) ? cp : 0;
+        auto const mp = 200 * l - (cp >> 1);
+
+        int r1p, g1p, b1p;
+        switch (hs) {
+        case 0:
+                r1p = cp;
+                g1p = xp;
+                b1p = 0;
+                break;
+        case 1:
+                r1p = xp;
+                g1p = cp;
+                b1p = 0;
+                break;
+        case 2:
+                r1p = 0;
+                g1p = cp;
+                b1p = xp;
+                break;
+        case 3:
+                r1p = 0;
+                g1p = xp;
+                b1p = cp;
+                break;
+        case 4:
+                r1p = xp;
+                g1p = 0;
+                b1p = cp;
+                break;
+        case 5:
+                r1p = cp;
+                g1p = 0;
+                b1p = xp;
+                break;
+        default:
+                __builtin_unreachable();
+        }
+
+        *r = ((r1p + mp) * 255 + 10000) / 20000;
+        *g = ((g1p + mp) * 255 + 10000) / 20000;
+        *b = ((b1p + mp) * 255 + 10000) / 20000;
+}
+
+/* END */
+
+static void
+test_context_color_hls(void)
+{
+        /* Test that our HLS colour conversion gives the right results
+         * by comparing it against the xterm/libsixel implementation.
+         *
+         * The values may differ by 1, which happen only for (L, S) in
+         * {(5, 100), (40, 75), (50, 80), (60, 75), (75, 60), (95, 100)}.
+         * There, one or more of the R, G, B components' unscaled values,
+         * times 255, produces an exact fraction of .5 in hsl2rgb_double,
+         * which, plus 0.5,, and due to inexactness, result in the truncated
+         * value "(short)v" being one less than the result of the integer
+         * computation.
+         */
+
+        for (auto h = 0; h <= 360; ++h) {
+                for (auto l = 0; l <= 100; ++l) {
+                        for (auto s = 0; s <= 100; ++s) {
+                                int rd, gd, bd, ri, gi, bi;
+
+                                hls2rgb_double(h, l, s, &rd, &gd, &bd);
+                                hls2rgb_int(h, l, s, &ri, &gi, &bi);
+
+                                g_assert_true((rd == ri || (rd + 1) == ri) &&
+                                              (gd == gi || (gd + 1) == gi) &&
+                                              (bd == bi || (bd + 1) == bi));
+                        }
+                }
+        }
+}
+
+template<class C>
+static void
+assert_image_dimensions(C& context,
+                        unsigned width,
+                        unsigned height,
+                        int line = __builtin_LINE())
+{
+        g_assert_cmpuint(context.image_width(), ==, width);
+        g_assert_cmpuint(context.image_height(), ==, height);
+}
+
+static void
+test_context_raster_attributes(void)
+{
+        /* Test that DECGRA sets the image dimensions */
+
+        auto context = TestContext{};
+        parse_image(context, "\"0;0;64;128"sv);
+        assert_image_dimensions(context, 64, 128);
+}
+
+static void
+test_context_repeat(void)
+{
+        /* Test that DECGRI repetition works */
+
+        auto context = TestContext{};
+        auto [pixels, size] = parse_pixels(context, "#1!5@"sv);
+        assert_image_dimensions(context, 5, 1);
+
+        auto data = pixels.get();
+        auto const v = *data++;
+        for (auto x = 1u; x < context.image_width(); ++x)
+                g_assert_cmpuint(*data++, ==, v);
+
+        g_assert_cmpuint(size_t(data - pixels.get()), <=, size);
+}
+
+static void
+test_context_scanlines_grow(void)
+{
+        /* Test that scanlines grow on demand */
+
+        auto context = TestContext{};
+        parse_image(context, "@$AA$?$??~-~"sv);
+        assert_image_dimensions(context, 3, 12);
+}
+
+static void
+test_context_scanlines_underfull(void)
+{
+        /* Test that the image height is determined by the last set sixel, not
+         * necessarily the number of scanlines.
+         */
+
+        auto context = TestContext{};
+
+        parse_image(context, "?"sv);
+        assert_image_dimensions(context, 1, 0);
+
+        for (auto n = 0; n < 6; ++n) {
+                parse_image(context, {Sixel(1u << n)});
+                assert_image_dimensions(context, 1, n + 1);
+
+                parse_image(context, {Sixel(0), Sixel(0), DECGNL(), Sixel(1u << n)});
+                assert_image_dimensions(context, 2, 6 + n + 1);
+        }
+}
+
+static void
+test_context_scanlines_max_width(void)
+{
+        /* Test that scanlines up to max_width() work, and scanlines longer than that
+         * are accepted but do not write outside the maximum width.
+         */
+
+        auto context = TestContext{};
+
+        parse_image(context, {Sixel(1u << 0), DECGNL(), DECGRI(context.max_width() - 1), Sixel(0x3f)});
+        assert_image_dimensions(context, context.max_width() - 1, 12);
+
+        parse_image(context, {Sixel(1u << 0), DECGNL(), DECGRI(context.max_width()), Sixel(0x3f)});
+        assert_image_dimensions(context, context.max_width(), 12);
+
+        parse_image(context, {Sixel(1u << 0), DECGNL(), DECGRI(context.max_width() + 1), Sixel(0x3f)});
+        assert_image_dimensions(context, context.max_width(), 12);
+}
+
+static void
+test_context_scanlines_max_height(void)
+{
+        /* Test that scanlines up to max_height() work, and scanlines beyond that
+         * are accepted but do nothing.
+         */
+
+        auto context = TestContext{};
+
+        auto items = ItemList{};
+        for (auto n = 0u; n < (context.max_height() / 6 - 1); ++n) {
+                if (n > 0)
+                        items.emplace_back(DECGNL());
+                items.emplace_back(Sixel(1u << 5));
+        }
+
+        parse_image(context, items);
+        assert_image_dimensions(context, 1, context.max_height() - 6);
+
+        items.emplace_back(DECGNL());
+        items.emplace_back(Sixel(1u << 4));
+
+        parse_image(context, items);
+        assert_image_dimensions(context, 1, context.max_height() - 1);
+
+        items.emplace_back(DECGCR());
+        items.emplace_back(Sixel(1u << 5));
+
+        parse_image(context, items);
+        assert_image_dimensions(context, 1, context.max_height());
+
+        /* Image cannot grow further */
+
+        items.emplace_back(DECGNL());
+        items.emplace_back(Sixel(1u << 0));
+
+        parse_image(context, items);
+        assert_image_dimensions(context, 1, context.max_height());
+
+        items.emplace_back(DECGNL());
+        items.emplace_back(Sixel(1u << 5));
+
+        parse_image(context, items);
+        assert_image_dimensions(context, 1, context.max_height());
+}
+
+static void
+test_context_image_stride(void)
+{
+        /* Test that data in the stride padding is set to background */
+
+        auto context = TestContext{};
+
+        auto const extra_stride = 3u;
+        auto [pixels, size] = parse_pixels(context, "#1~~-~~"sv, extra_stride);
+        assert_image_dimensions(context, 2, 12);
+
+        auto data = pixels.get();
+        auto const reg = 1 + 1; /* Colour registers start at 1 */
+
+        for (auto y = 0u; y < context.image_height(); ++y) {
+                for (auto x = 0u; x < context.image_width(); ++x)
+                        g_assert_cmpuint(*data++, ==, unsigned(reg));
+                for (auto e = 0u; e < extra_stride; ++e)
+                        g_assert_cmpuint(*data++, ==, 0);
+        }
+
+        g_assert_cmpuint(size_t(data - pixels.get()), <=, size);
+}
+
+class RGB {
+public:
+        uint8_t r{0};
+        uint8_t g{0};
+        uint8_t b{0};
+
+        RGB() = default;
+        ~RGB() = default;
+
+        RGB(int rv, int gv, int bv)
+                : r(rv), g(gv), b(bv)
+        {
+        }
+};
+
+static void
+test_context_image_palette(void)
+{
+        /* Test that the colour palette is recognised, and that colour registers
+         * wrap around.
+         */
+
+        auto make_color_rgb = [](unsigned rp,
+                                 unsigned gp,
+                                 unsigned bp) constexpr noexcept -> auto
+        {
+                auto scale = [](unsigned value) constexpr noexcept -> auto
+                {
+                        return (value * 255u + 50u) / 100u;
+                };
+
+                auto make_color = [](unsigned r,
+                                     unsigned g,
+                                     unsigned b) constexpr noexcept -> Context::color_t
+                {
+                        if constexpr (std::endian::native == std::endian::little) {
+                                        return b | g << 8 | r << 16 | 0xffu << 24 /* opaque */;
+                                } else if constexpr (std::endian::native == std::endian::big) {
+                                        return 0xffu /* opaque */ | r << 8 | g << 16 | b << 24;
+                                } else {
+                                __builtin_unreachable();
+                        }
+                };
+
+                return make_color(scale(rp), scale(gp), scale(bp));
+        };
+
+        auto context = TestContext{};
+
+        std::array<RGB, context.num_colors()> palette;
+        for (auto& p : palette) {
+                p = RGB(g_test_rand_int_range(0, 100),
+                        g_test_rand_int_range(0, 100),
+                        g_test_rand_int_range(0, 100));
+        }
+
+        auto items = ItemList{};
+        auto reg = context.num_colors();
+        for (auto const& p : palette) {
+                items.emplace_back(DECGCI_RGB(reg++, p.r, p.g, p.b));
+        }
+
+        parse_image(context, items);
+
+        for (auto n = 0; n < context.num_colors(); ++n) {
+                g_assert_cmpuint(make_color_rgb(palette[n].r, palette[n].g, palette[n].b),
+                                 ==,
+                                 context.color(n + 1));
+        }
+}
+
+static void
+test_context_image_compositing(void)
+{
+        /* Test that multiple sixels in different colours are composited. */
+
+        auto context = TestContext{};
+
+        auto [pixels, size] = parse_pixels(context,
+                                           "#256!24F$#257!24w-#258!24F$#259!24w-#260!24F$#261!24w"sv);
+
+        auto data = pixels.get();
+        for (auto y = 0u; y < context.image_height(); ++y) {
+                auto const reg = (256 + y / 3) + 1; /* registers start at 1 */
+                for (auto x = 0u; x < context.image_width(); ++x)
+                        g_assert_cmpuint(*data++, ==, reg);
+        }
+
+
+        g_assert_cmpuint(size_t(data - pixels.get()), <=, size);
+}
+
 // Main
 
 int
@@ -1011,6 +1552,16 @@ main(int argc,
         g_test_add_func("/vte/sixel/parser/controls/c0/ignored", test_parser_controls_c0_ignored);
         g_test_add_func("/vte/sixel/parser/controls/del", test_parser_controls_del);
         g_test_add_func("/vte/sixel/parser/controls/c1", test_parser_controls_c1);
+        g_test_add_func("/vte/sixel/context/color/hls", test_context_color_hls);
+        g_test_add_func("/vte/sixel/context/raster-attributes", test_context_raster_attributes);
+        g_test_add_func("/vte/sixel/context/repeat", test_context_repeat);
+        g_test_add_func("/vte/sixel/context/scanlines/grow", test_context_scanlines_grow);
+        g_test_add_func("/vte/sixel/context/scanlines/underfull", test_context_scanlines_underfull);
+        g_test_add_func("/vte/sixel/context/scanlines/max-width", test_context_scanlines_max_width);
+        g_test_add_func("/vte/sixel/context/scanlines/max-height", test_context_scanlines_max_height);
+        g_test_add_func("/vte/sixel/context/image/stride", test_context_image_stride);
+        g_test_add_func("/vte/sixel/context/image/palette", test_context_image_palette);
+        g_test_add_func("/vte/sixel/context/image/compositing", test_context_image_compositing);
 
         return g_test_run();
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]