3813 lines
125 KiB
C
3813 lines
125 KiB
C
|
/* 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.
|
||
|
//
|
||
|
////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
#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 <spawn.h>
|
||
|
#include <stdarg.h>
|
||
|
#include <stdio.h>
|
||
|
#include <stdlib.h>
|
||
|
#include <string.h>
|
||
|
#include <sys/stat.h>
|
||
|
#include <time.h>
|
||
|
#include <unistd.h>
|
||
|
#include <errno.h>
|
||
|
|
||
|
#include "graphics.h"
|
||
|
#include "khash.h"
|
||
|
#include "kvec.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,
|
||
|
};
|
||
|
|
||
|
const char *image_uploading_failure_strings[5] = {
|
||
|
"NO_ERROR",
|
||
|
"ERROR_OVER_SIZE_LIMIT",
|
||
|
"ERROR_CANNOT_OPEN_CACHED_FILE",
|
||
|
"ERROR_UNEXPECTED_SIZE",
|
||
|
"ERROR_CANNOT_COPY_FILE",
|
||
|
};
|
||
|
|
||
|
////////////////////////////////////////////////////////////////////////////////
|
||
|
//
|
||
|
// 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;
|
||
|
} 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_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.
|
||
|
static int this_redraw_cycle_loaded_files = 0;
|
||
|
/// The number of pixmaps loaded in the current redraw cycle.
|
||
|
static int this_redraw_cycle_loaded_pixmaps = 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);
|
||
|
}
|
||
|
|
||
|
/// 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, 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);
|
||
|
gr_unload_placement(placement);
|
||
|
kv_destroy(placement->pixmaps_beyond_the_first);
|
||
|
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, 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)
|
||
|
continue;
|
||
|
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) {
|
||
|
GR_LOG("After cleaning: ram: %ld KiB disk: %ld KiB "
|
||
|
"img count: %d placement count: %d\n",
|
||
|
images_ram_size / 1024, images_disk_size / 1024,
|
||
|
kh_size(images), total_placement_count);
|
||
|
}
|
||
|
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();
|
||
|
|
||
|
// The default format is 32.
|
||
|
int format = frame->format ? frame->format : 32;
|
||
|
|
||
|
if (frame->compression == 0) {
|
||
|
gr_load_raw_pixel_data_uncompressed(data, file, format,
|
||
|
total_pixels);
|
||
|
} else {
|
||
|
int ret = gr_load_raw_pixel_data_compressed(data, file, 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;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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->format == 0)
|
||
|
frame_data_image = imlib_load_image(filename);
|
||
|
if (frame->format == 32 || frame->format == 24 ||
|
||
|
(!frame_data_image && frame->format == 0))
|
||
|
frame_data_image = gr_load_raw_pixel_data(frame, filename);
|
||
|
this_redraw_cycle_loaded_files++;
|
||
|
|
||
|
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;
|
||
|
|
||
|
GR_LOG("After loading image %u frame %d ram: %ld KiB (+ %u KiB)\n",
|
||
|
frame->image->image_id, frame->index,
|
||
|
images_ram_size / 1024, gr_frame_current_ram_size(frame) / 1024);
|
||
|
}
|
||
|
|
||
|
/// 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) {
|
||
|
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;
|
||
|
XMatchVisualInfo(disp, DefaultScreen(disp), 32, TrueColor, &visinfo);
|
||
|
XImage *ximage = XCreateImage(disp, visinfo.visual, 32, ZPixmap, 0,
|
||
|
(char *)data, scaled_w, scaled_h, 32, 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);
|
||
|
this_redraw_cycle_loaded_pixmaps++;
|
||
|
|
||
|
GR_LOG("After loading placement %u/%u frame %d ram: %ld KiB (+ %u "
|
||
|
"KiB)\n",
|
||
|
frame->image->image_id, placement->placement_id, frame->index,
|
||
|
images_ram_size / 1024,
|
||
|
gr_placement_single_frame_ram_size(placement) / 1024);
|
||
|
|
||
|
// 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);
|
||
|
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;
|
||
|
this_redraw_cycle_loaded_files = 0;
|
||
|
this_redraw_cycle_loaded_pixmaps = 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, this_redraw_cycle_loaded_files,
|
||
|
this_redraw_cycle_loaded_pixmaps);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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 either 'm=0' or 'm=1' is specified.
|
||
|
char is_data_transmission;
|
||
|
/// 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;
|
||
|
/// '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;
|
||
|
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 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;
|
||
|
}
|
||
|
|
||
|
// 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) {
|
||
|
frame->status = STATUS_UPLOADING_ERROR;
|
||
|
frame->uploading_failure = ERROR_CANNOT_OPEN_CACHED_FILE;
|
||
|
if (!more)
|
||
|
gr_reportuploaderror(frame);
|
||
|
return;
|
||
|
}
|
||
|
frame->open_file = file;
|
||
|
}
|
||
|
|
||
|
// Write data to the file and update disk size variables.
|
||
|
fwrite(data, 1, data_size, frame->open_file);
|
||
|
free(data);
|
||
|
frame->disk_size += data_size;
|
||
|
frame->image->total_disk_size += data_size;
|
||
|
images_disk_size += data_size;
|
||
|
gr_touch_frame(frame);
|
||
|
|
||
|
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 and sets
|
||
|
/// the image_id of `cmd` if the image was found.
|
||
|
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);
|
||
|
if (img)
|
||
|
cmd->image_id = img->image_id;
|
||
|
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) {
|
||
|
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;
|
||
|
frame->format = cmd->format;
|
||
|
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;
|
||
|
}
|
||
|
// 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;
|
||
|
gr_append_data(frame, cmd->payload, cmd->more);
|
||
|
return frame;
|
||
|
}
|
||
|
// If no action is specified, it's not the first transmission
|
||
|
// command. If we couldn't find the image, something went wrong
|
||
|
// and we should just drop this command.
|
||
|
if (cmd->action == 0)
|
||
|
return NULL;
|
||
|
// 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 {
|
||
|
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) {
|
||
|
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;
|
||
|
/// If true, delete the image object if there are no more placements.
|
||
|
char delete_image_if_no_ref;
|
||
|
} DeletionData;
|
||
|
|
||
|
/// The callback called for each cell to perform deletion.
|
||
|
static int gr_deletion_callback(void *data, uint32_t image_id,
|
||
|
uint32_t placement_id, int col,
|
||
|
int row, char is_classic) {
|
||
|
DeletionData *del_data = data;
|
||
|
// Leave unicode placeholders alone.
|
||
|
if (!is_classic)
|
||
|
return 0;
|
||
|
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;
|
||
|
Image *img = gr_find_image(image_id);
|
||
|
// If the image is already deleted, just erase the placeholder.
|
||
|
if (!img)
|
||
|
return 1;
|
||
|
// Delete the placement.
|
||
|
if (placement_id)
|
||
|
gr_delete_placement(gr_find_placement(img, placement_id));
|
||
|
// Delete the image if image deletion is requested (uppercase delete
|
||
|
// specifier) and there are no more placements.
|
||
|
if (del_data->delete_image_if_no_ref && kh_size(img->placements) == 0)
|
||
|
gr_delete_image(img);
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
/// Handles the delete command.
|
||
|
static void gr_handle_delete_command(GraphicsCommand *cmd) {
|
||
|
DeletionData del_data = {0};
|
||
|
del_data.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;
|
||
|
}
|
||
|
|
||
|
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");
|
||
|
return;
|
||
|
}
|
||
|
del_data.placement_id = cmd->placement_id;
|
||
|
// 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 (!del_data.placement_id && del_data.delete_image_if_no_ref)
|
||
|
gr_delete_image(gr_find_image(cmd->image_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);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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) {
|
||
|
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 may be a data transmission
|
||
|
// command if 'm=' is specified.
|
||
|
if (cmd->is_data_transmission) {
|
||
|
gr_handle_transmit_command(cmd);
|
||
|
break;
|
||
|
}
|
||
|
gr_reporterror_cmd(cmd, "EINVAL: no action specified");
|
||
|
break;
|
||
|
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->is_data_transmission = 1;
|
||
|
cmd->more = num;
|
||
|
break;
|
||
|
case 'S':
|
||
|
cmd->size = 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;
|
||
|
|
||
|
memset(&graphics_command_result, 0, sizeof(GraphicsCommandResult));
|
||
|
|
||
|
global_command_counter++;
|
||
|
GR_LOG("### Command %lu: %.80s\n", global_command_counter, buf);
|
||
|
|
||
|
// 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';
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
}
|