summaryrefslogtreecommitdiff
path: root/src/sixel-context.cc
diff options
context:
space:
mode:
authorChristian Persch <chpe@src.gnome.org>2020-10-19 00:16:36 +0200
committerChristian Persch <chpe@src.gnome.org>2020-10-19 00:16:36 +0200
commit93d10c5db61cc286373e7fa75c8856a411f5d469 (patch)
tree72f0786adddda5192a09f1879b4dd92d06730da1 /src/sixel-context.cc
parentd3b562e1156c97199542d3243fc21153546d26d7 (diff)
downloadvte-93d10c5db61cc286373e7fa75c8856a411f5d469.tar.gz
lib: Add new SIXEL context and test
Diffstat (limited to 'src/sixel-context.cc')
-rw-r--r--src/sixel-context.cc505
1 files changed, 505 insertions, 0 deletions
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