[vte] lib: Add new SIXEL context and test
- From: Christian Persch <chpe src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [vte] lib: Add new SIXEL context and test
- Date: Sun, 18 Oct 2020 22:17:08 +0000 (UTC)
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]