summaryrefslogtreecommitdiff
path: root/graphics.c
diff options
context:
space:
mode:
Diffstat (limited to 'graphics.c')
-rw-r--r--graphics.c4063
1 files changed, 4063 insertions, 0 deletions
diff --git a/graphics.c b/graphics.c
new file mode 100644
index 0000000..98e20f4
--- /dev/null
+++ b/graphics.c
@@ -0,0 +1,4063 @@
+/* The MIT License
+
+ Copyright (c) 2021-2024 Sergei Grechanik <sergei.grechanik@gmail.com>
+
+ 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 AUTHORS OR COPYRIGHT HOLDERS
+ 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.
+*/
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// This file implements a subset of the kitty graphics protocol.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// A workaround for mac os to enable mkdtemp.
+#ifdef __APPLE__
+#define _DARWIN_C_SOURCE
+#endif
+
+#define _POSIX_C_SOURCE 200809L
+
+#include <zlib.h>
+#include <Imlib2.h>
+#include <X11/Xlib.h>
+#include <X11/extensions/Xrender.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <fcntl.h>
+#include <spawn.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "khash.h"
+#include "kvec.h"
+
+#include "st.h"
+#include "graphics.h"
+
+extern char **environ;
+
+#define MAX_FILENAME_SIZE 256
+#define MAX_INFO_LEN 256
+#define MAX_IMAGE_RECTS 20
+
+/// The type used in this file to represent time. Used both for time differences
+/// and absolute times (as milliseconds since an arbitrary point in time, see
+/// `initialization_time`).
+typedef int64_t Milliseconds;
+
+enum ScaleMode {
+ SCALE_MODE_UNSET = 0,
+ /// Stretch or shrink the image to fill the box, ignoring aspect ratio.
+ SCALE_MODE_FILL = 1,
+ /// Preserve aspect ratio and fit to width or to height so that the
+ /// whole image is visible.
+ SCALE_MODE_CONTAIN = 2,
+ /// Do not scale. The image may be cropped if the box is too small.
+ SCALE_MODE_NONE = 3,
+ /// Do not scale, unless the box is too small, in which case the image
+ /// will be shrunk like with `SCALE_MODE_CONTAIN`.
+ SCALE_MODE_NONE_OR_CONTAIN = 4,
+};
+
+enum AnimationState {
+ ANIMATION_STATE_UNSET = 0,
+ /// The animation is stopped. Display the current frame, but don't
+ /// advance to the next one.
+ ANIMATION_STATE_STOPPED = 1,
+ /// Run the animation to then end, then wait for the next frame.
+ ANIMATION_STATE_LOADING = 2,
+ /// Run the animation in a loop.
+ ANIMATION_STATE_LOOPING = 3,
+};
+
+/// The status of an image. Each image uploaded to the terminal is cached on
+/// disk, then it is loaded to ram when needed.
+enum ImageStatus {
+ STATUS_UNINITIALIZED = 0,
+ STATUS_UPLOADING = 1,
+ STATUS_UPLOADING_ERROR = 2,
+ STATUS_UPLOADING_SUCCESS = 3,
+ STATUS_RAM_LOADING_ERROR = 4,
+ STATUS_RAM_LOADING_IN_PROGRESS = 5,
+ STATUS_RAM_LOADING_SUCCESS = 6,
+};
+
+const char *image_status_strings[6] = {
+ "STATUS_UNINITIALIZED",
+ "STATUS_UPLOADING",
+ "STATUS_UPLOADING_ERROR",
+ "STATUS_UPLOADING_SUCCESS",
+ "STATUS_RAM_LOADING_ERROR",
+ "STATUS_RAM_LOADING_SUCCESS",
+};
+
+enum ImageUploadingFailure {
+ ERROR_OVER_SIZE_LIMIT = 1,
+ ERROR_CANNOT_OPEN_CACHED_FILE = 2,
+ ERROR_UNEXPECTED_SIZE = 3,
+ ERROR_CANNOT_COPY_FILE = 4,
+ ERROR_CANNOT_OPEN_SHM = 5,
+};
+
+const char *image_uploading_failure_strings[6] = {
+ "NO_ERROR",
+ "ERROR_OVER_SIZE_LIMIT",
+ "ERROR_CANNOT_OPEN_CACHED_FILE",
+ "ERROR_UNEXPECTED_SIZE",
+ "ERROR_CANNOT_COPY_FILE",
+ "ERROR_CANNOT_OPEN_SHM",
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// We use the following structures to represent images and placements:
+//
+// - Image: this is the main structure representing an image, usually created
+// by actions 'a=t', 'a=T`. Each image has an id (image id aka client id,
+// specified by 'i='). An image may have multiple frames (ImageFrame) and
+// placements (ImagePlacement).
+//
+// - ImageFrame: represents a single frame of an image, usually created by
+// the action 'a=f' (and the first frame is created with the image itself).
+// Each frame has an index and also:
+// - a file containing the frame data (considered to be "on disk", although
+// it's probably in tmpfs),
+// - an imlib object containing the fully composed frame (i.e. the frame
+// data from the file composed onto the background frame or color). It is
+// not ready for display yet, because it needs to be scaled and uploaded
+// to the X server.
+//
+// - ImagePlacement: represents a placement of an image, created by 'a=p' and
+// 'a=T'. Each placement has an id (placement id, specified by 'p='). Also
+// each placement has an array of pixmaps: one for each frame of the image.
+// Each pixmap is a scaled and uploaded image ready to be displayed.
+//
+// Images are store in the `images` hash table, mapping image ids to Image
+// objects (allocated on the heap).
+//
+// Placements are stored in the `placements` hash table of each Image object,
+// mapping placement ids to ImagePlacement objects (also allocated on the heap).
+//
+// ImageFrames are stored in the `first_frame` field and in the
+// `frames_beyond_the_first` array of each Image object. They are stored by
+// value, so ImageFrame pointer may be invalidated when frames are
+// added/deleted, be careful.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+struct Image;
+struct ImageFrame;
+struct ImagePlacement;
+
+KHASH_MAP_INIT_INT(id2image, struct Image *)
+KHASH_MAP_INIT_INT(id2placement, struct ImagePlacement *)
+
+typedef struct ImageFrame {
+ /// The image this frame belongs to.
+ struct Image *image;
+ /// The 1-based index of the frame. Zero if the frame isn't initialized.
+ int index;
+ /// The last time when the frame was displayed or otherwise touched.
+ Milliseconds atime;
+ /// The background color of the frame in the 0xRRGGBBAA format.
+ uint32_t background_color;
+ /// The index of the background frame. Zero to use the color instead.
+ int background_frame_index;
+ /// The duration of the frame in milliseconds.
+ int gap;
+ /// The expected size of the frame image file (specified with 'S='),
+ /// used to check if uploading succeeded.
+ unsigned expected_size;
+ /// Format specification (see the `f=` key).
+ int format;
+ /// Pixel width and height of the non-composed (on-disk) frame data. May
+ /// differ from the image (i.e. first frame) dimensions.
+ int data_pix_width, data_pix_height;
+ /// The offset of the frame relative to the first frame.
+ int x, y;
+ /// Compression mode (see the `o=` key).
+ char compression;
+ /// The status (see `ImageStatus`).
+ char status;
+ /// The reason of uploading failure (see `ImageUploadingFailure`).
+ char uploading_failure;
+ /// Whether failures and successes should be reported ('q=').
+ char quiet;
+ /// Whether to blend the frame with the background or replace it.
+ char blend;
+ /// The file corresponding to the on-disk cache, used when uploading.
+ FILE *open_file;
+ /// The size of the corresponding file cached on disk.
+ unsigned disk_size;
+ /// The imlib object containing the fully composed frame. It's not
+ /// scaled for screen display yet.
+ Imlib_Image imlib_object;
+} ImageFrame;
+
+typedef struct Image {
+ /// The client id (the one specified with 'i='). Must be nonzero.
+ uint32_t image_id;
+ /// The client id specified in the query command (`a=q`). This one must
+ /// be used to create the response if it's non-zero.
+ uint32_t query_id;
+ /// The number specified in the transmission command (`I=`). If
+ /// non-zero, it may be used to identify the image instead of the
+ /// image_id, and it also should be mentioned in responses.
+ uint32_t image_number;
+ /// The last time when the image was displayed or otherwise touched.
+ Milliseconds atime;
+ /// The total duration of the animation in milliseconds.
+ int total_duration;
+ /// The total size of cached image files for all frames.
+ int total_disk_size;
+ /// The global index of the creation command. Used to decide which image
+ /// is newer if they have the same image number.
+ uint64_t global_command_index;
+ /// The 1-based index of the currently displayed frame.
+ int current_frame;
+ /// The state of the animation, see `AnimationState`.
+ char animation_state;
+ /// The absolute time that is assumed to be the start of the current
+ /// frame (in ms since initialization).
+ Milliseconds current_frame_time;
+ /// The absolute time of the last redraw (in ms since initialization).
+ /// Used to check whether it's the first time we draw the image in the
+ /// current redraw cycle.
+ Milliseconds last_redraw;
+ /// The absolute time of the next redraw (in ms since initialization).
+ /// 0 means no redraw is scheduled.
+ Milliseconds next_redraw;
+ /// The unscaled pixel width and height of the image. Usually inherited
+ /// from the first frame.
+ int pix_width, pix_height;
+ /// The first frame.
+ ImageFrame first_frame;
+ /// The array of frames beyond the first one.
+ kvec_t(ImageFrame) frames_beyond_the_first;
+ /// Image placements.
+ khash_t(id2placement) *placements;
+ /// The default placement.
+ uint32_t default_placement;
+ /// The initial placement id, specified with the transmission command,
+ /// used to report success or failure.
+ uint32_t initial_placement_id;
+} Image;
+
+typedef struct ImagePlacement {
+ /// The image this placement belongs to.
+ Image *image;
+ /// The id of the placement. Must be nonzero.
+ uint32_t placement_id;
+ /// The last time when the placement was displayed or otherwise touched.
+ Milliseconds atime;
+ /// The 1-based index of the protected pixmap. We protect a pixmap in
+ /// gr_load_pixmap to avoid unloading it right after it was loaded.
+ int protected_frame;
+ /// Whether the placement is used only for Unicode placeholders.
+ char virtual;
+ /// The scaling mode (see `ScaleMode`).
+ char scale_mode;
+ /// Height and width in cells.
+ uint16_t rows, cols;
+ /// Top-left corner of the source rectangle ('x=' and 'y=').
+ int src_pix_x, src_pix_y;
+ /// Height and width of the source rectangle (zero if full image).
+ int src_pix_width, src_pix_height;
+ /// The image appropriately scaled and uploaded to the X server. This
+ /// pixmap is premultiplied by alpha.
+ Pixmap first_pixmap;
+ /// The array of pixmaps beyond the first one.
+ kvec_t(Pixmap) pixmaps_beyond_the_first;
+ /// The dimensions of the cell used to scale the image. If cell
+ /// dimensions are changed (font change), the image will be rescaled.
+ uint16_t scaled_cw, scaled_ch;
+ /// If true, do not move the cursor when displaying this placement
+ /// (non-virtual placements only).
+ char do_not_move_cursor;
+ /// The text underneath this placement, valid only for classic
+ /// placements. On deletion, the text is restored. This is a malloced
+ /// array of rows*cols Glyphs.
+ Glyph *text_underneath;
+} ImagePlacement;
+
+/// A rectangular piece of an image to be drawn.
+typedef struct {
+ uint32_t image_id;
+ uint32_t placement_id;
+ /// The position of the rectangle in pixels.
+ int screen_x_pix, screen_y_pix;
+ /// The starting row on the screen.
+ int screen_y_row;
+ /// The part of the whole image to be drawn, in cells. Starts are
+ /// zero-based, ends are exclusive.
+ int img_start_col, img_end_col, img_start_row, img_end_row;
+ /// The current cell width and height in pixels.
+ int cw, ch;
+ /// Whether colors should be inverted.
+ int reverse;
+} ImageRect;
+
+/// Executes `code` for each frame of an image. Example:
+///
+/// foreach_frame(image, frame, {
+/// printf("Frame %d\n", frame->index);
+/// });
+///
+#define foreach_frame(image, framevar, code) { size_t __i; \
+ for (__i = 0; __i <= kv_size((image).frames_beyond_the_first); ++__i) { \
+ ImageFrame *framevar = \
+ __i == 0 ? &(image).first_frame \
+ : &kv_A((image).frames_beyond_the_first, __i - 1); \
+ code; \
+ } }
+
+/// Executes `code` for each pixmap of a placement. Example:
+///
+/// foreach_pixmap(placement, pixmap, {
+/// ...
+/// });
+///
+#define foreach_pixmap(placement, pixmapvar, code) { size_t __i; \
+ for (__i = 0; __i <= kv_size((placement).pixmaps_beyond_the_first); ++__i) { \
+ Pixmap pixmapvar = \
+ __i == 0 ? (placement).first_pixmap \
+ : kv_A((placement).pixmaps_beyond_the_first, __i - 1); \
+ code; \
+ } }
+
+
+static Image *gr_find_image(uint32_t image_id);
+static void gr_get_frame_filename(ImageFrame *frame, char *out, size_t max_len);
+static void gr_delete_image(Image *img);
+static void gr_erase_placement(ImagePlacement *placement);
+static void gr_check_limits();
+static char *gr_base64dec(const char *src, size_t *size);
+static void sanitize_str(char *str, size_t max_len);
+static const char *sanitized_filename(const char *str);
+
+/// The array of image rectangles to draw. It is reset each frame.
+static ImageRect image_rects[MAX_IMAGE_RECTS] = {{0}};
+/// The known images (including the ones being uploaded).
+static khash_t(id2image) *images = NULL;
+/// The total number of placements in all images.
+static unsigned total_placement_count = 0;
+/// The total size of all image files stored in the on-disk cache.
+static int64_t images_disk_size = 0;
+/// The total size of all images and placements loaded into ram.
+static int64_t images_ram_size = 0;
+/// The id of the last loaded image.
+static uint32_t last_image_id = 0;
+/// Current cell width and heigh in pixels.
+static int current_cw = 0, current_ch = 0;
+/// The id of the currently uploaded image (when using direct uploading).
+static uint32_t current_upload_image_id = 0;
+/// The index of the frame currently being uploaded.
+static int current_upload_frame_index = 0;
+/// The time when the graphics module was initialized.
+static struct timespec initialization_time = {0};
+/// The time when the current frame drawing started, used for debugging fps and
+/// to calculate the current frame for animations.
+static Milliseconds drawing_start_time;
+/// The global index of the current command.
+static uint64_t global_command_counter = 0;
+/// The next redraw times for each row of the terminal. Used for animations.
+/// 0 means no redraw is scheduled.
+static kvec_t(Milliseconds) next_redraw_times = {0, 0, NULL};
+/// The number of files loaded in the current redraw cycle or command execution.
+static int debug_loaded_files_counter = 0;
+/// The number of pixmaps loaded in the current redraw cycle or command execution.
+static int debug_loaded_pixmaps_counter = 0;
+
+/// The directory where the cache files are stored.
+static char cache_dir[MAX_FILENAME_SIZE - 16];
+
+/// The table used for color inversion.
+static unsigned char reverse_table[256];
+
+// Declared in the header.
+GraphicsDebugMode graphics_debug_mode = GRAPHICS_DEBUG_NONE;
+char graphics_display_images = 1;
+GraphicsCommandResult graphics_command_result = {0};
+int graphics_next_redraw_delay = INT_MAX;
+
+// Defined in config.h
+extern const char graphics_cache_dir_template[];
+extern unsigned graphics_max_single_image_file_size;
+extern unsigned graphics_total_file_cache_size;
+extern unsigned graphics_max_single_image_ram_size;
+extern unsigned graphics_max_total_ram_size;
+extern unsigned graphics_max_total_placements;
+extern double graphics_excess_tolerance_ratio;
+extern unsigned graphics_animation_min_delay;
+
+
+////////////////////////////////////////////////////////////////////////////////
+// Basic helpers.
+////////////////////////////////////////////////////////////////////////////////
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) < (b) ? (b) : (a))
+
+/// Returns the difference between `end` and `start` in milliseconds.
+static int64_t gr_timediff_ms(const struct timespec *end,
+ const struct timespec *start) {
+ return (end->tv_sec - start->tv_sec) * 1000 +
+ (end->tv_nsec - start->tv_nsec) / 1000000;
+}
+
+/// Returns the current time in milliseconds since the initialization.
+static Milliseconds gr_now_ms() {
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ return gr_timediff_ms(&now, &initialization_time);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Logging.
+////////////////////////////////////////////////////////////////////////////////
+
+#define GR_LOG(...) \
+ do { if(graphics_debug_mode) fprintf(stderr, __VA_ARGS__); } while(0)
+
+////////////////////////////////////////////////////////////////////////////////
+// Basic image management functions (create, delete, find, etc).
+////////////////////////////////////////////////////////////////////////////////
+
+/// Returns the 1-based index of the last frame. Note that you may want to use
+/// `gr_last_uploaded_frame_index` instead since the last frame may be not
+/// fully uploaded yet.
+static inline int gr_last_frame_index(Image *img) {
+ return kv_size(img->frames_beyond_the_first) + 1;
+}
+
+/// Returns the frame with the given index. Returns NULL if the index is out of
+/// bounds. The index is 1-based.
+static ImageFrame *gr_get_frame(Image *img, int index) {
+ if (!img)
+ return NULL;
+ if (index == 1)
+ return &img->first_frame;
+ if (2 <= index && index <= gr_last_frame_index(img))
+ return &kv_A(img->frames_beyond_the_first, index - 2);
+ return NULL;
+}
+
+/// Returns the last frame of the image. Returns NULL if `img` is NULL.
+static ImageFrame *gr_get_last_frame(Image *img) {
+ if (!img)
+ return NULL;
+ return gr_get_frame(img, gr_last_frame_index(img));
+}
+
+/// Returns the 1-based index of the last frame or the second-to-last frame if
+/// the last frame is not fully uploaded yet.
+static inline int gr_last_uploaded_frame_index(Image *img) {
+ int last_index = gr_last_frame_index(img);
+ if (last_index > 1 &&
+ gr_get_frame(img, last_index)->status < STATUS_UPLOADING_SUCCESS)
+ return last_index - 1;
+ return last_index;
+}
+
+/// Returns the pixmap for the frame with the given index. Returns 0 if the
+/// index is out of bounds. The index is 1-based.
+static Pixmap gr_get_frame_pixmap(ImagePlacement *placement, int index) {
+ if (index == 1)
+ return placement->first_pixmap;
+ if (2 <= index &&
+ index <= kv_size(placement->pixmaps_beyond_the_first) + 1)
+ return kv_A(placement->pixmaps_beyond_the_first, index - 2);
+ return 0;
+}
+
+/// Sets the pixmap for the frame with the given index. The index is 1-based.
+/// The array of pixmaps is resized if needed.
+static void gr_set_frame_pixmap(ImagePlacement *placement, int index,
+ Pixmap pixmap) {
+ if (index == 1) {
+ placement->first_pixmap = pixmap;
+ return;
+ }
+ // Resize the array if needed.
+ size_t old_size = kv_size(placement->pixmaps_beyond_the_first);
+ if (old_size < index - 1) {
+ kv_a(Pixmap, placement->pixmaps_beyond_the_first, index - 2);
+ for (size_t i = old_size; i < index - 1; i++)
+ kv_A(placement->pixmaps_beyond_the_first, i) = 0;
+ }
+ kv_A(placement->pixmaps_beyond_the_first, index - 2) = pixmap;
+}
+
+/// Finds the image corresponding to the client id. Returns NULL if cannot find.
+static Image *gr_find_image(uint32_t image_id) {
+ khiter_t k = kh_get(id2image, images, image_id);
+ if (k == kh_end(images))
+ return NULL;
+ Image *res = kh_value(images, k);
+ return res;
+}
+
+/// Finds the newest image corresponding to the image number. Returns NULL if
+/// cannot find.
+static Image *gr_find_image_by_number(uint32_t image_number) {
+ if (image_number == 0)
+ return NULL;
+ Image *newest_img = NULL;
+ Image *img = NULL;
+ kh_foreach_value(images, img, {
+ if (img->image_number == image_number &&
+ (!newest_img || newest_img->global_command_index <
+ img->global_command_index))
+ newest_img = img;
+ });
+ if (!newest_img)
+ GR_LOG("Image number %u not found\n", image_number);
+ else
+ GR_LOG("Found image number %u, its id is %u\n", image_number,
+ img->image_id);
+ return newest_img;
+}
+
+/// Finds the placement corresponding to the id. If the placement id is 0,
+/// returns some default placement.
+static ImagePlacement *gr_find_placement(Image *img, uint32_t placement_id) {
+ if (!img)
+ return NULL;
+ if (placement_id == 0) {
+ // Try to get the default placement.
+ ImagePlacement *dflt = NULL;
+ if (img->default_placement != 0)
+ dflt = gr_find_placement(img, img->default_placement);
+ if (dflt)
+ return dflt;
+ // If there is no default placement, return the first one and
+ // set it as the default.
+ kh_foreach_value(img->placements, dflt, {
+ img->default_placement = dflt->placement_id;
+ return dflt;
+ });
+ // If there are no placements, return NULL.
+ return NULL;
+ }
+ khiter_t k = kh_get(id2placement, img->placements, placement_id);
+ if (k == kh_end(img->placements))
+ return NULL;
+ ImagePlacement *res = kh_value(img->placements, k);
+ return res;
+}
+
+/// Finds the placement by image id and placement id.
+static ImagePlacement *gr_find_image_and_placement(uint32_t image_id,
+ uint32_t placement_id) {
+ return gr_find_placement(gr_find_image(image_id), placement_id);
+}
+
+/// Returns a pointer to the glyph under the classic placement with `image_id`
+/// and `placement_id` at `col` and `row` (1-based). May return NULL if the
+/// underneath text is unknown.
+Glyph *gr_get_glyph_underneath_image(uint32_t image_id, uint32_t placement_id,
+ int col, int row) {
+ ImagePlacement *placement =
+ gr_find_image_and_placement(image_id, placement_id);
+ if (!placement || !placement->text_underneath)
+ return NULL;
+ col--;
+ row--;
+ if (col < 0 || col >= placement->cols || row < 0 ||
+ row >= placement->rows)
+ return NULL;
+ return &placement->text_underneath[row * placement->cols + col];
+}
+
+/// Writes the name of the on-disk cache file to `out`. `max_len` should be the
+/// size of `out`. The name will be something like
+/// "/tmp/st-images-xxx/img-ID-FRAME".
+static void gr_get_frame_filename(ImageFrame *frame, char *out,
+ size_t max_len) {
+ snprintf(out, max_len, "%s/img-%.3u-%.3u", cache_dir,
+ frame->image->image_id, frame->index);
+}
+
+/// Returns the (estimation) of the RAM size used by the frame right now.
+static unsigned gr_frame_current_ram_size(ImageFrame *frame) {
+ if (!frame->imlib_object)
+ return 0;
+ return (unsigned)frame->image->pix_width * frame->image->pix_height * 4;
+}
+
+/// Returns the (estimation) of the RAM size used by a single frame pixmap.
+static unsigned gr_placement_single_frame_ram_size(ImagePlacement *placement) {
+ return (unsigned)placement->rows * placement->cols *
+ placement->scaled_ch * placement->scaled_cw * 4;
+}
+
+/// Returns the (estimation) of the RAM size used by the placemenet right now.
+static unsigned gr_placement_current_ram_size(ImagePlacement *placement) {
+ unsigned single_frame_size =
+ gr_placement_single_frame_ram_size(placement);
+ unsigned result = 0;
+ foreach_pixmap(*placement, pixmap, {
+ if (pixmap)
+ result += single_frame_size;
+ });
+ return result;
+}
+
+/// Unload the frame from RAM (i.e. delete the corresponding imlib object).
+/// If the on-disk file of the frame is preserved, it can be reloaded later.
+static void gr_unload_frame(ImageFrame *frame) {
+ if (!frame->imlib_object)
+ return;
+
+ unsigned frame_ram_size = gr_frame_current_ram_size(frame);
+ images_ram_size -= frame_ram_size;
+
+ imlib_context_set_image(frame->imlib_object);
+ imlib_free_image_and_decache();
+ frame->imlib_object = NULL;
+
+ GR_LOG("After unloading image %u frame %u (atime %ld ms ago) "
+ "ram: %ld KiB (- %u KiB)\n",
+ frame->image->image_id, frame->index,
+ drawing_start_time - frame->atime, images_ram_size / 1024,
+ frame_ram_size / 1024);
+}
+
+/// Unload all frames of the image.
+static void gr_unload_all_frames(Image *img) {
+ foreach_frame(*img, frame, {
+ gr_unload_frame(frame);
+ });
+}
+
+/// Unload the placement from RAM (i.e. free all of the corresponding pixmaps).
+/// If the on-disk files or imlib objects of the corresponding image are
+/// preserved, the placement can be reloaded later.
+static void gr_unload_placement(ImagePlacement *placement) {
+ unsigned placement_ram_size = gr_placement_current_ram_size(placement);
+ images_ram_size -= placement_ram_size;
+
+ Display *disp = imlib_context_get_display();
+ foreach_pixmap(*placement, pixmap, {
+ if (pixmap)
+ XFreePixmap(disp, pixmap);
+ });
+
+ placement->first_pixmap = 0;
+ placement->pixmaps_beyond_the_first.n = 0;
+ placement->scaled_ch = placement->scaled_cw = 0;
+
+ GR_LOG("After unloading placement %u/%u (atime %ld ms ago) "
+ "ram: %ld KiB (- %u KiB)\n",
+ placement->image->image_id, placement->placement_id,
+ drawing_start_time - placement->atime, images_ram_size / 1024,
+ placement_ram_size / 1024);
+}
+
+/// Unload a single pixmap of the placement from RAM.
+static void gr_unload_pixmap(ImagePlacement *placement, int frameidx) {
+ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx);
+ if (!pixmap)
+ return;
+
+ Display *disp = imlib_context_get_display();
+ XFreePixmap(disp, pixmap);
+ gr_set_frame_pixmap(placement, frameidx, 0);
+ images_ram_size -= gr_placement_single_frame_ram_size(placement);
+
+ GR_LOG("After unloading pixmap %ld of "
+ "placement %u/%u (atime %ld ms ago) "
+ "frame %u (atime %ld ms ago) "
+ "ram: %ld KiB (- %u KiB)\n",
+ pixmap, placement->image->image_id, placement->placement_id,
+ drawing_start_time - placement->atime, frameidx,
+ drawing_start_time -
+ gr_get_frame(placement->image, frameidx)->atime,
+ images_ram_size / 1024,
+ gr_placement_single_frame_ram_size(placement) / 1024);
+}
+
+/// Deletes the on-disk cache file corresponding to the frame. The in-ram image
+/// object (if it exists) is not deleted, placements are not unloaded either.
+static void gr_delete_imagefile(ImageFrame *frame) {
+ // It may still be being loaded. Close the file in this case.
+ if (frame->open_file) {
+ fclose(frame->open_file);
+ frame->open_file = NULL;
+ }
+
+ if (frame->disk_size == 0)
+ return;
+
+ char filename[MAX_FILENAME_SIZE];
+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
+ remove(filename);
+
+ unsigned disk_size = frame->disk_size;
+ images_disk_size -= disk_size;
+ frame->image->total_disk_size -= disk_size;
+ frame->disk_size = 0;
+
+ GR_LOG("After deleting image file %u frame %u (atime %ld ms ago) "
+ "disk: %ld KiB (- %u KiB)\n",
+ frame->image->image_id, frame->index,
+ drawing_start_time - frame->atime, images_disk_size / 1024,
+ disk_size / 1024);
+}
+
+/// Deletes all on-disk cache files of the image (for each frame).
+static void gr_delete_imagefiles(Image *img) {
+ foreach_frame(*img, frame, {
+ gr_delete_imagefile(frame);
+ });
+}
+
+/// Deletes the given placement: unloads, frees the object, erases it from the
+/// screen in the classic case, but doesn't change the `placements` hash table.
+static void gr_delete_placement_keep_id(ImagePlacement *placement) {
+ if (!placement)
+ return;
+ GR_LOG("Deleting placement %u/%u\n", placement->image->image_id,
+ placement->placement_id);
+ // Erase the placement from the screen if it's classic and there is some
+ // saved text underneath.
+ if (placement->text_underneath && !placement->virtual)
+ gr_erase_placement(placement);
+ gr_unload_placement(placement);
+ kv_destroy(placement->pixmaps_beyond_the_first);
+ free(placement->text_underneath);
+ free(placement);
+ total_placement_count--;
+}
+
+/// Deletes all placements of `img`.
+static void gr_delete_all_placements(Image *img) {
+ ImagePlacement *placement = NULL;
+ kh_foreach_value(img->placements, placement, {
+ gr_delete_placement_keep_id(placement);
+ });
+ kh_clear(id2placement, img->placements);
+}
+
+/// Deletes the given image: unloads, deletes the file, frees the Image object,
+/// but doesn't change the `images` hash table.
+static void gr_delete_image_keep_id(Image *img) {
+ if (!img)
+ return;
+ GR_LOG("Deleting image %u\n", img->image_id);
+ foreach_frame(*img, frame, {
+ gr_delete_imagefile(frame);
+ gr_unload_frame(frame);
+ });
+ kv_destroy(img->frames_beyond_the_first);
+ gr_delete_all_placements(img);
+ kh_destroy(id2placement, img->placements);
+ free(img);
+}
+
+/// Deletes the given image: unloads, deletes the file, frees the Image object,
+/// and also removes it from `images`.
+static void gr_delete_image(Image *img) {
+ if (!img)
+ return;
+ uint32_t id = img->image_id;
+ gr_delete_image_keep_id(img);
+ khiter_t k = kh_get(id2image, images, id);
+ kh_del(id2image, images, k);
+}
+
+/// Deletes the given placement: unloads, frees the object, erases from the
+/// screen (in the classic case), and also removes it from `placements`.
+static void gr_delete_placement(ImagePlacement *placement) {
+ if (!placement)
+ return;
+ uint32_t id = placement->placement_id;
+ Image *img = placement->image;
+ gr_delete_placement_keep_id(placement);
+ khiter_t k = kh_get(id2placement, img->placements, id);
+ kh_del(id2placement, img->placements, k);
+}
+
+/// Deletes all images and clears `images`.
+static void gr_delete_all_images() {
+ Image *img = NULL;
+ kh_foreach_value(images, img, {
+ gr_delete_image_keep_id(img);
+ });
+ kh_clear(id2image, images);
+}
+
+/// Update the atime of the image.
+static void gr_touch_image(Image *img) {
+ img->atime = gr_now_ms();
+}
+
+/// Update the atime of the frame.
+static void gr_touch_frame(ImageFrame *frame) {
+ frame->image->atime = frame->atime = gr_now_ms();
+}
+
+/// Update the atime of the placement. Touches the images too.
+static void gr_touch_placement(ImagePlacement *placement) {
+ placement->image->atime = placement->atime = gr_now_ms();
+}
+
+/// Creates a new image with the given id. If an image with that id already
+/// exists, it is deleted first. If the provided id is 0, generates a
+/// random id.
+static Image *gr_new_image(uint32_t id) {
+ if (id == 0) {
+ do {
+ id = rand();
+ // Avoid IDs that don't need full 32 bits.
+ } while ((id & 0xFF000000) == 0 || (id & 0x00FFFF00) == 0 ||
+ gr_find_image(id));
+ GR_LOG("Generated random image id %u\n", id);
+ }
+ Image *img = gr_find_image(id);
+ gr_delete_image_keep_id(img);
+ GR_LOG("Creating image %u\n", id);
+ img = malloc(sizeof(Image));
+ memset(img, 0, sizeof(Image));
+ img->placements = kh_init(id2placement);
+ int ret;
+ khiter_t k = kh_put(id2image, images, id, &ret);
+ kh_value(images, k) = img;
+ img->image_id = id;
+ gr_touch_image(img);
+ img->global_command_index = global_command_counter;
+ return img;
+}
+
+/// Creates a new frame at the end of the frame array. It may be the first frame
+/// if there are no frames yet.
+static ImageFrame *gr_append_new_frame(Image *img) {
+ ImageFrame *frame = NULL;
+ if (img->first_frame.index == 0 &&
+ kv_size(img->frames_beyond_the_first) == 0) {
+ frame = &img->first_frame;
+ frame->index = 1;
+ } else {
+ frame = kv_pushp(ImageFrame, img->frames_beyond_the_first);
+ memset(frame, 0, sizeof(ImageFrame));
+ frame->index = kv_size(img->frames_beyond_the_first) + 1;
+ }
+ frame->image = img;
+ gr_touch_frame(frame);
+ GR_LOG("Appending frame %d to image %u\n", frame->index, img->image_id);
+ return frame;
+}
+
+/// Creates a new placement with the given id. If a placement with that id
+/// already exists, it is deleted first. If the provided id is 0, generates a
+/// random id.
+static ImagePlacement *gr_new_placement(Image *img, uint32_t id) {
+ if (id == 0) {
+ do {
+ // Currently we support only 24-bit IDs.
+ id = rand() & 0xFFFFFF;
+ // Avoid IDs that need only one byte.
+ } while ((id & 0x00FFFF00) == 0 || gr_find_placement(img, id));
+ }
+ ImagePlacement *placement = gr_find_placement(img, id);
+ gr_delete_placement_keep_id(placement);
+ GR_LOG("Creating placement %u/%u\n", img->image_id, id);
+ placement = malloc(sizeof(ImagePlacement));
+ memset(placement, 0, sizeof(ImagePlacement));
+ total_placement_count++;
+ int ret;
+ khiter_t k = kh_put(id2placement, img->placements, id, &ret);
+ kh_value(img->placements, k) = placement;
+ placement->image = img;
+ placement->placement_id = id;
+ gr_touch_placement(placement);
+ if (img->default_placement == 0)
+ img->default_placement = id;
+ return placement;
+}
+
+static int64_t ceil_div(int64_t a, int64_t b) {
+ return (a + b - 1) / b;
+}
+
+/// Computes the best number of rows and columns for a placement if it's not
+/// specified, and also adjusts the source rectangle size.
+static void gr_infer_placement_size_maybe(ImagePlacement *placement) {
+ // The size of the image.
+ int image_pix_width = placement->image->pix_width;
+ int image_pix_height = placement->image->pix_height;
+ // Negative values are not allowed. Quietly set them to 0.
+ if (placement->src_pix_x < 0)
+ placement->src_pix_x = 0;
+ if (placement->src_pix_y < 0)
+ placement->src_pix_y = 0;
+ if (placement->src_pix_width < 0)
+ placement->src_pix_width = 0;
+ if (placement->src_pix_height < 0)
+ placement->src_pix_height = 0;
+ // If the source rectangle is outside the image, truncate it.
+ if (placement->src_pix_x > image_pix_width)
+ placement->src_pix_x = image_pix_width;
+ if (placement->src_pix_y > image_pix_height)
+ placement->src_pix_y = image_pix_height;
+ // If the source rectangle is not specified, use the whole image. If
+ // it's partially outside the image, truncate it.
+ if (placement->src_pix_width == 0 ||
+ placement->src_pix_x + placement->src_pix_width > image_pix_width)
+ placement->src_pix_width =
+ image_pix_width - placement->src_pix_x;
+ if (placement->src_pix_height == 0 ||
+ placement->src_pix_y + placement->src_pix_height > image_pix_height)
+ placement->src_pix_height =
+ image_pix_height - placement->src_pix_y;
+
+ if (placement->cols != 0 && placement->rows != 0)
+ return;
+ if (placement->src_pix_width == 0 || placement->src_pix_height == 0)
+ return;
+ if (current_cw == 0 || current_ch == 0)
+ return;
+
+ // If no size is specified, use the image size.
+ if (placement->cols == 0 && placement->rows == 0) {
+ placement->cols =
+ ceil_div(placement->src_pix_width, current_cw);
+ placement->rows =
+ ceil_div(placement->src_pix_height, current_ch);
+ return;
+ }
+
+ // Some applications specify only one of the dimensions.
+ if (placement->scale_mode == SCALE_MODE_CONTAIN) {
+ // If we preserve aspect ratio and fit to width/height, the most
+ // logical thing is to find the minimum size of the
+ // non-specified dimension that allows the image to fit the
+ // specified dimension.
+ if (placement->cols == 0) {
+ placement->cols = ceil_div(
+ placement->src_pix_width * placement->rows *
+ current_ch,
+ placement->src_pix_height * current_cw);
+ return;
+ }
+ if (placement->rows == 0) {
+ placement->rows =
+ ceil_div(placement->src_pix_height *
+ placement->cols * current_cw,
+ placement->src_pix_width * current_ch);
+ return;
+ }
+ } else {
+ // Otherwise we stretch the image or preserve the original size.
+ // In both cases we compute the best number of columns from the
+ // pixel size and cell size.
+ // TODO: In the case of stretching it's not the most logical
+ // thing to do, may need to revisit in the future.
+ // Currently we switch to SCALE_MODE_CONTAIN when only one
+ // of the dimensions is specified, so this case shouldn't
+ // happen in practice.
+ if (!placement->cols)
+ placement->cols =
+ ceil_div(placement->src_pix_width, current_cw);
+ if (!placement->rows)
+ placement->rows =
+ ceil_div(placement->src_pix_height, current_ch);
+ }
+}
+
+/// Adjusts the current frame index if enough time has passed since the display
+/// of the current frame. Also computes the time of the next redraw of this
+/// image (`img->next_redraw`). The current time is passed as an argument so
+/// that all animations are in sync.
+static void gr_update_frame_index(Image *img, Milliseconds now) {
+ if (img->current_frame == 0) {
+ img->current_frame_time = now;
+ img->current_frame = 1;
+ img->next_redraw = now + MAX(1, img->first_frame.gap);
+ return;
+ }
+ // If the animation is stopped, show the current frame.
+ if (!img->animation_state ||
+ img->animation_state == ANIMATION_STATE_STOPPED ||
+ img->animation_state == ANIMATION_STATE_UNSET) {
+ // The next redraw is never (unless the state is changed).
+ img->next_redraw = 0;
+ return;
+ }
+ int last_uploaded_frame_index = gr_last_uploaded_frame_index(img);
+ // If we are loading and we reached the last frame, show the last frame.
+ if (img->animation_state == ANIMATION_STATE_LOADING &&
+ img->current_frame == last_uploaded_frame_index) {
+ // The next redraw is never (unless the state is changed or
+ // frames are added).
+ img->next_redraw = 0;
+ return;
+ }
+
+ // Check how many milliseconds passed since the current frame was shown.
+ int passed_ms = now - img->current_frame_time;
+ // If the animation is looping and too much time has passes, we can
+ // make a shortcut.
+ if (img->animation_state == ANIMATION_STATE_LOOPING &&
+ img->total_duration > 0 && passed_ms >= img->total_duration) {
+ passed_ms %= img->total_duration;
+ img->current_frame_time = now - passed_ms;
+ }
+ // Find the next frame.
+ int original_frame_index = img->current_frame;
+ while (1) {
+ ImageFrame *frame = gr_get_frame(img, img->current_frame);
+ if (!frame) {
+ // The frame doesn't exist, go to the first frame.
+ img->current_frame = 1;
+ img->current_frame_time = now;
+ img->next_redraw = now + MAX(1, img->first_frame.gap);
+ return;
+ }
+ if (frame->gap >= 0 && passed_ms < frame->gap) {
+ // Not enough time has passed, we are still in the same
+ // frame, and it's not a gapless frame.
+ img->next_redraw =
+ img->current_frame_time + MAX(1, frame->gap);
+ return;
+ }
+ // Otherwise go to the next frame.
+ passed_ms -= MAX(0, frame->gap);
+ if (img->current_frame >= last_uploaded_frame_index) {
+ // It's the last frame, if the animation is loading,
+ // remain on it.
+ if (img->animation_state == ANIMATION_STATE_LOADING) {
+ img->next_redraw = 0;
+ return;
+ }
+ // Otherwise the animation is looping.
+ img->current_frame = 1;
+ // TODO: Support finite number of loops.
+ } else {
+ img->current_frame++;
+ }
+ // Make sure we don't get stuck in an infinite loop.
+ if (img->current_frame == original_frame_index) {
+ // We looped through all frames, but haven't reached the
+ // next frame yet. This may happen if too much time has
+ // passed since the last redraw or all the frames are
+ // gapless. Just move on to the next frame.
+ img->current_frame++;
+ if (img->current_frame >
+ last_uploaded_frame_index)
+ img->current_frame = 1;
+ img->current_frame_time = now;
+ img->next_redraw = now + MAX(
+ 1, gr_get_frame(img, img->current_frame)->gap);
+ return;
+ }
+ // Adjust the start time of the frame. The next redraw time will
+ // be set in the next iteration.
+ img->current_frame_time += MAX(0, frame->gap);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Unloading and deleting images to save resources.
+////////////////////////////////////////////////////////////////////////////////
+
+/// A helper to compare frames by atime for qsort.
+static int gr_cmp_frames_by_atime(const void *a, const void *b) {
+ ImageFrame *frame_a = *(ImageFrame *const *)a;
+ ImageFrame *frame_b = *(ImageFrame *const *)b;
+ if (frame_a->atime == frame_b->atime)
+ return frame_a->image->global_command_index -
+ frame_b->image->global_command_index;
+ return frame_a->atime - frame_b->atime;
+}
+
+/// A helper to compare images by atime for qsort.
+static int gr_cmp_images_by_atime(const void *a, const void *b) {
+ Image *img_a = *(Image *const *)a;
+ Image *img_b = *(Image *const *)b;
+ if (img_a->atime == img_b->atime)
+ return img_a->global_command_index -
+ img_b->global_command_index;
+ return img_a->atime - img_b->atime;
+}
+
+/// A helper to compare placements by atime for qsort.
+static int gr_cmp_placements_by_atime(const void *a, const void *b) {
+ ImagePlacement *p_a = *(ImagePlacement **)a;
+ ImagePlacement *p_b = *(ImagePlacement **)b;
+ if (p_a->atime == p_b->atime)
+ return p_a->image->global_command_index -
+ p_b->image->global_command_index;
+ return p_a->atime - p_b->atime;
+}
+
+typedef kvec_t(Image *) ImageVec;
+typedef kvec_t(ImagePlacement *) ImagePlacementVec;
+typedef kvec_t(ImageFrame *) ImageFrameVec;
+
+/// Returns an array of pointers to all images sorted by atime.
+static ImageVec gr_get_images_sorted_by_atime() {
+ ImageVec vec;
+ kv_init(vec);
+ if (kh_size(images) == 0)
+ return vec;
+ kv_resize(Image *, vec, kh_size(images));
+ Image *img = NULL;
+ kh_foreach_value(images, img, { kv_push(Image *, vec, img); });
+ qsort(vec.a, kv_size(vec), sizeof(Image *), gr_cmp_images_by_atime);
+ return vec;
+}
+
+/// Returns an array of pointers to all placements sorted by atime.
+static ImagePlacementVec gr_get_placements_sorted_by_atime() {
+ ImagePlacementVec vec;
+ kv_init(vec);
+ if (total_placement_count == 0)
+ return vec;
+ kv_resize(ImagePlacement *, vec, total_placement_count);
+ Image *img = NULL;
+ ImagePlacement *placement = NULL;
+ kh_foreach_value(images, img, {
+ kh_foreach_value(img->placements, placement, {
+ kv_push(ImagePlacement *, vec, placement);
+ });
+ });
+ qsort(vec.a, kv_size(vec), sizeof(ImagePlacement *),
+ gr_cmp_placements_by_atime);
+ return vec;
+}
+
+/// Returns an array of pointers to all frames sorted by atime.
+static ImageFrameVec gr_get_frames_sorted_by_atime() {
+ ImageFrameVec frames;
+ kv_init(frames);
+ Image *img = NULL;
+ kh_foreach_value(images, img, {
+ foreach_frame(*img, frame, {
+ kv_push(ImageFrame *, frames, frame);
+ });
+ });
+ qsort(frames.a, kv_size(frames), sizeof(ImageFrame *),
+ gr_cmp_frames_by_atime);
+ return frames;
+}
+
+/// An object that can be unloaded from RAM.
+typedef struct {
+ /// Some score, probably based on access time. The lower the score, the
+ /// more likely that the object should be unloaded.
+ int64_t score;
+ union {
+ ImagePlacement *placement;
+ ImageFrame *frame;
+ };
+ /// If zero, the object is the imlib object of `frame`, if non-zero,
+ /// the object is a pixmap of `frameidx`-th frame of `placement`.
+ int frameidx;
+} UnloadableObject;
+
+typedef kvec_t(UnloadableObject) UnloadableObjectVec;
+
+/// A helper to compare unloadable objects by score for qsort.
+static int gr_cmp_unloadable_objects(const void *a, const void *b) {
+ UnloadableObject *obj_a = (UnloadableObject *)a;
+ UnloadableObject *obj_b = (UnloadableObject *)b;
+ return obj_a->score - obj_b->score;
+}
+
+/// Unloads an unloadable object from RAM.
+static void gr_unload_object(UnloadableObject *obj) {
+ if (obj->frameidx) {
+ if (obj->placement->protected_frame == obj->frameidx)
+ return;
+ gr_unload_pixmap(obj->placement, obj->frameidx);
+ } else {
+ gr_unload_frame(obj->frame);
+ }
+}
+
+/// Returns the recency threshold for an image. Frames that were accessed within
+/// this threshold from now are considered recent and may be handled
+/// differently because we may need them again very soon.
+static Milliseconds gr_recency_threshold(Image *img) {
+ return img->total_duration * 2 + 1000;
+}
+
+/// Creates an unloadable object for the imlib object of a frame.
+static UnloadableObject gr_unloadable_object_for_frame(Milliseconds now,
+ ImageFrame *frame) {
+ UnloadableObject obj = {0};
+ obj.frameidx = 0;
+ obj.frame = frame;
+ Milliseconds atime = frame->atime;
+ obj.score = atime;
+ if (atime >= now - gr_recency_threshold(frame->image)) {
+ // This is a recent frame, probably from an active animation.
+ // Score it above `now` to prefer unloading non-active frames.
+ // Randomize the score because it's not very clear in which
+ // order we want to unload them: reloading a frame may require
+ // reloading other frames.
+ obj.score = now + 1000 + rand() % 1000;
+ }
+ return obj;
+}
+
+/// Creates an unloadable object for a pixmap.
+static UnloadableObject
+gr_unloadable_object_for_pixmap(Milliseconds now, ImageFrame *frame,
+ ImagePlacement *placement) {
+ UnloadableObject obj = {0};
+ obj.frameidx = frame->index;
+ obj.placement = placement;
+ obj.score = placement->atime;
+ // Since we don't store pixmap atimes, use the
+ // oldest atime of the frame and the placement.
+ Milliseconds atime = MIN(placement->atime, frame->atime);
+ obj.score = atime;
+ if (atime >= now - gr_recency_threshold(frame->image)) {
+ // This is a recent pixmap, probably from an active animation.
+ // Score it above `now` to prefer unloading non-active frames.
+ // Also assign higher scores to frames that are closer to the
+ // current frame (more likely to be used soon).
+ int num_frames = gr_last_frame_index(frame->image);
+ int dist = frame->index - frame->image->current_frame;
+ if (dist < 0)
+ dist += num_frames;
+ obj.score =
+ now + 1000 + (num_frames - dist) * 1000 / num_frames;
+ // If the pixmap is much larger than the imlib image, prefer to
+ // unload the pixmap by adding up to -1000 to the score. If the
+ // imlib image is larger, add up to +1000.
+ float imlib_size = gr_frame_current_ram_size(frame);
+ float pixmap_size =
+ gr_placement_single_frame_ram_size(placement);
+ obj.score +=
+ 2000 * (imlib_size / (imlib_size + pixmap_size) - 0.5);
+ }
+ return obj;
+}
+
+/// Returns an array of unloadable objects sorted by score.
+static UnloadableObjectVec
+gr_get_unloadable_objects_sorted_by_score(Milliseconds now) {
+ UnloadableObjectVec objects;
+ kv_init(objects);
+ Image *img = NULL;
+ ImagePlacement *placement = NULL;
+ kh_foreach_value(images, img, {
+ foreach_frame(*img, frame, {
+ if (frame->imlib_object) {
+ kv_push(UnloadableObject, objects,
+ gr_unloadable_object_for_frame(now,
+ frame));
+ }
+ int frameidx = frame->index;
+ kh_foreach_value(img->placements, placement, {
+ if (!gr_get_frame_pixmap(placement, frameidx))
+ continue;
+ kv_push(UnloadableObject, objects,
+ gr_unloadable_object_for_pixmap(
+ now, frame, placement));
+ });
+ });
+ });
+ qsort(objects.a, kv_size(objects), sizeof(UnloadableObject),
+ gr_cmp_unloadable_objects);
+ return objects;
+}
+
+/// Returns the limit adjusted by the excess tolerance ratio.
+static inline unsigned apply_tolerance(unsigned limit) {
+ return limit + (unsigned)(limit * graphics_excess_tolerance_ratio);
+}
+
+/// Checks RAM and disk cache limits and deletes/unloads some images.
+static void gr_check_limits() {
+ Milliseconds now = gr_now_ms();
+ ImageVec images_sorted = {0};
+ ImagePlacementVec placements_sorted = {0};
+ ImageFrameVec frames_sorted = {0};
+ UnloadableObjectVec objects_sorted = {0};
+ int images_begin = 0;
+ int placements_begin = 0;
+ char changed = 0;
+ // First reduce the number of images if there are too many.
+ if (kh_size(images) > apply_tolerance(graphics_max_total_placements)) {
+ GR_LOG("Too many images: %d\n", kh_size(images));
+ changed = 1;
+ images_sorted = gr_get_images_sorted_by_atime();
+ int to_delete = kv_size(images_sorted) -
+ graphics_max_total_placements;
+ for (; images_begin < to_delete; images_begin++)
+ gr_delete_image(images_sorted.a[images_begin]);
+ }
+ // Then reduce the number of placements if there are too many.
+ if (total_placement_count >
+ apply_tolerance(graphics_max_total_placements)) {
+ GR_LOG("Too many placements: %d\n", total_placement_count);
+ changed = 1;
+ placements_sorted = gr_get_placements_sorted_by_atime();
+ int to_delete = kv_size(placements_sorted) -
+ graphics_max_total_placements;
+ for (; placements_begin < to_delete; placements_begin++) {
+ ImagePlacement *placement =
+ placements_sorted.a[placements_begin];
+ if (placement->protected_frame)
+ break;
+ gr_delete_placement(placement);
+ }
+ }
+ // Then reduce the size of the image file cache. The files correspond to
+ // image frames.
+ if (images_disk_size >
+ apply_tolerance(graphics_total_file_cache_size)) {
+ GR_LOG("Too big disk cache: %ld KiB\n",
+ images_disk_size / 1024);
+ changed = 1;
+ frames_sorted = gr_get_frames_sorted_by_atime();
+ for (int i = 0; i < kv_size(frames_sorted); i++) {
+ if (images_disk_size <= graphics_total_file_cache_size)
+ break;
+ gr_delete_imagefile(kv_A(frames_sorted, i));
+ }
+ }
+ // Then unload images from RAM.
+ if (images_ram_size > apply_tolerance(graphics_max_total_ram_size)) {
+ changed = 1;
+ int frames_begin = 0;
+ GR_LOG("Too much ram: %ld KiB\n", images_ram_size / 1024);
+ objects_sorted = gr_get_unloadable_objects_sorted_by_score(now);
+ for (int i = 0; i < kv_size(objects_sorted); i++) {
+ if (images_ram_size <= graphics_max_total_ram_size)
+ break;
+ gr_unload_object(&kv_A(objects_sorted, i));
+ }
+ }
+ if (changed) {
+ Milliseconds end = gr_now_ms();
+ GR_LOG("After cleaning: ram: %ld KiB disk: %ld KiB "
+ "img count: %d placement count: %d Took %ld ms\n",
+ images_ram_size / 1024, images_disk_size / 1024,
+ kh_size(images), total_placement_count, end - now);
+ }
+ kv_destroy(images_sorted);
+ kv_destroy(placements_sorted);
+ kv_destroy(frames_sorted);
+ kv_destroy(objects_sorted);
+}
+
+/// Unloads all images by user request.
+void gr_unload_images_to_reduce_ram() {
+ Image *img = NULL;
+ ImagePlacement *placement = NULL;
+ kh_foreach_value(images, img, {
+ kh_foreach_value(img->placements, placement, {
+ if (placement->protected_frame)
+ continue;
+ gr_unload_placement(placement);
+ });
+ gr_unload_all_frames(img);
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Image loading.
+////////////////////////////////////////////////////////////////////////////////
+
+/// Copies `num_pixels` pixels (not bytes!) from a buffer `from` to an imlib2
+/// image data `to`. The format may be 24 (RGB) or 32 (RGBA), and it's converted
+/// to imlib2's representation, which is 0xAARRGGBB (having BGRA memory layout
+/// on little-endian architectures).
+static inline void gr_copy_pixels(DATA32 *to, unsigned char *from, int format,
+ size_t num_pixels) {
+ size_t pixel_size = format == 24 ? 3 : 4;
+ if (format == 32) {
+ for (unsigned i = 0; i < num_pixels; ++i) {
+ unsigned byte_i = i * pixel_size;
+ to[i] = ((DATA32)from[byte_i + 2]) |
+ ((DATA32)from[byte_i + 1]) << 8 |
+ ((DATA32)from[byte_i]) << 16 |
+ ((DATA32)from[byte_i + 3]) << 24;
+ }
+ } else {
+ for (unsigned i = 0; i < num_pixels; ++i) {
+ unsigned byte_i = i * pixel_size;
+ to[i] = ((DATA32)from[byte_i + 2]) |
+ ((DATA32)from[byte_i + 1]) << 8 |
+ ((DATA32)from[byte_i]) << 16 | 0xFF000000;
+ }
+ }
+}
+
+/// Loads uncompressed RGB or RGBA image data from a file.
+static void gr_load_raw_pixel_data_uncompressed(DATA32 *data, FILE *file,
+ int format,
+ size_t total_pixels) {
+ unsigned char chunk[BUFSIZ];
+ size_t pixel_size = format == 24 ? 3 : 4;
+ size_t chunk_size_pix = BUFSIZ / 4;
+ size_t chunk_size_bytes = chunk_size_pix * pixel_size;
+ size_t bytes = total_pixels * pixel_size;
+ for (size_t chunk_start_pix = 0; chunk_start_pix < total_pixels;
+ chunk_start_pix += chunk_size_pix) {
+ size_t read_size = fread(chunk, 1, chunk_size_bytes, file);
+ size_t read_pixels = read_size / pixel_size;
+ if (chunk_start_pix + read_pixels > total_pixels)
+ read_pixels = total_pixels - chunk_start_pix;
+ gr_copy_pixels(data + chunk_start_pix, chunk, format,
+ read_pixels);
+ }
+}
+
+#define COMPRESSED_CHUNK_SIZE BUFSIZ
+#define DECOMPRESSED_CHUNK_SIZE (BUFSIZ * 4)
+
+/// Loads compressed RGB or RGBA image data from a file.
+static int gr_load_raw_pixel_data_compressed(DATA32 *data, FILE *file,
+ int format, size_t total_pixels) {
+ size_t pixel_size = format == 24 ? 3 : 4;
+ unsigned char compressed_chunk[COMPRESSED_CHUNK_SIZE];
+ unsigned char decompressed_chunk[DECOMPRESSED_CHUNK_SIZE];
+
+ z_stream strm;
+ strm.zalloc = Z_NULL;
+ strm.zfree = Z_NULL;
+ strm.opaque = Z_NULL;
+ strm.next_out = decompressed_chunk;
+ strm.avail_out = DECOMPRESSED_CHUNK_SIZE;
+ strm.avail_in = 0;
+ strm.next_in = Z_NULL;
+ int ret = inflateInit(&strm);
+ if (ret != Z_OK)
+ return 1;
+
+ int error = 0;
+ int progress = 0;
+ size_t total_copied_pixels = 0;
+ while (1) {
+ // If we don't have enough data in the input buffer, try to read
+ // from the file.
+ if (strm.avail_in <= COMPRESSED_CHUNK_SIZE / 4) {
+ // Move the existing data to the beginning.
+ memmove(compressed_chunk, strm.next_in, strm.avail_in);
+ strm.next_in = compressed_chunk;
+ // Read more data.
+ size_t bytes_read = fread(
+ compressed_chunk + strm.avail_in, 1,
+ COMPRESSED_CHUNK_SIZE - strm.avail_in, file);
+ strm.avail_in += bytes_read;
+ if (bytes_read != 0)
+ progress = 1;
+ }
+
+ // Try to inflate the data.
+ int ret = inflate(&strm, Z_SYNC_FLUSH);
+ if (ret == Z_MEM_ERROR || ret == Z_DATA_ERROR) {
+ error = 1;
+ fprintf(stderr,
+ "error: could not decompress the image, error "
+ "%s\n",
+ ret == Z_MEM_ERROR ? "Z_MEM_ERROR"
+ : "Z_DATA_ERROR");
+ break;
+ }
+
+ // Copy the data from the output buffer to the image.
+ size_t full_pixels =
+ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) / pixel_size;
+ // Make sure we don't overflow the image.
+ if (full_pixels > total_pixels - total_copied_pixels)
+ full_pixels = total_pixels - total_copied_pixels;
+ if (full_pixels > 0) {
+ // Copy pixels.
+ gr_copy_pixels(data, decompressed_chunk, format,
+ full_pixels);
+ data += full_pixels;
+ total_copied_pixels += full_pixels;
+ if (total_copied_pixels >= total_pixels) {
+ // We filled the whole image, there may be some
+ // data left, but we just truncate it.
+ break;
+ }
+ // Move the remaining data to the beginning.
+ size_t copied_bytes = full_pixels * pixel_size;
+ size_t leftover =
+ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) -
+ copied_bytes;
+ memmove(decompressed_chunk,
+ decompressed_chunk + copied_bytes, leftover);
+ strm.next_out -= copied_bytes;
+ strm.avail_out += copied_bytes;
+ progress = 1;
+ }
+
+ // If we haven't made any progress, then we have reached the end
+ // of both the file and the inflated data.
+ if (!progress)
+ break;
+ progress = 0;
+ }
+
+ inflateEnd(&strm);
+ return error;
+}
+
+#undef COMPRESSED_CHUNK_SIZE
+#undef DECOMPRESSED_CHUNK_SIZE
+
+/// Load the image from a file containing raw pixel data (RGB or RGBA), the data
+/// may be compressed.
+static Imlib_Image gr_load_raw_pixel_data(ImageFrame *frame,
+ const char *filename) {
+ size_t total_pixels = frame->data_pix_width * frame->data_pix_height;
+ if (total_pixels * 4 > graphics_max_single_image_ram_size) {
+ fprintf(stderr,
+ "error: image %u frame %u is too big too load: %zu > %u\n",
+ frame->image->image_id, frame->index, total_pixels * 4,
+ graphics_max_single_image_ram_size);
+ return NULL;
+ }
+
+ FILE* file = fopen(filename, "rb");
+ if (!file) {
+ fprintf(stderr,
+ "error: could not open image file: %s\n",
+ sanitized_filename(filename));
+ return NULL;
+ }
+
+ Imlib_Image image = imlib_create_image(frame->data_pix_width,
+ frame->data_pix_height);
+ if (!image) {
+ fprintf(stderr,
+ "error: could not create an image of size %d x %d\n",
+ frame->data_pix_width, frame->data_pix_height);
+ fclose(file);
+ return NULL;
+ }
+
+ imlib_context_set_image(image);
+ imlib_image_set_has_alpha(1);
+ DATA32* data = imlib_image_get_data();
+
+ if (frame->compression == 0) {
+ gr_load_raw_pixel_data_uncompressed(data, file, frame->format,
+ total_pixels);
+ } else {
+ int ret = gr_load_raw_pixel_data_compressed(
+ data, file, frame->format, total_pixels);
+ if (ret != 0) {
+ imlib_image_put_back_data(data);
+ imlib_free_image();
+ fclose(file);
+ return NULL;
+ }
+ }
+
+ fclose(file);
+ imlib_image_put_back_data(data);
+ return image;
+}
+
+/// Loads the unscaled frame into RAM as an imlib object. The frame imlib object
+/// is fully composed on top of the background frame. If the frame is already
+/// loaded, does nothing. Loading may fail, in which case the status of the
+/// frame will be set to STATUS_RAM_LOADING_ERROR.
+static void gr_load_imlib_object(ImageFrame *frame) {
+ if (frame->imlib_object)
+ return;
+
+ // If the image is uninitialized or uploading has failed, or the file
+ // has been deleted, we cannot load the image.
+ if (frame->status < STATUS_UPLOADING_SUCCESS)
+ return;
+ if (frame->disk_size == 0) {
+ if (frame->status != STATUS_RAM_LOADING_ERROR) {
+ fprintf(stderr,
+ "error: cached image was deleted: %u frame %u\n",
+ frame->image->image_id, frame->index);
+ }
+ frame->status = STATUS_RAM_LOADING_ERROR;
+ return;
+ }
+
+ // Prevent recursive dependences between frames.
+ if (frame->status == STATUS_RAM_LOADING_IN_PROGRESS) {
+ fprintf(stderr,
+ "error: recursive loading of image %u frame %u\n",
+ frame->image->image_id, frame->index);
+ frame->status = STATUS_RAM_LOADING_ERROR;
+ return;
+ }
+ frame->status = STATUS_RAM_LOADING_IN_PROGRESS;
+
+ // Load the background frame if needed. Hopefully it's not recursive.
+ ImageFrame *bg_frame = NULL;
+ if (frame->background_frame_index) {
+ bg_frame = gr_get_frame(frame->image,
+ frame->background_frame_index);
+ if (!bg_frame) {
+ fprintf(stderr,
+ "error: could not find background "
+ "frame %d for image %u frame %d\n",
+ frame->background_frame_index,
+ frame->image->image_id, frame->index);
+ frame->status = STATUS_RAM_LOADING_ERROR;
+ return;
+ }
+ gr_load_imlib_object(bg_frame);
+ if (!bg_frame->imlib_object) {
+ fprintf(stderr,
+ "error: could not load background frame %d for "
+ "image %u frame %d\n",
+ frame->background_frame_index,
+ frame->image->image_id, frame->index);
+ frame->status = STATUS_RAM_LOADING_ERROR;
+ return;
+ }
+ }
+
+ // We exclude background frames from the time to load the frame.
+ Milliseconds loading_start = gr_now_ms();
+
+ // Load the frame data image.
+ Imlib_Image frame_data_image = NULL;
+ char filename[MAX_FILENAME_SIZE];
+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
+ GR_LOG("Loading image: %s\n", sanitized_filename(filename));
+ if (frame->format == 100)
+ frame_data_image = imlib_load_image(filename);
+ if (frame->format == 32 || frame->format == 24)
+ frame_data_image = gr_load_raw_pixel_data(frame, filename);
+ debug_loaded_files_counter++;
+
+ if (!frame_data_image) {
+ if (frame->status != STATUS_RAM_LOADING_ERROR) {
+ fprintf(stderr, "error: could not load image: %s\n",
+ sanitized_filename(filename));
+ }
+ frame->status = STATUS_RAM_LOADING_ERROR;
+ return;
+ }
+
+ imlib_context_set_image(frame_data_image);
+ int frame_data_width = imlib_image_get_width();
+ int frame_data_height = imlib_image_get_height();
+ GR_LOG("Successfully loaded, size %d x %d\n", frame_data_width,
+ frame_data_height);
+ // If imlib loading succeeded, and it is the first frame, set the
+ // information about the original image size, unless it's already set.
+ if (frame->index == 1 && frame->image->pix_width == 0 &&
+ frame->image->pix_height == 0) {
+ frame->image->pix_width = frame_data_width;
+ frame->image->pix_height = frame_data_height;
+ }
+
+ int image_width = frame->image->pix_width;
+ int image_height = frame->image->pix_height;
+
+ // Compose the image with the background color or frame.
+ if (frame->background_color != 0 || bg_frame ||
+ image_width != frame_data_width ||
+ image_height != frame_data_height) {
+ GR_LOG("Composing the frame bg = 0x%08X, bgframe = %d\n",
+ frame->background_color, frame->background_frame_index);
+ Imlib_Image composed_image = imlib_create_image(
+ image_width, image_height);
+ imlib_context_set_image(composed_image);
+ imlib_image_set_has_alpha(1);
+ imlib_context_set_anti_alias(0);
+
+ // Start with the background frame or color.
+ imlib_context_set_blend(0);
+ if (bg_frame && bg_frame->imlib_object) {
+ imlib_blend_image_onto_image(
+ bg_frame->imlib_object, 1, 0, 0,
+ image_width, image_height, 0, 0,
+ image_width, image_height);
+ } else {
+ int r = (frame->background_color >> 24) & 0xFF;
+ int g = (frame->background_color >> 16) & 0xFF;
+ int b = (frame->background_color >> 8) & 0xFF;
+ int a = frame->background_color & 0xFF;
+ imlib_context_set_color(r, g, b, a);
+ imlib_image_fill_rectangle(0, 0, image_width,
+ image_height);
+ }
+
+ // Blend the frame data image onto the background.
+ imlib_context_set_blend(1);
+ imlib_blend_image_onto_image(
+ frame_data_image, 1, 0, 0, frame->data_pix_width,
+ frame->data_pix_height, frame->x, frame->y,
+ frame->data_pix_width, frame->data_pix_height);
+
+ // Free the frame data image.
+ imlib_context_set_image(frame_data_image);
+ imlib_free_image();
+
+ frame_data_image = composed_image;
+ }
+
+ frame->imlib_object = frame_data_image;
+
+ images_ram_size += gr_frame_current_ram_size(frame);
+ frame->status = STATUS_RAM_LOADING_SUCCESS;
+
+ Milliseconds loading_end = gr_now_ms();
+ GR_LOG("After loading image %u frame %d ram: %ld KiB (+ %u KiB) Took "
+ "%ld ms\n",
+ frame->image->image_id, frame->index, images_ram_size / 1024,
+ gr_frame_current_ram_size(frame) / 1024,
+ loading_end - loading_start);
+}
+
+/// Premultiplies the alpha channel of the image data. The data is an array of
+/// pixels such that each pixel is a 32-bit integer in the format 0xAARRGGBB.
+static void gr_premultiply_alpha(DATA32 *data, size_t num_pixels) {
+ for (size_t i = 0; i < num_pixels; ++i) {
+ DATA32 pixel = data[i];
+ unsigned char a = pixel >> 24;
+ if (a == 0) {
+ data[i] = 0;
+ } else if (a != 255) {
+ unsigned char b = (pixel & 0xFF) * a / 255;
+ unsigned char g = ((pixel >> 8) & 0xFF) * a / 255;
+ unsigned char r = ((pixel >> 16) & 0xFF) * a / 255;
+ data[i] = (a << 24) | (r << 16) | (g << 8) | b;
+ }
+ }
+}
+
+/// Creates a pixmap for the frame of an image placement. The pixmap contain the
+/// image data correctly scaled and fit to the box defined by the number of
+/// rows/columns of the image placement and the provided cell dimensions in
+/// pixels. If the placement is already loaded, it will be reloaded only if the
+/// cell dimensions have changed.
+Pixmap gr_load_pixmap(ImagePlacement *placement, int frameidx, int cw, int ch) {
+ Milliseconds loading_start = gr_now_ms();
+ Image *img = placement->image;
+ ImageFrame *frame = gr_get_frame(img, frameidx);
+
+ // Update the atime uncoditionally.
+ gr_touch_placement(placement);
+ if (frame)
+ gr_touch_frame(frame);
+
+ // If cw or ch are different, unload all the pixmaps.
+ if (placement->scaled_cw != cw || placement->scaled_ch != ch) {
+ gr_unload_placement(placement);
+ placement->scaled_cw = cw;
+ placement->scaled_ch = ch;
+ }
+
+ // If it's already loaded, do nothing.
+ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx);
+ if (pixmap)
+ return pixmap;
+
+ GR_LOG("Loading placement: %u/%u frame %u\n", img->image_id,
+ placement->placement_id, frameidx);
+
+ // Load the imlib object for the frame.
+ if (!frame) {
+ fprintf(stderr,
+ "error: could not find frame %u for image %u\n",
+ frameidx, img->image_id);
+ return 0;
+ }
+ gr_load_imlib_object(frame);
+ if (!frame->imlib_object)
+ return 0;
+
+ // Infer the placement size if needed.
+ gr_infer_placement_size_maybe(placement);
+
+ // Create the scaled image. This is temporary, we will scale it
+ // appropriately, upload to the X server, and then delete immediately.
+ int scaled_w = (int)placement->cols * cw;
+ int scaled_h = (int)placement->rows * ch;
+ if (scaled_w * scaled_h * 4 > graphics_max_single_image_ram_size) {
+ fprintf(stderr,
+ "error: placement %u/%u would be too big to load: %d x "
+ "%d x 4 > %u\n",
+ img->image_id, placement->placement_id, scaled_w,
+ scaled_h, graphics_max_single_image_ram_size);
+ return 0;
+ }
+ Imlib_Image scaled_image = imlib_create_image(scaled_w, scaled_h);
+ if (!scaled_image) {
+ fprintf(stderr,
+ "error: imlib_create_image(%d, %d) returned "
+ "null\n",
+ scaled_w, scaled_h);
+ return 0;
+ }
+ imlib_context_set_image(scaled_image);
+ imlib_image_set_has_alpha(1);
+
+ // First fill the scaled image with the transparent color.
+ imlib_context_set_blend(0);
+ imlib_context_set_color(0, 0, 0, 0);
+ imlib_image_fill_rectangle(0, 0, scaled_w, scaled_h);
+ imlib_context_set_anti_alias(1);
+ imlib_context_set_blend(1);
+
+ // The source rectangle.
+ int src_x = placement->src_pix_x;
+ int src_y = placement->src_pix_y;
+ int src_w = placement->src_pix_width;
+ int src_h = placement->src_pix_height;
+ // Whether the box is too small to use the true size of the image.
+ char box_too_small = scaled_w < src_w || scaled_h < src_h;
+ char mode = placement->scale_mode;
+
+ // Then blend the original image onto the transparent background.
+ if (src_w <= 0 || src_h <= 0) {
+ fprintf(stderr, "warning: image of zero size\n");
+ } else if (mode == SCALE_MODE_FILL) {
+ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
+ src_y, src_w, src_h, 0, 0,
+ scaled_w, scaled_h);
+ } else if (mode == SCALE_MODE_NONE ||
+ (mode == SCALE_MODE_NONE_OR_CONTAIN && !box_too_small)) {
+ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
+ src_y, src_w, src_h, 0, 0, src_w,
+ src_h);
+ } else {
+ if (mode != SCALE_MODE_CONTAIN &&
+ mode != SCALE_MODE_NONE_OR_CONTAIN) {
+ fprintf(stderr,
+ "warning: unknown scale mode %u, using "
+ "'contain' instead\n",
+ mode);
+ }
+ int dest_x, dest_y;
+ int dest_w, dest_h;
+ if (scaled_w * src_h > src_w * scaled_h) {
+ // If the box is wider than the original image, fit to
+ // height.
+ dest_h = scaled_h;
+ dest_y = 0;
+ dest_w = src_w * scaled_h / src_h;
+ dest_x = (scaled_w - dest_w) / 2;
+ } else {
+ // Otherwise, fit to width.
+ dest_w = scaled_w;
+ dest_x = 0;
+ dest_h = src_h * scaled_w / src_w;
+ dest_y = (scaled_h - dest_h) / 2;
+ }
+ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
+ src_y, src_w, src_h, dest_x,
+ dest_y, dest_w, dest_h);
+ }
+
+ // XRender needs the alpha channel premultiplied.
+ DATA32 *data = imlib_image_get_data();
+ gr_premultiply_alpha(data, scaled_w * scaled_h);
+
+ // Upload the image to the X server.
+ Display *disp = imlib_context_get_display();
+ Visual *vis = imlib_context_get_visual();
+ Colormap cmap = imlib_context_get_colormap();
+ Drawable drawable = imlib_context_get_drawable();
+ if (!drawable)
+ drawable = DefaultRootWindow(disp);
+ pixmap = XCreatePixmap(disp, drawable, scaled_w, scaled_h, 32);
+ XVisualInfo visinfo = {0};
+ Status visual_found = XMatchVisualInfo(disp, DefaultScreen(disp), 32,
+ TrueColor, &visinfo) ||
+ XMatchVisualInfo(disp, DefaultScreen(disp), 24,
+ TrueColor, &visinfo);
+ if (!visual_found) {
+ fprintf(stderr,
+ "error: could not find 32-bit TrueColor visual\n");
+ // Proceed anyway.
+ visinfo.visual = NULL;
+ }
+ XImage *ximage = XCreateImage(disp, visinfo.visual, 32, ZPixmap, 0,
+ (char *)data, scaled_w, scaled_h, 32, 0);
+ if (!ximage) {
+ fprintf(stderr, "error: could not create XImage\n");
+ imlib_image_put_back_data(data);
+ imlib_free_image();
+ return 0;
+ }
+ GC gc = XCreateGC(disp, pixmap, 0, NULL);
+ XPutImage(disp, pixmap, gc, ximage, 0, 0, 0, 0, scaled_w,
+ scaled_h);
+ XFreeGC(disp, gc);
+ // XDestroyImage will free the data as well, but it is managed by imlib,
+ // so set it to NULL.
+ ximage->data = NULL;
+ XDestroyImage(ximage);
+ imlib_image_put_back_data(data);
+ imlib_free_image();
+
+ // Assign the pixmap to the frame and increase the ram size.
+ gr_set_frame_pixmap(placement, frameidx, pixmap);
+ images_ram_size += gr_placement_single_frame_ram_size(placement);
+ debug_loaded_pixmaps_counter++;
+
+ Milliseconds loading_end = gr_now_ms();
+ GR_LOG("After loading placement %u/%u frame %d ram: %ld KiB (+ %u "
+ "KiB) Took %ld ms\n",
+ frame->image->image_id, placement->placement_id, frame->index,
+ images_ram_size / 1024,
+ gr_placement_single_frame_ram_size(placement) / 1024,
+ loading_end - loading_start);
+
+ // Free up ram if needed, but keep the pixmap we've loaded no matter
+ // what.
+ placement->protected_frame = frameidx;
+ gr_check_limits();
+ placement->protected_frame = 0;
+
+ return pixmap;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Initialization and deinitialization.
+////////////////////////////////////////////////////////////////////////////////
+
+/// Creates a temporary directory.
+static int gr_create_cache_dir() {
+ strncpy(cache_dir, graphics_cache_dir_template, sizeof(cache_dir));
+ if (!mkdtemp(cache_dir)) {
+ fprintf(stderr,
+ "error: could not create temporary dir from template "
+ "%s\n",
+ sanitized_filename(cache_dir));
+ return 0;
+ }
+ fprintf(stderr, "Graphics cache directory: %s\n", cache_dir);
+ return 1;
+}
+
+/// Checks whether `tmp_dir` exists and recreates it if it doesn't.
+static void gr_make_sure_tmpdir_exists() {
+ struct stat st;
+ if (stat(cache_dir, &st) == 0 && S_ISDIR(st.st_mode))
+ return;
+ fprintf(stderr,
+ "error: %s is not a directory, will need to create a new "
+ "graphics cache directory\n",
+ sanitized_filename(cache_dir));
+ gr_create_cache_dir();
+}
+
+/// Initialize the graphics module.
+void gr_init(Display *disp, Visual *vis, Colormap cm) {
+ // Set the initialization time.
+ clock_gettime(CLOCK_MONOTONIC, &initialization_time);
+
+ // Create the temporary dir.
+ if (!gr_create_cache_dir())
+ abort();
+
+ // Initialize imlib.
+ imlib_context_set_display(disp);
+ imlib_context_set_visual(vis);
+ imlib_context_set_colormap(cm);
+ imlib_context_set_anti_alias(1);
+ imlib_context_set_blend(1);
+ // Imlib2 checks only the file name when caching, which is not enough
+ // for us since we reuse file names. Disable caching.
+ imlib_set_cache_size(0);
+
+ // Prepare for color inversion.
+ for (size_t i = 0; i < 256; ++i)
+ reverse_table[i] = 255 - i;
+
+ // Create data structures.
+ images = kh_init(id2image);
+ kv_init(next_redraw_times);
+
+ atexit(gr_deinit);
+}
+
+/// Deinitialize the graphics module.
+void gr_deinit() {
+ // Remove the cache dir.
+ remove(cache_dir);
+ kv_destroy(next_redraw_times);
+ if (images) {
+ // Delete all images.
+ gr_delete_all_images();
+ // Destroy the data structures.
+ kh_destroy(id2image, images);
+ images = NULL;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Dumping, debugging, and image preview.
+////////////////////////////////////////////////////////////////////////////////
+
+/// Returns a string containing a time difference in a human-readable format.
+/// Uses a static buffer, so be careful.
+static const char *gr_ago(Milliseconds diff) {
+ static char result[32];
+ double seconds = (double)diff / 1000.0;
+ if (seconds < 1)
+ snprintf(result, sizeof(result), "%.2f sec ago", seconds);
+ else if (seconds < 60)
+ snprintf(result, sizeof(result), "%d sec ago", (int)seconds);
+ else if (seconds < 3600)
+ snprintf(result, sizeof(result), "%d min %d sec ago",
+ (int)(seconds / 60), (int)(seconds) % 60);
+ else {
+ snprintf(result, sizeof(result), "%d hr %d min %d sec ago",
+ (int)(seconds / 3600), (int)(seconds) % 3600 / 60,
+ (int)(seconds) % 60);
+ }
+ return result;
+}
+
+/// Prints to `file` with an indentation of `ind` spaces.
+static void fprintf_ind(FILE *file, int ind, const char *format, ...) {
+ fprintf(file, "%*s", ind, "");
+ va_list args;
+ va_start(args, format);
+ vfprintf(file, format, args);
+ va_end(args);
+}
+
+/// Dumps the image info to `file` with an indentation of `ind` spaces.
+static void gr_dump_image_info(FILE *file, Image *img, int ind) {
+ if (!img) {
+ fprintf_ind(file, ind, "Image is NULL\n");
+ return;
+ }
+ Milliseconds now = gr_now_ms();
+ fprintf_ind(file, ind, "Image %u\n", img->image_id);
+ ind += 4;
+ fprintf_ind(file, ind, "number: %u\n", img->image_number);
+ fprintf_ind(file, ind, "global command index: %lu\n",
+ img->global_command_index);
+ fprintf_ind(file, ind, "accessed: %ld %s\n", img->atime,
+ gr_ago(now - img->atime));
+ fprintf_ind(file, ind, "pix size: %ux%u\n", img->pix_width,
+ img->pix_height);
+ fprintf_ind(file, ind, "cur frame start time: %ld %s\n",
+ img->current_frame_time,
+ gr_ago(now - img->current_frame_time));
+ if (img->next_redraw)
+ fprintf_ind(file, ind, "next redraw: %ld in %ld ms\n",
+ img->next_redraw, img->next_redraw - now);
+ fprintf_ind(file, ind, "total disk size: %u KiB\n",
+ img->total_disk_size / 1024);
+ fprintf_ind(file, ind, "total duration: %d\n", img->total_duration);
+ fprintf_ind(file, ind, "frames: %d\n", gr_last_frame_index(img));
+ fprintf_ind(file, ind, "cur frame: %d\n", img->current_frame);
+ fprintf_ind(file, ind, "animation state: %d\n", img->animation_state);
+ fprintf_ind(file, ind, "default_placement: %u\n",
+ img->default_placement);
+}
+
+/// Dumps the frame info to `file` with an indentation of `ind` spaces.
+static void gr_dump_frame_info(FILE *file, ImageFrame *frame, int ind) {
+ if (!frame) {
+ fprintf_ind(file, ind, "Frame is NULL\n");
+ return;
+ }
+ Milliseconds now = gr_now_ms();
+ fprintf_ind(file, ind, "Frame %d\n", frame->index);
+ ind += 4;
+ if (frame->index == 0) {
+ fprintf_ind(file, ind, "NOT INITIALIZED\n");
+ return;
+ }
+ if (frame->uploading_failure)
+ fprintf_ind(file, ind, "uploading failure: %s\n",
+ image_uploading_failure_strings
+ [frame->uploading_failure]);
+ fprintf_ind(file, ind, "gap: %d\n", frame->gap);
+ fprintf_ind(file, ind, "accessed: %ld %s\n", frame->atime,
+ gr_ago(now - frame->atime));
+ fprintf_ind(file, ind, "data pix size: %ux%u\n", frame->data_pix_width,
+ frame->data_pix_height);
+ char filename[MAX_FILENAME_SIZE];
+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
+ if (access(filename, F_OK) != -1)
+ fprintf_ind(file, ind, "file: %s\n",
+ sanitized_filename(filename));
+ else
+ fprintf_ind(file, ind, "not on disk\n");
+ fprintf_ind(file, ind, "disk size: %u KiB\n", frame->disk_size / 1024);
+ if (frame->imlib_object) {
+ unsigned ram_size = gr_frame_current_ram_size(frame);
+ fprintf_ind(file, ind,
+ "loaded into ram, size: %d "
+ "KiB\n",
+ ram_size / 1024);
+ } else {
+ fprintf_ind(file, ind, "not loaded into ram\n");
+ }
+}
+
+/// Dumps the placement info to `file` with an indentation of `ind` spaces.
+static void gr_dump_placement_info(FILE *file, ImagePlacement *placement,
+ int ind) {
+ if (!placement) {
+ fprintf_ind(file, ind, "Placement is NULL\n");
+ return;
+ }
+ Milliseconds now = gr_now_ms();
+ fprintf_ind(file, ind, "Placement %u\n", placement->placement_id);
+ ind += 4;
+ fprintf_ind(file, ind, "accessed: %ld %s\n", placement->atime,
+ gr_ago(now - placement->atime));
+ fprintf_ind(file, ind, "scale_mode: %u\n", placement->scale_mode);
+ fprintf_ind(file, ind, "size: %u cols x %u rows\n", placement->cols,
+ placement->rows);
+ fprintf_ind(file, ind, "cell size: %ux%u\n", placement->scaled_cw,
+ placement->scaled_ch);
+ fprintf_ind(file, ind, "ram per frame: %u KiB\n",
+ gr_placement_single_frame_ram_size(placement) / 1024);
+ unsigned ram_size = gr_placement_current_ram_size(placement);
+ fprintf_ind(file, ind, "ram size: %d KiB\n", ram_size / 1024);
+}
+
+/// Dumps placement pixmaps to `file` with an indentation of `ind` spaces.
+static void gr_dump_placement_pixmaps(FILE *file, ImagePlacement *placement,
+ int ind) {
+ if (!placement)
+ return;
+ int frameidx = 1;
+ foreach_pixmap(*placement, pixmap, {
+ fprintf_ind(file, ind, "Frame %d pixmap %lu\n", frameidx,
+ pixmap);
+ ++frameidx;
+ });
+}
+
+/// Dumps the internal state (images and placements) to stderr.
+void gr_dump_state() {
+ FILE *file = stderr;
+ int ind = 0;
+ fprintf_ind(file, ind, "======= Graphics module state dump =======\n");
+ fprintf_ind(file, ind,
+ "sizeof(Image) = %lu sizeof(ImageFrame) = %lu "
+ "sizeof(ImagePlacement) = %lu\n",
+ sizeof(Image), sizeof(ImageFrame), sizeof(ImagePlacement));
+ fprintf_ind(file, ind, "Image count: %u\n", kh_size(images));
+ fprintf_ind(file, ind, "Placement count: %u\n", total_placement_count);
+ fprintf_ind(file, ind, "Estimated RAM usage: %ld KiB\n",
+ images_ram_size / 1024);
+ fprintf_ind(file, ind, "Estimated Disk usage: %ld KiB\n",
+ images_disk_size / 1024);
+
+ Milliseconds now = gr_now_ms();
+
+ int64_t images_ram_size_computed = 0;
+ int64_t images_disk_size_computed = 0;
+
+ Image *img = NULL;
+ ImagePlacement *placement = NULL;
+ kh_foreach_value(images, img, {
+ fprintf_ind(file, ind, "----------------\n");
+ gr_dump_image_info(file, img, 0);
+ int64_t total_disk_size_computed = 0;
+ int total_duration_computed = 0;
+ foreach_frame(*img, frame, {
+ gr_dump_frame_info(file, frame, 4);
+ if (frame->image != img)
+ fprintf_ind(file, 8,
+ "ERROR: WRONG IMAGE POINTER\n");
+ total_duration_computed += frame->gap;
+ images_disk_size_computed += frame->disk_size;
+ total_disk_size_computed += frame->disk_size;
+ if (frame->imlib_object)
+ images_ram_size_computed +=
+ gr_frame_current_ram_size(frame);
+ });
+ if (img->total_disk_size != total_disk_size_computed) {
+ fprintf_ind(file, ind,
+ " ERROR: total_disk_size is %u, but "
+ "computed value is %ld\n",
+ img->total_disk_size, total_disk_size_computed);
+ }
+ if (img->total_duration != total_duration_computed) {
+ fprintf_ind(file, ind,
+ " ERROR: total_duration is %d, but computed "
+ "value is %d\n",
+ img->total_duration, total_duration_computed);
+ }
+ kh_foreach_value(img->placements, placement, {
+ gr_dump_placement_info(file, placement, 4);
+ if (placement->image != img)
+ fprintf_ind(file, 8,
+ "ERROR: WRONG IMAGE POINTER\n");
+ fprintf_ind(file, 8,
+ "Pixmaps:\n");
+ gr_dump_placement_pixmaps(file, placement, 12);
+ unsigned ram_size =
+ gr_placement_current_ram_size(placement);
+ images_ram_size_computed += ram_size;
+ });
+ });
+ if (images_ram_size != images_ram_size_computed) {
+ fprintf_ind(file, ind,
+ "ERROR: images_ram_size is %ld, but computed value "
+ "is %ld\n",
+ images_ram_size, images_ram_size_computed);
+ }
+ if (images_disk_size != images_disk_size_computed) {
+ fprintf_ind(file, ind,
+ "ERROR: images_disk_size is %ld, but computed value "
+ "is %ld\n",
+ images_disk_size, images_disk_size_computed);
+ }
+ fprintf_ind(file, ind, "===========================================\n");
+}
+
+/// Executes `command` with the name of the file corresponding to `image_id` as
+/// the argument. Executes xmessage with an error message on failure.
+// TODO: Currently we do this for the first frame only. Not sure what to do with
+// animations.
+void gr_preview_image(uint32_t image_id, const char *exec) {
+ char command[256];
+ size_t len;
+ Image *img = gr_find_image(image_id);
+ if (img) {
+ ImageFrame *frame = &img->first_frame;
+ char filename[MAX_FILENAME_SIZE];
+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
+ if (frame->disk_size == 0) {
+ len = snprintf(command, 255,
+ "xmessage 'Image with id=%u is not "
+ "fully copied to %s'",
+ image_id, sanitized_filename(filename));
+ } else {
+ len = snprintf(command, 255, "%s %s &", exec,
+ sanitized_filename(filename));
+ }
+ } else {
+ len = snprintf(command, 255,
+ "xmessage 'Cannot find image with id=%u'",
+ image_id);
+ }
+ if (len > 255) {
+ fprintf(stderr, "error: command too long: %s\n", command);
+ snprintf(command, 255, "xmessage 'error: command too long'");
+ }
+ if (system(command) != 0) {
+ fprintf(stderr, "error: could not execute command %s\n",
+ command);
+ }
+}
+
+/// Executes `<st> -e less <file>` where <file> is the name of a temporary file
+/// containing the information about an image and placement, and <st> is
+/// specified with `st_executable`.
+void gr_show_image_info(uint32_t image_id, uint32_t placement_id,
+ uint32_t imgcol, uint32_t imgrow,
+ char is_classic_placeholder, int32_t diacritic_count,
+ char *st_executable) {
+ char filename[MAX_FILENAME_SIZE];
+ snprintf(filename, sizeof(filename), "%s/info-%u", cache_dir, image_id);
+ FILE *file = fopen(filename, "w");
+ if (!file) {
+ perror("fopen");
+ return;
+ }
+ // Basic information about the cell.
+ fprintf(file, "image_id = %u = 0x%08X\n", image_id, image_id);
+ fprintf(file, "placement_id = %u = 0x%08X\n", placement_id, placement_id);
+ fprintf(file, "column = %d, row = %d\n", imgcol, imgrow);
+ fprintf(file, "classic/unicode placeholder = %s\n",
+ is_classic_placeholder ? "classic" : "unicode");
+ fprintf(file, "original diacritic count = %d\n", diacritic_count);
+ // Information about the image and the placement.
+ Image *img = gr_find_image(image_id);
+ ImagePlacement *placement = gr_find_placement(img, placement_id);
+ gr_dump_image_info(file, img, 0);
+ gr_dump_placement_info(file, placement, 0);
+ // The text underneath this particular cell.
+ if (placement && placement->text_underneath && imgcol >= 1 &&
+ imgrow >= 1 && imgcol <= placement->cols &&
+ imgrow <= placement->rows) {
+ fprintf(file, "Glyph underneath:\n");
+ Glyph *glyph =
+ &placement->text_underneath[(imgrow - 1) *
+ placement->cols +
+ imgcol - 1];
+ fprintf(file, " rune = 0x%08X\n", glyph->u);
+ fprintf(file, " bg = 0x%08X\n", glyph->bg);
+ fprintf(file, " fg = 0x%08X\n", glyph->fg);
+ fprintf(file, " decor = 0x%08X\n", glyph->decor);
+ fprintf(file, " mode = 0x%08X\n", glyph->mode);
+ }
+ if (img) {
+ fprintf(file, "Frames:\n");
+ foreach_frame(*img, frame, {
+ gr_dump_frame_info(file, frame, 4);
+ });
+ }
+ if (placement) {
+ fprintf(file, "Placement pixmaps:\n");
+ gr_dump_placement_pixmaps(file, placement, 4);
+ }
+ fclose(file);
+ char *argv[] = {st_executable, "-e", "less", filename, NULL};
+ if (posix_spawnp(NULL, st_executable, NULL, NULL, argv, environ) != 0) {
+ perror("posix_spawnp");
+ return;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Appending and displaying image rectangles.
+////////////////////////////////////////////////////////////////////////////////
+
+/// Displays debug information in the rectangle using colors col1 and col2.
+static void gr_displayinfo(Drawable buf, ImageRect *rect, int col1, int col2,
+ const char *message) {
+ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw;
+ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch;
+ Display *disp = imlib_context_get_display();
+ GC gc = XCreateGC(disp, buf, 0, NULL);
+ char info[MAX_INFO_LEN];
+ if (rect->placement_id)
+ snprintf(info, MAX_INFO_LEN, "%s%u/%u [%d:%d)x[%d:%d)", message,
+ rect->image_id, rect->placement_id,
+ rect->img_start_col, rect->img_end_col,
+ rect->img_start_row, rect->img_end_row);
+ else
+ snprintf(info, MAX_INFO_LEN, "%s%u [%d:%d)x[%d:%d)", message,
+ rect->image_id, rect->img_start_col, rect->img_end_col,
+ rect->img_start_row, rect->img_end_row);
+ XSetForeground(disp, gc, col1);
+ XDrawString(disp, buf, gc, rect->screen_x_pix + 4,
+ rect->screen_y_pix + h_pix - 3, info, strlen(info));
+ XSetForeground(disp, gc, col2);
+ XDrawString(disp, buf, gc, rect->screen_x_pix + 2,
+ rect->screen_y_pix + h_pix - 5, info, strlen(info));
+ XFreeGC(disp, gc);
+}
+
+/// Draws a rectangle (bounding box) for debugging.
+static void gr_showrect(Drawable buf, ImageRect *rect) {
+ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw;
+ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch;
+ Display *disp = imlib_context_get_display();
+ GC gc = XCreateGC(disp, buf, 0, NULL);
+ XSetForeground(disp, gc, 0xFF00FF00);
+ XDrawRectangle(disp, buf, gc, rect->screen_x_pix, rect->screen_y_pix,
+ w_pix - 1, h_pix - 1);
+ XSetForeground(disp, gc, 0xFFFF0000);
+ XDrawRectangle(disp, buf, gc, rect->screen_x_pix + 1,
+ rect->screen_y_pix + 1, w_pix - 3, h_pix - 3);
+ XFreeGC(disp, gc);
+}
+
+/// Updates the next redraw time for the given row. Resizes the
+/// next_redraw_times array if needed.
+static void gr_update_next_redraw_time(int row, Milliseconds next_redraw) {
+ if (next_redraw == 0)
+ return;
+ if (row >= kv_size(next_redraw_times)) {
+ size_t old_size = kv_size(next_redraw_times);
+ kv_a(Milliseconds, next_redraw_times, row);
+ for (size_t i = old_size; i <= row; ++i)
+ kv_A(next_redraw_times, i) = 0;
+ }
+ Milliseconds old_value = kv_A(next_redraw_times, row);
+ if (old_value == 0 || old_value > next_redraw)
+ kv_A(next_redraw_times, row) = next_redraw;
+}
+
+/// Draws the given part of an image.
+static void gr_drawimagerect(Drawable buf, ImageRect *rect) {
+ ImagePlacement *placement =
+ gr_find_image_and_placement(rect->image_id, rect->placement_id);
+ // If the image does not exist or image display is switched off, draw
+ // the bounding box.
+ if (!placement || !graphics_display_images) {
+ gr_showrect(buf, rect);
+ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
+ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
+ return;
+ }
+
+ Image *img = placement->image;
+
+ if (img->last_redraw < drawing_start_time) {
+ // This is the first time we draw this image in this redraw
+ // cycle. Update the frame index we are going to display. Note
+ // that currently all image placements are synchronized.
+ int old_frame = img->current_frame;
+ gr_update_frame_index(img, drawing_start_time);
+ img->last_redraw = drawing_start_time;
+ }
+
+ // Adjust next redraw times for the rows of this image rect.
+ if (img->next_redraw) {
+ for (int row = rect->screen_y_row;
+ row <= rect->screen_y_row + rect->img_end_row -
+ rect->img_start_row - 1; ++row) {
+ gr_update_next_redraw_time(
+ row, img->next_redraw);
+ }
+ }
+
+ // Load the frame.
+ Pixmap pixmap = gr_load_pixmap(placement, img->current_frame, rect->cw,
+ rect->ch);
+
+ // If the image couldn't be loaded, display the bounding box.
+ if (!pixmap) {
+ gr_showrect(buf, rect);
+ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
+ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
+ return;
+ }
+
+ int src_x = rect->img_start_col * rect->cw;
+ int src_y = rect->img_start_row * rect->ch;
+ int width = (rect->img_end_col - rect->img_start_col) * rect->cw;
+ int height = (rect->img_end_row - rect->img_start_row) * rect->ch;
+ int dst_x = rect->screen_x_pix;
+ int dst_y = rect->screen_y_pix;
+
+ // Display the image.
+ Display *disp = imlib_context_get_display();
+ Visual *vis = imlib_context_get_visual();
+
+ // Create an xrender picture for the window.
+ XRenderPictFormat *win_format =
+ XRenderFindVisualFormat(disp, vis);
+ Picture window_pic =
+ XRenderCreatePicture(disp, buf, win_format, 0, NULL);
+
+ // If needed, invert the image pixmap. Note that this naive approach of
+ // inverting the pixmap is not entirely correct, because the pixmap is
+ // premultiplied. But the result is good enough to visually indicate
+ // selection.
+ if (rect->reverse) {
+ unsigned pixmap_w =
+ (unsigned)placement->cols * placement->scaled_cw;
+ unsigned pixmap_h =
+ (unsigned)placement->rows * placement->scaled_ch;
+ Pixmap invpixmap =
+ XCreatePixmap(disp, buf, pixmap_w, pixmap_h, 32);
+ XGCValues gcv = {.function = GXcopyInverted};
+ GC gc = XCreateGC(disp, invpixmap, GCFunction, &gcv);
+ XCopyArea(disp, pixmap, invpixmap, gc, 0, 0, pixmap_w,
+ pixmap_h, 0, 0);
+ XFreeGC(disp, gc);
+ pixmap = invpixmap;
+ }
+
+ // Create a picture for the image pixmap.
+ XRenderPictFormat *pic_format =
+ XRenderFindStandardFormat(disp, PictStandardARGB32);
+ Picture pixmap_pic =
+ XRenderCreatePicture(disp, pixmap, pic_format, 0, NULL);
+
+ // Composite the image onto the window. In the reverse mode we ignore
+ // the alpha channel of the image because the naive inversion above
+ // seems to invert the alpha channel as well.
+ int pictop = rect->reverse ? PictOpSrc : PictOpOver;
+ XRenderComposite(disp, pictop, pixmap_pic, 0, window_pic,
+ src_x, src_y, src_x, src_y, dst_x, dst_y, width,
+ height);
+
+ // Free resources
+ XRenderFreePicture(disp, pixmap_pic);
+ XRenderFreePicture(disp, window_pic);
+ if (rect->reverse)
+ XFreePixmap(disp, pixmap);
+
+ // In debug mode always draw bounding boxes and print info.
+ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) {
+ gr_showrect(buf, rect);
+ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
+ }
+}
+
+/// Removes the given image rectangle.
+static void gr_freerect(ImageRect *rect) { memset(rect, 0, sizeof(ImageRect)); }
+
+/// Returns the bottom coordinate of the rect.
+static int gr_getrectbottom(ImageRect *rect) {
+ return rect->screen_y_pix +
+ (rect->img_end_row - rect->img_start_row) * rect->ch;
+}
+
+/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell.
+void gr_start_drawing(Drawable buf, int cw, int ch) {
+ current_cw = cw;
+ current_ch = ch;
+ debug_loaded_files_counter = 0;
+ debug_loaded_pixmaps_counter = 0;
+ drawing_start_time = gr_now_ms();
+ imlib_context_set_drawable(buf);
+}
+
+/// Finish image drawing. This functions will draw all the rectangles left to
+/// draw.
+void gr_finish_drawing(Drawable buf) {
+ // Draw and then delete all known image rectangles.
+ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
+ ImageRect *rect = &image_rects[i];
+ if (!rect->image_id)
+ continue;
+ gr_drawimagerect(buf, rect);
+ gr_freerect(rect);
+ }
+
+ // Compute the delay until the next redraw as the minimum of the next
+ // redraw delays for all rows.
+ Milliseconds drawing_end_time = gr_now_ms();
+ graphics_next_redraw_delay = INT_MAX;
+ for (int row = 0; row < kv_size(next_redraw_times); ++row) {
+ Milliseconds row_next_redraw = kv_A(next_redraw_times, row);
+ if (row_next_redraw > 0) {
+ int delay = MAX(graphics_animation_min_delay,
+ row_next_redraw - drawing_end_time);
+ graphics_next_redraw_delay =
+ MIN(graphics_next_redraw_delay, delay);
+ }
+ }
+
+ // In debug mode display additional info.
+ if (graphics_debug_mode) {
+ int milliseconds = drawing_end_time - drawing_start_time;
+
+ Display *disp = imlib_context_get_display();
+ GC gc = XCreateGC(disp, buf, 0, NULL);
+ const char *debug_mode_str =
+ graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES
+ ? "(boxes shown) "
+ : "";
+ int redraw_delay = graphics_next_redraw_delay == INT_MAX
+ ? -1
+ : graphics_next_redraw_delay;
+ char info[MAX_INFO_LEN];
+ snprintf(info, MAX_INFO_LEN,
+ "%sRender time: %d ms ram %ld K disk %ld K count "
+ "%d cell %dx%d delay %d",
+ debug_mode_str, milliseconds, images_ram_size / 1024,
+ images_disk_size / 1024, kh_size(images), current_cw,
+ current_ch, redraw_delay);
+ XSetForeground(disp, gc, 0xFF000000);
+ XFillRectangle(disp, buf, gc, 0, 0, 600, 16);
+ XSetForeground(disp, gc, 0xFFFFFFFF);
+ XDrawString(disp, buf, gc, 0, 14, info, strlen(info));
+ XFreeGC(disp, gc);
+
+ if (milliseconds > 0) {
+ fprintf(stderr, "%s (loaded %d files, %d pixmaps)\n",
+ info, debug_loaded_files_counter,
+ debug_loaded_pixmaps_counter);
+ }
+ }
+
+ // Check the limits in case we have used too much ram for placements.
+ gr_check_limits();
+}
+
+// Add an image rectangle to the list of rectangles to draw.
+void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id,
+ int img_start_col, int img_end_col, int img_start_row,
+ int img_end_row, int x_col, int y_row, int x_pix,
+ int y_pix, int cw, int ch, int reverse) {
+ current_cw = cw;
+ current_ch = ch;
+
+ ImageRect new_rect;
+ new_rect.image_id = image_id;
+ new_rect.placement_id = placement_id;
+ new_rect.img_start_col = img_start_col;
+ new_rect.img_end_col = img_end_col;
+ new_rect.img_start_row = img_start_row;
+ new_rect.img_end_row = img_end_row;
+ new_rect.screen_y_row = y_row;
+ new_rect.screen_x_pix = x_pix;
+ new_rect.screen_y_pix = y_pix;
+ new_rect.ch = ch;
+ new_rect.cw = cw;
+ new_rect.reverse = reverse;
+
+ // Display some red text in debug mode.
+ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
+ gr_displayinfo(buf, &new_rect, 0xFF000000, 0xFFFF0000, "? ");
+
+ // If it's the empty image (image_id=0) or an empty rectangle, do
+ // nothing.
+ if (image_id == 0 || img_end_col - img_start_col <= 0 ||
+ img_end_row - img_start_row <= 0)
+ return;
+ // Try to find a rect to merge with.
+ ImageRect *free_rect = NULL;
+ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
+ ImageRect *rect = &image_rects[i];
+ if (rect->image_id == 0) {
+ if (!free_rect)
+ free_rect = rect;
+ continue;
+ }
+ if (rect->image_id != image_id ||
+ rect->placement_id != placement_id || rect->cw != cw ||
+ rect->ch != ch || rect->reverse != reverse)
+ continue;
+ // We only support the case when the new stripe is added to the
+ // bottom of an existing rectangle and they are perfectly
+ // aligned.
+ if (rect->img_end_row == img_start_row &&
+ gr_getrectbottom(rect) == y_pix) {
+ if (rect->img_start_col == img_start_col &&
+ rect->img_end_col == img_end_col &&
+ rect->screen_x_pix == x_pix) {
+ rect->img_end_row = img_end_row;
+ return;
+ }
+ }
+ }
+ // If we haven't merged the new rect with any existing rect, and there
+ // is no free rect, we have to render one of the existing rects.
+ if (!free_rect) {
+ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
+ ImageRect *rect = &image_rects[i];
+ if (!free_rect || gr_getrectbottom(free_rect) >
+ gr_getrectbottom(rect))
+ free_rect = rect;
+ }
+ gr_drawimagerect(buf, free_rect);
+ gr_freerect(free_rect);
+ }
+ // Start a new rectangle in `free_rect`.
+ *free_rect = new_rect;
+}
+
+/// Mark rows containing animations as dirty if it's time to redraw them. Must
+/// be called right after `gr_start_drawing`.
+void gr_mark_dirty_animations(int *dirty, int rows) {
+ if (rows < kv_size(next_redraw_times))
+ kv_size(next_redraw_times) = rows;
+ if (rows * 2 < kv_max(next_redraw_times))
+ kv_resize(Milliseconds, next_redraw_times, rows);
+ for (int i = 0; i < MIN(rows, kv_size(next_redraw_times)); ++i) {
+ if (dirty[i]) {
+ kv_A(next_redraw_times, i) = 0;
+ continue;
+ }
+ Milliseconds next_update = kv_A(next_redraw_times, i);
+ if (next_update > 0 && next_update <= drawing_start_time) {
+ dirty[i] = 1;
+ kv_A(next_redraw_times, i) = 0;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Command parsing and handling.
+////////////////////////////////////////////////////////////////////////////////
+
+/// A parsed kitty graphics protocol command.
+typedef struct {
+ /// The command itself, without the 'G'.
+ char *command;
+ /// The payload (after ';').
+ char *payload;
+ /// 'a=', may be 't', 'q', 'f', 'T', 'p', 'd', 'a'.
+ char action;
+ /// 'q=', 1 to suppress OK response, 2 to suppress errors too.
+ int quiet;
+ /// 'f=', use 24 or 32 for raw pixel data, 100 to autodetect with
+ /// imlib2. If 'f=0', will try to load with imlib2, then fallback to
+ /// 32-bit pixel data.
+ int format;
+ /// 'o=', may be 'z' for RFC 1950 ZLIB.
+ int compression;
+ /// 't=', may be 'f', 't' or 'd'.
+ char transmission_medium;
+ /// 'd='
+ char delete_specifier;
+ /// 's=', 'v=', if 'a=t' or 'a=T', used only when 'f=24' or 'f=32'.
+ /// When 'a=f', this is the size of the frame rectangle when composed on
+ /// top of another frame.
+ int frame_pix_width, frame_pix_height;
+ /// 'x=', 'y=' - top-left corner of the source rectangle.
+ int src_pix_x, src_pix_y;
+ /// 'w=', 'h=' - width and height of the source rectangle.
+ int src_pix_width, src_pix_height;
+ /// 'r=', 'c='
+ int rows, columns;
+ /// 'i='
+ uint32_t image_id;
+ /// 'I='
+ uint32_t image_number;
+ /// 'p='
+ uint32_t placement_id;
+ /// 'm=', may be 0 or 1.
+ int more;
+ /// True if turns out that this command is a continuation of a data
+ /// transmission and not the first one for this image. Populated by
+ /// `gr_handle_transmit_command`.
+ char is_direct_transmission_continuation;
+ /// 'S=', used to check the size of uploaded data.
+ int size;
+ /// The offset of the frame image data in the shared memory ('O=').
+ unsigned offset;
+ /// 'U=', whether it's a virtual placement for Unicode placeholders.
+ int virtual;
+ /// 'C=', if true, do not move the cursor when displaying this placement
+ /// (non-virtual placements only).
+ char do_not_move_cursor;
+ // ---------------------------------------------------------------------
+ // Animation-related fields. Their keys often overlap with keys of other
+ // commands, so these make sense only if the action is 'a=f' (frame
+ // transmission) or 'a=a' (animation control).
+ //
+ // 'x=' and 'y=', the relative position of the frame image when it's
+ // composed on top of another frame.
+ int frame_dst_pix_x, frame_dst_pix_y;
+ /// 'X=', 'X=1' to replace colors instead of alpha blending on top of
+ /// the background color or frame.
+ char replace_instead_of_blending;
+ /// 'Y=', the background color in the 0xRRGGBBAA format (still
+ /// transmitted as a decimal number).
+ uint32_t background_color;
+ /// (Only for 'a=f'). 'c=', the 1-based index of the background frame.
+ int background_frame;
+ /// (Only for 'a=a'). 'c=', sets the index of the current frame.
+ int current_frame;
+ /// 'r=', the 1-based index of the frame to edit.
+ int edit_frame;
+ /// 'z=', the duration of the frame. Zero if not specified, negative if
+ /// the frame is gapless (i.e. skipped).
+ int gap;
+ /// (Only for 'a=a'). 's=', if non-zero, sets the state of the
+ /// animation, 1 to stop, 2 to run in loading mode, 3 to loop.
+ int animation_state;
+ /// (Only for 'a=a'). 'v=', if non-zero, sets the number of times the
+ /// animation will loop. 1 to loop infinitely, N to loop N-1 times.
+ int loops;
+} GraphicsCommand;
+
+/// Replaces all non-printed characters in `str` with '?' and truncates the
+/// string to `max_size`, maybe inserting ellipsis at the end.
+static void sanitize_str(char *str, size_t max_size) {
+ assert(max_size >= 4);
+ for (size_t i = 0; i < max_size; ++i) {
+ unsigned c = str[i];
+ if (c == '\0')
+ return;
+ if (c >= 128 || !isprint(c))
+ str[i] = '?';
+ }
+ str[max_size - 1] = '\0';
+ str[max_size - 2] = '.';
+ str[max_size - 3] = '.';
+ str[max_size - 4] = '.';
+}
+
+/// A non-destructive version of `sanitize_str`. Uses a static buffer, so be
+/// careful.
+static const char *sanitized_filename(const char *str) {
+ static char buf[MAX_FILENAME_SIZE];
+ strncpy(buf, str, sizeof(buf));
+ sanitize_str(buf, sizeof(buf));
+ return buf;
+}
+
+/// Creates a response to the current command in `graphics_command_result`.
+static void gr_createresponse(uint32_t image_id, uint32_t image_number,
+ uint32_t placement_id, const char *msg) {
+ if (!image_id && !image_number && !placement_id) {
+ // Nobody expects the response in this case, so just print it to
+ // stderr.
+ fprintf(stderr,
+ "error: No image id or image number or placement_id, "
+ "but still there is a response: %s\n",
+ msg);
+ return;
+ }
+ char *buf = graphics_command_result.response;
+ size_t maxlen = MAX_GRAPHICS_RESPONSE_LEN;
+ size_t written;
+ written = snprintf(buf, maxlen, "\033_G");
+ buf += written;
+ maxlen -= written;
+ if (image_id) {
+ written = snprintf(buf, maxlen, "i=%u,", image_id);
+ buf += written;
+ maxlen -= written;
+ }
+ if (image_number) {
+ written = snprintf(buf, maxlen, "I=%u,", image_number);
+ buf += written;
+ maxlen -= written;
+ }
+ if (placement_id) {
+ written = snprintf(buf, maxlen, "p=%u,", placement_id);
+ buf += written;
+ maxlen -= written;
+ }
+ buf[-1] = ';';
+ written = snprintf(buf, maxlen, "%s\033\\", msg);
+ buf += written;
+ maxlen -= written;
+ buf[-2] = '\033';
+ buf[-1] = '\\';
+}
+
+/// Creates the 'OK' response to the current command, unless suppressed or a
+/// non-final data transmission.
+static void gr_reportsuccess_cmd(GraphicsCommand *cmd) {
+ if (cmd->quiet < 1 && !cmd->more)
+ gr_createresponse(cmd->image_id, cmd->image_number,
+ cmd->placement_id, "OK");
+}
+
+/// Creates the 'OK' response to the current command (unless suppressed).
+static void gr_reportsuccess_frame(ImageFrame *frame) {
+ uint32_t id = frame->image->query_id ? frame->image->query_id
+ : frame->image->image_id;
+ if (frame->quiet < 1)
+ gr_createresponse(id, frame->image->image_number,
+ frame->image->initial_placement_id, "OK");
+}
+
+/// Creates an error response to the current command (unless suppressed).
+static void gr_reporterror_cmd(GraphicsCommand *cmd, const char *format, ...) {
+ char errmsg[MAX_GRAPHICS_RESPONSE_LEN];
+ graphics_command_result.error = 1;
+ va_list args;
+ va_start(args, format);
+ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args);
+ va_end(args);
+
+ fprintf(stderr, "%s in command: %s\n", errmsg, cmd->command);
+ if (cmd->quiet < 2)
+ gr_createresponse(cmd->image_id, cmd->image_number,
+ cmd->placement_id, errmsg);
+}
+
+/// Creates an error response to the current command (unless suppressed).
+static void gr_reporterror_frame(ImageFrame *frame, const char *format, ...) {
+ char errmsg[MAX_GRAPHICS_RESPONSE_LEN];
+ graphics_command_result.error = 1;
+ va_list args;
+ va_start(args, format);
+ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args);
+ va_end(args);
+
+ if (!frame) {
+ fprintf(stderr, "%s\n", errmsg);
+ gr_createresponse(0, 0, 0, errmsg);
+ } else {
+ uint32_t id = frame->image->query_id ? frame->image->query_id
+ : frame->image->image_id;
+ fprintf(stderr, "%s id=%u\n", errmsg, id);
+ if (frame->quiet < 2)
+ gr_createresponse(id, frame->image->image_number,
+ frame->image->initial_placement_id,
+ errmsg);
+ }
+}
+
+/// Loads an image and creates a success/failure response. Returns `frame`, or
+/// NULL if it's a query action and the image was deleted.
+static ImageFrame *gr_loadimage_and_report(ImageFrame *frame) {
+ gr_load_imlib_object(frame);
+ if (!frame->imlib_object) {
+ gr_reporterror_frame(frame, "EBADF: could not load image");
+ } else {
+ gr_reportsuccess_frame(frame);
+ }
+ // If it was a query action, discard the image.
+ if (frame->image->query_id) {
+ gr_delete_image(frame->image);
+ return NULL;
+ }
+ return frame;
+}
+
+/// Creates an appropriate uploading failure response to the current command.
+static void gr_reportuploaderror(ImageFrame *frame) {
+ switch (frame->uploading_failure) {
+ case 0:
+ return;
+ case ERROR_CANNOT_OPEN_CACHED_FILE:
+ gr_reporterror_frame(frame,
+ "EIO: could not create a file for image");
+ break;
+ case ERROR_OVER_SIZE_LIMIT:
+ gr_reporterror_frame(
+ frame,
+ "EFBIG: the size of the uploaded image exceeded "
+ "the image size limit %u",
+ graphics_max_single_image_file_size);
+ break;
+ case ERROR_UNEXPECTED_SIZE:
+ gr_reporterror_frame(frame,
+ "EINVAL: the size of the uploaded image %u "
+ "doesn't match the expected size %u",
+ frame->disk_size, frame->expected_size);
+ break;
+ };
+}
+
+/// Displays a non-virtual placement. This functions records the information in
+/// `graphics_command_result`, the placeholder itself is created by the terminal
+/// after handling the current command in the graphics module.
+static void gr_display_nonvirtual_placement(ImagePlacement *placement) {
+ if (placement->virtual)
+ return;
+ if (placement->image->first_frame.status < STATUS_RAM_LOADING_SUCCESS)
+ return;
+ // Infer the placement size if needed.
+ gr_infer_placement_size_maybe(placement);
+ // Populate the information about the placeholder which will be created
+ // by the terminal.
+ graphics_command_result.create_placeholder = 1;
+ graphics_command_result.placeholder.image_id = placement->image->image_id;
+ graphics_command_result.placeholder.placement_id = placement->placement_id;
+ graphics_command_result.placeholder.columns = placement->cols;
+ graphics_command_result.placeholder.rows = placement->rows;
+ graphics_command_result.placeholder.do_not_move_cursor =
+ placement->do_not_move_cursor;
+ placement->text_underneath =
+ calloc(placement->rows * placement->cols, sizeof(Glyph));
+ graphics_command_result.placeholder.text_underneath =
+ placement->text_underneath;
+ GR_LOG("Creating a placeholder for %u/%u %d x %d\n",
+ placement->image->image_id, placement->placement_id,
+ placement->cols, placement->rows);
+}
+
+/// Marks the rows that are occupied by the image as dirty.
+static void gr_schedule_image_redraw(Image *img) {
+ if (!img)
+ return;
+ gr_schedule_image_redraw_by_id(img->image_id);
+}
+
+/// Appends `data` to the on-disk cache file of the frame `frame`. Creates the
+/// file if it doesn't exist. Updates `frame->disk_size` and the total disk
+/// size. Returns 1 on success and 0 on failure.
+static int gr_append_raw_data_to_file(ImageFrame *frame, const char *data,
+ size_t data_size) {
+ // If there is no open file corresponding to the image, create it.
+ if (!frame->open_file) {
+ gr_make_sure_tmpdir_exists();
+ char filename[MAX_FILENAME_SIZE];
+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
+ FILE *file = fopen(filename, frame->disk_size ? "a" : "w");
+ if (!file)
+ return 0;
+ frame->open_file = file;
+ }
+
+ // Write data to the file and update disk size variables.
+ fwrite(data, 1, data_size, frame->open_file);
+ frame->disk_size += data_size;
+ frame->image->total_disk_size += data_size;
+ images_disk_size += data_size;
+ gr_touch_frame(frame);
+ return 1;
+}
+
+/// Appends data from `payload` to the frame `frame` when using direct
+/// transmission. Note that we report errors only for the final command
+/// (`!more`) to avoid spamming the client. If the frame is not specified, use
+/// the image id and frame index we are currently uploading.
+static void gr_append_data(ImageFrame *frame, const char *payload, int more) {
+ if (!frame) {
+ Image *img = gr_find_image(current_upload_image_id);
+ frame = gr_get_frame(img, current_upload_frame_index);
+ GR_LOG("Appending data to image %u frame %d\n",
+ current_upload_image_id, current_upload_frame_index);
+ if (!img)
+ GR_LOG("ERROR: this image doesn't exist\n");
+ if (!frame)
+ GR_LOG("ERROR: this frame doesn't exist\n");
+ }
+ if (!more) {
+ current_upload_image_id = 0;
+ current_upload_frame_index = 0;
+ }
+ if (!frame) {
+ if (!more)
+ gr_reporterror_frame(NULL, "ENOENT: could not find the "
+ "image to append data to");
+ return;
+ }
+ if (frame->status != STATUS_UPLOADING) {
+ if (!more)
+ gr_reportuploaderror(frame);
+ return;
+ }
+
+ // Decode the data.
+ size_t data_size = 0;
+ char *data = gr_base64dec(payload, &data_size);
+
+ GR_LOG("appending %u + %zu = %zu bytes\n", frame->disk_size, data_size,
+ frame->disk_size + data_size);
+
+ // Do not append this data if the image exceeds the size limit.
+ if (frame->disk_size + data_size >
+ graphics_max_single_image_file_size ||
+ frame->expected_size > graphics_max_single_image_file_size) {
+ free(data);
+ gr_delete_imagefile(frame);
+ frame->uploading_failure = ERROR_OVER_SIZE_LIMIT;
+ if (!more)
+ gr_reportuploaderror(frame);
+ return;
+ }
+
+ // Append the data to the file.
+ if (!gr_append_raw_data_to_file(frame, data, data_size)) {
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure = ERROR_CANNOT_OPEN_CACHED_FILE;
+ if (!more)
+ gr_reportuploaderror(frame);
+ return;
+ }
+ free(data);
+
+ if (more) {
+ current_upload_image_id = frame->image->image_id;
+ current_upload_frame_index = frame->index;
+ } else {
+ current_upload_image_id = 0;
+ current_upload_frame_index = 0;
+ // Close the file.
+ if (frame->open_file) {
+ fclose(frame->open_file);
+ frame->open_file = NULL;
+ }
+ frame->status = STATUS_UPLOADING_SUCCESS;
+ uint32_t placement_id = frame->image->default_placement;
+ if (frame->expected_size &&
+ frame->expected_size != frame->disk_size) {
+ // Report failure if the uploaded image size doesn't
+ // match the expected size.
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure = ERROR_UNEXPECTED_SIZE;
+ gr_reportuploaderror(frame);
+ } else {
+ // Make sure to redraw all existing image instances.
+ gr_schedule_image_redraw(frame->image);
+ // Try to load the image into ram and report the result.
+ frame = gr_loadimage_and_report(frame);
+ // If there is a non-virtual image placement, we may
+ // need to display it.
+ if (frame && frame->index == 1) {
+ Image *img = frame->image;
+ ImagePlacement *placement = NULL;
+ kh_foreach_value(img->placements, placement, {
+ gr_display_nonvirtual_placement(placement);
+ });
+ }
+ }
+ }
+
+ // Check whether we need to delete old images.
+ gr_check_limits();
+}
+
+/// Finds the image either by id or by number specified in the command.
+static Image *gr_find_image_for_command(GraphicsCommand *cmd) {
+ if (cmd->image_id)
+ return gr_find_image(cmd->image_id);
+ Image *img = NULL;
+ // If the image number is not specified, we can't find the image, unless
+ // it's a put command, in which case we will try the last image.
+ if (cmd->image_number == 0 && cmd->action == 'p')
+ img = gr_find_image(last_image_id);
+ else
+ img = gr_find_image_by_number(cmd->image_number);
+ return img;
+}
+
+/// Creates a new image or a new frame in an existing image (depending on the
+/// command's action) and initializes its parameters from the command.
+static ImageFrame *gr_new_image_or_frame_from_command(GraphicsCommand *cmd) {
+ if (cmd->format != 0 && cmd->format != 32 && cmd->format != 24 &&
+ cmd->compression != 0) {
+ gr_reporterror_cmd(cmd, "EINVAL: compression is supported only "
+ "for raw pixel data (f=32 or f=24)");
+ // Even though we report an error, we still create an image.
+ }
+
+ Image *img = NULL;
+ if (cmd->action == 'f') {
+ // If it's a frame transmission action, there must be an
+ // existing image.
+ img = gr_find_image_for_command(cmd);
+ if (img) {
+ cmd->image_id = img->image_id;
+ } else {
+ gr_reporterror_cmd(cmd, "ENOENT: image not found");
+ return NULL;
+ }
+ } else {
+ // Otherwise create a new image object. If the action is `q`,
+ // we'll use random id instead of the one specified in the
+ // command.
+ uint32_t image_id = cmd->action == 'q' ? 0 : cmd->image_id;
+ img = gr_new_image(image_id);
+ if (!img)
+ return NULL;
+ if (cmd->action == 'q')
+ img->query_id = cmd->image_id;
+ else if (!cmd->image_id)
+ cmd->image_id = img->image_id;
+ // Set the image number.
+ img->image_number = cmd->image_number;
+ }
+
+ ImageFrame *frame = gr_append_new_frame(img);
+ // Initialize the frame.
+ frame->expected_size = cmd->size;
+ // The default format is 32.
+ frame->format = cmd->format ? cmd->format : 32;
+ frame->compression = cmd->compression;
+ frame->background_color = cmd->background_color;
+ frame->background_frame_index = cmd->background_frame;
+ frame->gap = cmd->gap;
+ img->total_duration += frame->gap;
+ frame->blend = !cmd->replace_instead_of_blending;
+ frame->data_pix_width = cmd->frame_pix_width;
+ frame->data_pix_height = cmd->frame_pix_height;
+ if (cmd->action == 'f') {
+ frame->x = cmd->frame_dst_pix_x;
+ frame->y = cmd->frame_dst_pix_y;
+ }
+ // If the expected size is not specified, we can infer it from the pixel
+ // width and height if the format is 24 or 32 and there is no
+ // compression. This is required for the shared memory transmission.
+ if (!frame->expected_size && !frame->compression &&
+ (frame->format == 24 || frame->format == 32)) {
+ frame->expected_size = frame->data_pix_width *
+ frame->data_pix_height *
+ (frame->format / 8);
+ }
+ // We save the quietness information in the frame because for direct
+ // transmission subsequent transmission command won't contain this info.
+ frame->quiet = cmd->quiet;
+ return frame;
+}
+
+/// Removes a file if it actually looks like a temporary file.
+static void gr_delete_tmp_file(const char *filename) {
+ if (strstr(filename, "tty-graphics-protocol") == NULL)
+ return;
+ if (strstr(filename, "/tmp/") != filename) {
+ const char *tmpdir = getenv("TMPDIR");
+ if (!tmpdir || !tmpdir[0] ||
+ strstr(filename, tmpdir) != filename)
+ return;
+ }
+ unlink(filename);
+}
+
+/// Handles a data transmission command.
+static ImageFrame *gr_handle_transmit_command(GraphicsCommand *cmd) {
+ // The default is direct transmission.
+ if (!cmd->transmission_medium)
+ cmd->transmission_medium = 'd';
+
+ // If neither id, nor image number is specified, and the transmission
+ // medium is 'd' (or unspecified), and there is an active direct upload,
+ // this is a continuation of the upload.
+ if (current_upload_image_id != 0 && cmd->image_id == 0 &&
+ cmd->image_number == 0 && cmd->transmission_medium == 'd') {
+ cmd->image_id = current_upload_image_id;
+ GR_LOG("No images id is specified, continuing uploading %u\n",
+ cmd->image_id);
+ }
+
+ ImageFrame *frame = NULL;
+ if (cmd->transmission_medium == 'f' ||
+ cmd->transmission_medium == 't') {
+ // File transmission.
+ // Create a new image or a new frame of an existing image.
+ frame = gr_new_image_or_frame_from_command(cmd);
+ if (!frame)
+ return NULL;
+ last_image_id = frame->image->image_id;
+ // Decode the filename.
+ char *original_filename = gr_base64dec(cmd->payload, NULL);
+ GR_LOG("Copying image %s\n",
+ sanitized_filename(original_filename));
+ // Stat the file and check that it's a regular file and not too
+ // big.
+ struct stat st;
+ int stat_res = stat(original_filename, &st);
+ const char *stat_error = NULL;
+ if (stat_res)
+ stat_error = strerror(errno);
+ else if (!S_ISREG(st.st_mode))
+ stat_error = "Not a regular file";
+ else if (st.st_size == 0)
+ stat_error = "The size of the file is zero";
+ else if (st.st_size > graphics_max_single_image_file_size)
+ stat_error = "The file is too large";
+ if (stat_error) {
+ gr_reporterror_cmd(cmd,
+ "EBADF: %s", stat_error);
+ fprintf(stderr, "Could not load the file %s\n",
+ sanitized_filename(original_filename));
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure = ERROR_CANNOT_COPY_FILE;
+ } else {
+ gr_make_sure_tmpdir_exists();
+ // Build the filename for the cached copy of the file.
+ char cache_filename[MAX_FILENAME_SIZE];
+ gr_get_frame_filename(frame, cache_filename,
+ MAX_FILENAME_SIZE);
+ // We will create a symlink to the original file, and
+ // then copy the file to the temporary cache dir. We do
+ // this symlink trick mostly to be able to use cp for
+ // copying, and avoid escaping file name characters when
+ // calling system at the same time.
+ char tmp_filename_symlink[MAX_FILENAME_SIZE + 4] = {0};
+ strcat(tmp_filename_symlink, cache_filename);
+ strcat(tmp_filename_symlink, ".sym");
+ char command[MAX_FILENAME_SIZE + 256];
+ size_t len =
+ snprintf(command, MAX_FILENAME_SIZE + 255,
+ "cp '%s' '%s'", tmp_filename_symlink,
+ cache_filename);
+ if (len > MAX_FILENAME_SIZE + 255 ||
+ symlink(original_filename, tmp_filename_symlink) ||
+ system(command) != 0) {
+ gr_reporterror_cmd(cmd,
+ "EBADF: could not copy the "
+ "image to the cache dir");
+ fprintf(stderr,
+ "Could not copy the image "
+ "%s (symlink %s) to %s",
+ sanitized_filename(original_filename),
+ tmp_filename_symlink, cache_filename);
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure = ERROR_CANNOT_COPY_FILE;
+ } else {
+ // Get the file size of the copied file.
+ frame->status = STATUS_UPLOADING_SUCCESS;
+ frame->disk_size = st.st_size;
+ frame->image->total_disk_size += st.st_size;
+ images_disk_size += frame->disk_size;
+ if (frame->expected_size &&
+ frame->expected_size != frame->disk_size) {
+ // The file has unexpected size.
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure =
+ ERROR_UNEXPECTED_SIZE;
+ gr_reportuploaderror(frame);
+ } else {
+ // Everything seems fine, try to load
+ // and redraw existing instances.
+ gr_schedule_image_redraw(frame->image);
+ frame = gr_loadimage_and_report(frame);
+ }
+ }
+ // Delete the symlink.
+ unlink(tmp_filename_symlink);
+ // Delete the original file if it's temporary.
+ if (cmd->transmission_medium == 't')
+ gr_delete_tmp_file(original_filename);
+ }
+ free(original_filename);
+ gr_check_limits();
+ } else if (cmd->transmission_medium == 'd') {
+ // Direct transmission (default if 't' is not specified).
+ frame = gr_get_last_frame(gr_find_image_for_command(cmd));
+ if (frame && frame->status == STATUS_UPLOADING) {
+ // This is a continuation of the previous transmission.
+ cmd->is_direct_transmission_continuation = 1;
+ cmd->image_id = frame->image->image_id;
+ gr_append_data(frame, cmd->payload, cmd->more);
+ return frame;
+ }
+ // Otherwise create a new image or frame structure.
+ frame = gr_new_image_or_frame_from_command(cmd);
+ if (!frame)
+ return NULL;
+ last_image_id = frame->image->image_id;
+ frame->status = STATUS_UPLOADING;
+ // Start appending data.
+ gr_append_data(frame, cmd->payload, cmd->more);
+ } else if (cmd->transmission_medium == 's') {
+ // Shared memory transmission.
+ // Create a new image or a new frame of an existing image.
+ frame = gr_new_image_or_frame_from_command(cmd);
+ if (!frame)
+ return NULL;
+ last_image_id = frame->image->image_id;
+ // Check that we know the size.
+ if (!frame->expected_size) {
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure = ERROR_UNEXPECTED_SIZE;
+ gr_reporterror_cmd(
+ cmd, "EINVAL: the size of the image is not "
+ "specified and cannot be inferred");
+ return frame;
+ }
+ // Check the data size limit.
+ if (frame->expected_size > graphics_max_single_image_file_size) {
+ frame->uploading_failure = ERROR_OVER_SIZE_LIMIT;
+ gr_reportuploaderror(frame);
+ return frame;
+ }
+ // Decode the filename.
+ char *original_filename = gr_base64dec(cmd->payload, NULL);
+ GR_LOG("Loading image from shared memory %s\n",
+ sanitized_filename(original_filename));
+ // Open the shared memory object.
+ int fd = shm_open(original_filename, O_RDONLY, 0);
+ if (fd == -1) {
+ gr_reporterror_cmd(cmd, "EBADF: shm_open: %s",
+ strerror(errno));
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure = ERROR_CANNOT_OPEN_SHM;
+ fprintf(stderr, "shm_open failed for %s\n",
+ sanitized_filename(original_filename));
+ shm_unlink(original_filename);
+ free(original_filename);
+ return frame;
+ }
+ shm_unlink(original_filename);
+ free(original_filename);
+ // The offset we pass to mmap must be a multiple of the page
+ // size. If it's not, adjust it and the size.
+ size_t page_size = sysconf(_SC_PAGESIZE);
+ if (page_size == -1)
+ page_size = 1;
+ size_t offset = cmd->offset - (cmd->offset % page_size);
+ size_t size = frame->expected_size + (cmd->offset - offset);
+ // Map the shared memory object.
+ void *data =
+ mmap(NULL, size, PROT_READ, MAP_SHARED, fd, offset);
+ if (data == MAP_FAILED) {
+ gr_reporterror_cmd(cmd, "EBADF: mmap: %s",
+ strerror(errno));
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure = ERROR_CANNOT_OPEN_SHM;
+ fprintf(stderr,
+ "mmap failed for size = %ld, offset = %ld\n",
+ size, offset);
+ close(fd);
+ return frame;
+ }
+ close(fd);
+ // Append the data to the cache file.
+ if (gr_append_raw_data_to_file(frame,
+ data + (cmd->offset - offset),
+ frame->expected_size)) {
+ frame->status = STATUS_UPLOADING_SUCCESS;
+ } else {
+ frame->status = STATUS_UPLOADING_ERROR;
+ frame->uploading_failure =
+ ERROR_CANNOT_OPEN_CACHED_FILE;
+ gr_reportuploaderror(frame);
+ }
+ // Close the cache file.
+ if (frame->open_file) {
+ fclose(frame->open_file);
+ frame->open_file = NULL;
+ }
+ // Unmap the data
+ if (munmap(data, size) != 0)
+ fprintf(stderr, "munmap failed: %s\n", strerror(errno));
+ // Try to load and redraw existing instances.
+ gr_schedule_image_redraw(frame->image);
+ frame = gr_loadimage_and_report(frame);
+ gr_check_limits();
+ } else {
+ gr_reporterror_cmd(
+ cmd,
+ "EINVAL: transmission medium '%c' is not supported",
+ cmd->transmission_medium);
+ return NULL;
+ }
+
+ return frame;
+}
+
+/// Handles the 'put' command by creating a placement.
+static void gr_handle_put_command(GraphicsCommand *cmd) {
+ if (cmd->image_id == 0 && cmd->image_number == 0) {
+ gr_reporterror_cmd(cmd,
+ "EINVAL: neither image id nor image number "
+ "are specified or both are zero");
+ return;
+ }
+
+ // Find the image with the id or number.
+ Image *img = gr_find_image_for_command(cmd);
+ if (img) {
+ cmd->image_id = img->image_id;
+ } else {
+ gr_reporterror_cmd(cmd, "ENOENT: image not found");
+ return;
+ }
+
+ // Create a placement. If a placement with the same id already exists,
+ // it will be deleted. If the id is zero, a random id will be generated.
+ ImagePlacement *placement = gr_new_placement(img, cmd->placement_id);
+ placement->virtual = cmd->virtual;
+ placement->src_pix_x = cmd->src_pix_x;
+ placement->src_pix_y = cmd->src_pix_y;
+ placement->src_pix_width = cmd->src_pix_width;
+ placement->src_pix_height = cmd->src_pix_height;
+ placement->cols = cmd->columns;
+ placement->rows = cmd->rows;
+ placement->do_not_move_cursor = cmd->do_not_move_cursor;
+
+ if (placement->virtual) {
+ placement->scale_mode = SCALE_MODE_CONTAIN;
+ } else if (placement->cols && placement->rows) {
+ // For classic placements the default is to stretch the image if
+ // both cols and rows are specified.
+ placement->scale_mode = SCALE_MODE_FILL;
+ } else if (placement->cols || placement->rows) {
+ // But if only one of them is specified, the default is to
+ // contain.
+ placement->scale_mode = SCALE_MODE_CONTAIN;
+ } else {
+ // If none of them are specified, the default is to use the
+ // original size.
+ placement->scale_mode = SCALE_MODE_NONE;
+ }
+
+ // Display the placement unless it's virtual.
+ gr_display_nonvirtual_placement(placement);
+
+ // Report success.
+ gr_reportsuccess_cmd(cmd);
+}
+
+/// Information about what to delete.
+typedef struct DeletionData {
+ uint32_t image_id;
+ uint32_t placement_id;
+ /// Visible placement found during screen traversal that need to be
+ /// deleted. Each placement must occur only once in this vector.
+ ImagePlacementVec placements_to_delete;
+} DeletionData;
+
+/// The callback called for each cell to perform deletion.
+static int gr_deletion_callback(void *data, Glyph *gp) {
+ DeletionData *del_data = data;
+ // Leave unicode placeholders alone.
+ if (!tgetisclassicplaceholder(gp))
+ return 0;
+ uint32_t image_id = tgetimgid(gp);
+ uint32_t placement_id = tgetimgplacementid(gp);
+ if (del_data->image_id && del_data->image_id != image_id)
+ return 0;
+ if (del_data->placement_id && del_data->placement_id != placement_id)
+ return 0;
+
+ ImagePlacement *placement = NULL;
+
+ // Record the placement to delete. We will actually delete it later.
+ for (int i = 0; i < kv_size(del_data->placements_to_delete); ++i) {
+ ImagePlacement *cand = kv_A(del_data->placements_to_delete, i);
+ if (cand->image->image_id == image_id &&
+ cand->placement_id == placement_id) {
+ placement = cand;
+ break;
+ }
+ }
+ if (!placement) {
+ placement = gr_find_image_and_placement(image_id, placement_id);
+ if (placement) {
+ kv_push(ImagePlacement *,
+ del_data->placements_to_delete, placement);
+ }
+ }
+
+ // Restore the text underneath the placement if possible.
+ if (placement && placement->text_underneath) {
+ int row = tgetimgrow(gp) - 1;
+ int col = tgetimgcol(gp) - 1;
+ if (col >= 0 && row >= 0 && row < placement->rows &&
+ col < placement->cols) {
+ *gp = placement->text_underneath[row * placement->cols +
+ col];
+ return 1;
+ }
+ }
+
+ // Otherwise just erase the cell.
+ gp->mode = 0;
+ gp->u = ' ';
+ return 1;
+}
+
+/// Handles the delete command.
+static void gr_handle_delete_command(GraphicsCommand *cmd) {
+ DeletionData del_data = {0};
+ char delete_image_if_no_ref = isupper(cmd->delete_specifier) != 0;
+ char d = tolower(cmd->delete_specifier);
+
+ if (d == 'n') {
+ d = 'i';
+ Image *img = gr_find_image_by_number(cmd->image_number);
+ if (!img)
+ return;
+ del_data.image_id = img->image_id;
+ }
+
+ kv_init(del_data.placements_to_delete);
+
+ if (!d || d == 'a') {
+ // Delete all visible placements.
+ gr_for_each_image_cell(gr_deletion_callback, &del_data);
+ } else if (d == 'i') {
+ // Delete the specified image by image id and maybe placement
+ // id.
+ if (!del_data.image_id)
+ del_data.image_id = cmd->image_id;
+ if (!del_data.image_id) {
+ fprintf(stderr,
+ "ERROR: image id is not specified in the "
+ "delete command\n");
+ kv_destroy(del_data.placements_to_delete);
+ return;
+ }
+ del_data.placement_id = cmd->placement_id;
+ gr_for_each_image_cell(gr_deletion_callback, &del_data);
+ } else {
+ fprintf(stderr,
+ "WARNING: unsupported value of the d key: '%c'. The "
+ "command is ignored.\n",
+ cmd->delete_specifier);
+ }
+
+ // Delete the placements we have collected and maybe images too.
+ for (int i = 0; i < kv_size(del_data.placements_to_delete); ++i) {
+ ImagePlacement *placement =
+ kv_A(del_data.placements_to_delete, i);
+ // Delete the text underneath the placement and set it to NULL
+ // to avoid erasing it from the screen again.
+ free(placement->text_underneath);
+ placement->text_underneath = NULL;
+ Image *img = placement->image;
+ gr_delete_placement(placement);
+ // Delete the image if image deletion is requested (uppercase
+ // delete specifier) and there are no more placements.
+ if (delete_image_if_no_ref && kh_size(img->placements) == 0)
+ gr_delete_image(img);
+ }
+
+ // NOTE: It's not very clear whether we should delete the image
+ // even if there are no _visible_ placements to delete. We do
+ // this because otherwise there is no way to delete an image
+ // with virtual placements in one command.
+ if (d == 'i' && !del_data.placement_id && delete_image_if_no_ref)
+ gr_delete_image(gr_find_image(cmd->image_id));
+
+ kv_destroy(del_data.placements_to_delete);
+}
+
+/// Clears the cells occupied by the placement. This is normally done when
+/// implicitly deleting a classic placement.
+static void gr_erase_placement(ImagePlacement *placement) {
+ DeletionData del_data = {0};
+ del_data.image_id = placement->image->image_id;
+ del_data.placement_id = placement->placement_id;
+ kv_init(del_data.placements_to_delete);
+ gr_for_each_image_cell(gr_deletion_callback, &del_data);
+ // Delete the text underneath the placement and set it to NULL
+ // to avoid erasing it from the screen again.
+ free(placement->text_underneath);
+ placement->text_underneath = NULL;
+ kv_destroy(del_data.placements_to_delete);
+}
+
+static void gr_handle_animation_control_command(GraphicsCommand *cmd) {
+ if (cmd->image_id == 0 && cmd->image_number == 0) {
+ gr_reporterror_cmd(cmd,
+ "EINVAL: neither image id nor image number "
+ "are specified or both are zero");
+ return;
+ }
+
+ // Find the image with the id or number.
+ Image *img = gr_find_image_for_command(cmd);
+ if (img) {
+ cmd->image_id = img->image_id;
+ } else {
+ gr_reporterror_cmd(cmd, "ENOENT: image not found");
+ return;
+ }
+
+ // Find the frame to edit, if requested.
+ ImageFrame *frame = NULL;
+ if (cmd->edit_frame)
+ frame = gr_get_frame(img, cmd->edit_frame);
+ if (cmd->edit_frame || cmd->gap) {
+ if (!frame) {
+ gr_reporterror_cmd(cmd, "ENOENT: frame %d not found",
+ cmd->edit_frame);
+ return;
+ }
+ if (cmd->gap) {
+ img->total_duration -= frame->gap;
+ frame->gap = cmd->gap;
+ img->total_duration += frame->gap;
+ }
+ }
+
+ // Set animation-related parameters of the image.
+ if (cmd->current_frame)
+ img->current_frame = cmd->current_frame;
+ if (cmd->animation_state) {
+ if (cmd->animation_state == 1) {
+ img->animation_state = ANIMATION_STATE_STOPPED;
+ } else if (cmd->animation_state == 2) {
+ img->animation_state = ANIMATION_STATE_LOADING;
+ } else if (cmd->animation_state == 3) {
+ img->animation_state = ANIMATION_STATE_LOOPING;
+ } else {
+ gr_reporterror_cmd(
+ cmd, "EINVAL: invalid animation state: %d",
+ cmd->animation_state);
+ }
+ }
+ // TODO: Set the number of loops to cmd->loops
+
+ // Make sure we redraw all instances of the image.
+ gr_schedule_image_redraw(img);
+}
+
+/// Handles a command.
+static void gr_handle_command(GraphicsCommand *cmd) {
+ if (!cmd->image_id && !cmd->image_number) {
+ // If there is no image id or image number, nobody expects a
+ // response, so set quiet to 2.
+ cmd->quiet = 2;
+ }
+ ImageFrame *frame = NULL;
+ switch (cmd->action) {
+ case 0:
+ // If no action is specified, it is data transmission.
+ case 't':
+ case 'q':
+ case 'f':
+ // Transmit data. 'q' means query, which is basically the same
+ // as transmit, but the image is discarded, and the id is fake.
+ // 'f' appends a frame to an existing image.
+ gr_handle_transmit_command(cmd);
+ break;
+ case 'p':
+ // Display (put) the image.
+ gr_handle_put_command(cmd);
+ break;
+ case 'T':
+ // Transmit and display.
+ frame = gr_handle_transmit_command(cmd);
+ if (frame && !cmd->is_direct_transmission_continuation) {
+ gr_handle_put_command(cmd);
+ if (cmd->placement_id)
+ frame->image->initial_placement_id =
+ cmd->placement_id;
+ }
+ break;
+ case 'd':
+ gr_handle_delete_command(cmd);
+ break;
+ case 'a':
+ gr_handle_animation_control_command(cmd);
+ break;
+ default:
+ gr_reporterror_cmd(cmd, "EINVAL: unsupported action: %c",
+ cmd->action);
+ return;
+ }
+}
+
+/// A partially parsed key-value pair.
+typedef struct KeyAndValue {
+ char *key_start;
+ char *val_start;
+ unsigned key_len, val_len;
+} KeyAndValue;
+
+/// Parses the value of a key and assigns it to the appropriate field of `cmd`.
+static void gr_set_keyvalue(GraphicsCommand *cmd, KeyAndValue *kv) {
+ char *key_start = kv->key_start;
+ char *key_end = key_start + kv->key_len;
+ char *value_start = kv->val_start;
+ char *value_end = value_start + kv->val_len;
+ // Currently all keys are one-character.
+ if (key_end - key_start != 1) {
+ gr_reporterror_cmd(cmd, "EINVAL: unknown key of length %ld: %s",
+ key_end - key_start, key_start);
+ return;
+ }
+ long num = 0;
+ if (*key_start == 'a' || *key_start == 't' || *key_start == 'd' ||
+ *key_start == 'o') {
+ // Some keys have one-character values.
+ if (value_end - value_start != 1) {
+ gr_reporterror_cmd(
+ cmd,
+ "EINVAL: value of 'a', 't' or 'd' must be a "
+ "single char: %s",
+ key_start);
+ return;
+ }
+ } else {
+ // All the other keys have integer values.
+ char *num_end = NULL;
+ num = strtol(value_start, &num_end, 10);
+ if (num_end != value_end) {
+ gr_reporterror_cmd(
+ cmd, "EINVAL: could not parse number value: %s",
+ key_start);
+ return;
+ }
+ }
+ switch (*key_start) {
+ case 'a':
+ cmd->action = *value_start;
+ break;
+ case 't':
+ cmd->transmission_medium = *value_start;
+ break;
+ case 'd':
+ cmd->delete_specifier = *value_start;
+ break;
+ case 'q':
+ cmd->quiet = num;
+ break;
+ case 'f':
+ cmd->format = num;
+ if (num != 0 && num != 24 && num != 32 && num != 100) {
+ gr_reporterror_cmd(
+ cmd,
+ "EINVAL: unsupported format specification: %s",
+ key_start);
+ }
+ break;
+ case 'o':
+ cmd->compression = *value_start;
+ if (cmd->compression != 'z') {
+ gr_reporterror_cmd(cmd,
+ "EINVAL: unsupported compression "
+ "specification: %s",
+ key_start);
+ }
+ break;
+ case 's':
+ if (cmd->action == 'a')
+ cmd->animation_state = num;
+ else
+ cmd->frame_pix_width = num;
+ break;
+ case 'v':
+ if (cmd->action == 'a')
+ cmd->loops = num;
+ else
+ cmd->frame_pix_height = num;
+ break;
+ case 'i':
+ cmd->image_id = num;
+ break;
+ case 'I':
+ cmd->image_number = num;
+ break;
+ case 'p':
+ cmd->placement_id = num;
+ break;
+ case 'x':
+ cmd->src_pix_x = num;
+ cmd->frame_dst_pix_x = num;
+ break;
+ case 'y':
+ if (cmd->action == 'f')
+ cmd->frame_dst_pix_y = num;
+ else
+ cmd->src_pix_y = num;
+ break;
+ case 'w':
+ cmd->src_pix_width = num;
+ break;
+ case 'h':
+ cmd->src_pix_height = num;
+ break;
+ case 'c':
+ if (cmd->action == 'f')
+ cmd->background_frame = num;
+ else if (cmd->action == 'a')
+ cmd->current_frame = num;
+ else
+ cmd->columns = num;
+ break;
+ case 'r':
+ if (cmd->action == 'f' || cmd->action == 'a')
+ cmd->edit_frame = num;
+ else
+ cmd->rows = num;
+ break;
+ case 'm':
+ cmd->more = num;
+ break;
+ case 'S':
+ cmd->size = num;
+ break;
+ case 'O':
+ cmd->offset = num;
+ break;
+ case 'U':
+ cmd->virtual = num;
+ break;
+ case 'X':
+ if (cmd->action == 'f')
+ cmd->replace_instead_of_blending = num;
+ else
+ break; /*ignore*/
+ break;
+ case 'Y':
+ if (cmd->action == 'f')
+ cmd->background_color = num;
+ else
+ break; /*ignore*/
+ break;
+ case 'z':
+ if (cmd->action == 'f' || cmd->action == 'a')
+ cmd->gap = num;
+ else
+ break; /*ignore*/
+ break;
+ case 'C':
+ cmd->do_not_move_cursor = num;
+ break;
+ default:
+ gr_reporterror_cmd(cmd, "EINVAL: unsupported key: %s",
+ key_start);
+ return;
+ }
+}
+
+/// Parse and execute a graphics command. `buf` must start with 'G' and contain
+/// at least `len + 1` characters. Returns 1 on success.
+int gr_parse_command(char *buf, size_t len) {
+ if (buf[0] != 'G')
+ return 0;
+
+ Milliseconds command_start_time = gr_now_ms();
+ debug_loaded_files_counter = 0;
+ debug_loaded_pixmaps_counter = 0;
+ global_command_counter++;
+ GR_LOG("### Command %lu: %.80s\n", global_command_counter, buf);
+
+ memset(&graphics_command_result, 0, sizeof(GraphicsCommandResult));
+
+ // Eat the 'G'.
+ ++buf;
+ --len;
+
+ GraphicsCommand cmd = {.command = buf};
+ // The state of parsing. 'k' to parse key, 'v' to parse value, 'p' to
+ // parse the payload.
+ char state = 'k';
+ // An array of partially parsed key-value pairs.
+ KeyAndValue key_vals[32];
+ unsigned key_vals_count = 0;
+ char *key_start = buf;
+ char *key_end = NULL;
+ char *val_start = NULL;
+ char *val_end = NULL;
+ char *c = buf;
+ while (c - buf < len + 1) {
+ if (state == 'k') {
+ switch (*c) {
+ case ',':
+ case ';':
+ case '\0':
+ state = *c == ',' ? 'k' : 'p';
+ key_end = c;
+ gr_reporterror_cmd(
+ &cmd, "EINVAL: key without value: %s ",
+ key_start);
+ break;
+ case '=':
+ key_end = c;
+ state = 'v';
+ val_start = c + 1;
+ break;
+ default:
+ break;
+ }
+ } else if (state == 'v') {
+ switch (*c) {
+ case ',':
+ case ';':
+ case '\0':
+ state = *c == ',' ? 'k' : 'p';
+ val_end = c;
+ if (key_vals_count >=
+ sizeof(key_vals) / sizeof(*key_vals)) {
+ gr_reporterror_cmd(&cmd,
+ "EINVAL: too many "
+ "key-value pairs");
+ break;
+ }
+ key_vals[key_vals_count].key_start = key_start;
+ key_vals[key_vals_count].val_start = val_start;
+ key_vals[key_vals_count].key_len =
+ key_end - key_start;
+ key_vals[key_vals_count].val_len =
+ val_end - val_start;
+ ++key_vals_count;
+ key_start = c + 1;
+ break;
+ default:
+ break;
+ }
+ } else if (state == 'p') {
+ cmd.payload = c;
+ // break out of the loop, we don't check the payload
+ break;
+ }
+ ++c;
+ }
+
+ // Set the action key ('a=') first because we need it to disambiguate
+ // some keys. Also set 'i=' and 'I=' for better error reporting.
+ for (unsigned i = 0; i < key_vals_count; ++i) {
+ if (key_vals[i].key_len == 1) {
+ char *start = key_vals[i].key_start;
+ if (*start == 'a' || *start == 'i' || *start == 'I') {
+ gr_set_keyvalue(&cmd, &key_vals[i]);
+ break;
+ }
+ }
+ }
+ // Set the rest of the keys.
+ for (unsigned i = 0; i < key_vals_count; ++i)
+ gr_set_keyvalue(&cmd, &key_vals[i]);
+
+ if (!cmd.payload)
+ cmd.payload = buf + len;
+
+ if (cmd.payload && cmd.payload[0])
+ GR_LOG(" payload size: %ld\n", strlen(cmd.payload));
+
+ if (!graphics_command_result.error)
+ gr_handle_command(&cmd);
+
+ if (graphics_debug_mode) {
+ fprintf(stderr, "Response: ");
+ for (const char *resp = graphics_command_result.response;
+ *resp != '\0'; ++resp) {
+ if (isprint(*resp))
+ fprintf(stderr, "%c", *resp);
+ else
+ fprintf(stderr, "(0x%x)", *resp);
+ }
+ fprintf(stderr, "\n");
+ }
+
+ // Make sure that we suppress response if needed. Usually cmd.quiet is
+ // taken into account when creating the response, but it's not very
+ // reliable in the current implementation.
+ if (cmd.quiet) {
+ if (!graphics_command_result.error || cmd.quiet >= 2)
+ graphics_command_result.response[0] = '\0';
+ }
+
+ Milliseconds command_end_time = gr_now_ms();
+ GR_LOG("Command %lu took %ld ms (loaded %d files, %d pixmaps)\n\n",
+ global_command_counter, command_end_time - command_start_time,
+ debug_loaded_files_counter, debug_loaded_pixmaps_counter);
+
+ return 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// base64 decoding part is basically copied from st.c
+////////////////////////////////////////////////////////////////////////////////
+
+static const char gr_base64_digits[] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54,
+ 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, -1, 0, 0, 0, 0, 1, 2,
+ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
+ 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30,
+ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
+ 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+
+static char gr_base64_getc(const char **src) {
+ while (**src && !isprint(**src))
+ (*src)++;
+ return **src ? *((*src)++) : '='; /* emulate padding if string ends */
+}
+
+char *gr_base64dec(const char *src, size_t *size) {
+ size_t in_len = strlen(src);
+ char *result, *dst;
+
+ result = dst = malloc((in_len + 3) / 4 * 3 + 1);
+ while (*src) {
+ int a = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
+ int b = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
+ int c = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
+ int d = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
+
+ if (a == -1 || b == -1)
+ break;
+
+ *dst++ = (a << 2) | ((b & 0x30) >> 4);
+ if (c == -1)
+ break;
+ *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2);
+ if (d == -1)
+ break;
+ *dst++ = ((c & 0x03) << 6) | d;
+ }
+ *dst = '\0';
+ if (size) {
+ *size = dst - result;
+ }
+ return result;
+}