/*
* 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 library. If not, see .
*/
#include "config.h"
#include "sixel-context.hh"
#include
#include
#include
#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
{
/* Keep buffer of default size */
if (m_scanlines_data_capacity > minimum_capacity()) {
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(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 + 2] = make_color_rgb( 0, 0, 0); /* HLS( 0, 0, 0) */ /* Black */
m_colors[1 + 2] = make_color_rgb(20, 20, 80); /* HLS( 0, 50, 60) */ /* Blue */
m_colors[2 + 2] = make_color_rgb(80, 13, 13); /* HLS(120, 46, 72) */ /* Red */
m_colors[3 + 2] = make_color_rgb(20, 80, 20); /* HLS(240, 50, 60) */ /* Green */
m_colors[4 + 2] = make_color_rgb(80, 20, 80); /* HLS( 60, 50, 60) */ /* Magenta */
m_colors[5 + 2] = make_color_rgb(20, 80, 80); /* HLS(300, 50, 60) */ /* Cyan */
m_colors[6 + 2] = make_color_rgb(80, 80, 20); /* HLS(180, 50, 60) */ /* Yellow */
m_colors[7 + 2] = make_color_rgb(53, 53, 53); /* HLS( 0, 53, 0) */ /* Grey 50% */
m_colors[8 + 2] = make_color_rgb(26, 26, 26); /* HLS( 0, 26, 0) */ /* Grey 25% */
m_colors[9 + 2] = make_color_rgb(33, 33, 60); /* HLS( 0, 46, 29) */ /* Blue* */
m_colors[10 + 2] = make_color_rgb(60, 26, 26); /* HLS(120, 43, 39) */ /* Red* */
m_colors[11 + 2] = make_color_rgb(33, 60, 33); /* HLS(240, 46, 29) */ /* Green* */
m_colors[12 + 2] = make_color_rgb(60, 33, 60); /* HLS( 60, 46, 29) */ /* Magenta* */
m_colors[13 + 2] = make_color_rgb(33, 60, 60); /* HLS(300, 46, 29) */ /* Cyan* */
m_colors[14 + 2] = make_color_rgb(60, 60, 33); /* HLS(180, 46, 29) */ /* Yellow* */
m_colors[15 + 2] = 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 + 2] = 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 + 2] = make_color(8 + n * 10, 8 + n * 10, 8 + n * 10);
/* Set all other colours to black */
for (auto n = 256 + 2; n < k_num_colors + 2; ++n)
m_colors[n] = make_color(0, 0, 0);
}
void
Context::prepare(uint32_t introducer,
unsigned fg_red,
unsigned fg_green,
unsigned fg_blue,
unsigned bg_red,
unsigned bg_green,
unsigned bg_blue,
bool bg_transparent,
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();
if (bg_transparent)
m_colors[0] = 0u; /* fully transparent */
else
m_colors[0] = make_color(bg_red, bg_green, bg_blue);
m_colors[1] = make_color(fg_red, fg_green, fg_blue);
/*
* DEC PPLV2 says that on entering DECSIXEL mode, the active colour
* is set to colour register 0. Xterm defaults to register 3.
* We use the current foreground color in our special register 1.
*/
set_current_color(1);
/* Clear buffer 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
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(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.
*/
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();
m_scanlines_data_capacity = 0;
}
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(size,
(image_width() + extra_width_stride) * sizeof(color_index_t),
[](color_index_t pen) constexpr noexcept -> color_index_t { return pen; });
}
#ifdef VTE_COMPILATION
uint8_t*
Context::image_data() noexcept
{
return reinterpret_cast(image_data(nullptr,
cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, image_width()),
[&](color_index_t pen) constexpr noexcept -> color_t { return m_colors[pen]; }));
}
vte::Freeable
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::take_freeable(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