labwc/src/img/img-xpm.c

400 lines
8.5 KiB
C

// SPDX-License-Identifier: LGPL-2.0-or-later
/*
* XPM image loader adapted from gdk-pixbuf
*
* Copyright (C) 1999 Mark Crichton
* Copyright (C) 1999 The Free Software Foundation
*
* Authors: Mark Crichton <crichton@gimp.org>
* Federico Mena-Quintero <federico@gimp.org>
*
* Adapted for labwc by John Lindgren, 2024
*/
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wlr/util/log.h>
#include "buffer.h"
#include "common/buf.h"
#include "common/graphic-helpers.h"
#include "common/mem.h"
#include "img/img-xpm.h"
enum buf_op { op_header, op_cmap, op_body };
struct xpm_color {
char *color_string;
uint32_t argb;
};
struct file_handle {
FILE *infile;
struct buf buf;
};
static inline uint32_t
make_argb(uint8_t a, uint8_t r, uint8_t g, uint8_t b)
{
return ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}
static bool
parse_color(const char *spec, uint32_t *argb)
{
if (spec[0] != '#') {
return lookup_named_color(spec, argb);
}
int red, green, blue;
switch (strlen(spec + 1)) {
case 3:
if (sscanf(spec + 1, "%1x%1x%1x", &red, &green, &blue) != 3) {
return false;
}
*argb = make_argb(255, (red * 255) / 15, (green * 255) / 15,
(blue * 255) / 15);
return true;
case 6:
if (sscanf(spec + 1, "%2x%2x%2x", &red, &green, &blue) != 3) {
return false;
}
*argb = make_argb(255, red, green, blue);
return true;
case 9:
if (sscanf(spec + 1, "%3x%3x%3x", &red, &green, &blue) != 3) {
return false;
}
*argb = make_argb(255, (red * 255) / 4095, (green * 255) / 4095,
(blue * 255) / 4095);
return true;
case 12:
if (sscanf(spec + 1, "%4x%4x%4x", &red, &green, &blue) != 3) {
return false;
}
*argb = make_argb(255, (red * 255) / 65535,
(green * 255) / 65535, (blue * 255) / 65535);
return true;
default:
return false;
}
}
static bool
xpm_seek_string(FILE *infile, const char *str)
{
char instr[1024];
while (!feof(infile)) {
if (fscanf(infile, "%1023s", instr) < 0) {
return false;
}
if (strcmp(instr, str) == 0) {
return true;
}
}
return false;
}
static bool
xpm_seek_char(FILE *infile, char c)
{
int b, oldb;
while ((b = getc(infile)) != EOF) {
if (c != b && b == '/') {
b = getc(infile);
if (b == EOF) {
return false;
} else if (b == '*') { /* we have a comment */
b = -1;
do {
oldb = b;
b = getc(infile);
if (b == EOF) {
return false;
}
} while (!(oldb == '*' && b == '/'));
}
} else if (c == b) {
return true;
}
}
return false;
}
static bool
xpm_read_string(FILE *infile, struct buf *buf)
{
buf_clear(buf);
int c;
do {
c = getc(infile);
if (c == EOF) {
return false;
}
} while (c != '"');
while ((c = getc(infile)) != EOF) {
if (c == '"') {
return true;
}
buf_add_char(buf, c);
}
return false;
}
static uint32_t
xpm_extract_color(const char *buffer)
{
const char *p = buffer;
int new_key = 0;
int key = 0;
int current_key = 1;
char word[129], color[129], current_color[129];
char *r;
word[0] = '\0';
color[0] = '\0';
current_color[0] = '\0';
while (true) {
/* skip whitespace */
for (; *p != '\0' && g_ascii_isspace(*p); p++) {
/* nothing */
}
/* copy word */
for (r = word; *p != '\0' && !g_ascii_isspace(*p)
&& r - word < (int)sizeof(word) - 1;
p++, r++) {
*r = *p;
}
*r = '\0';
if (*word == '\0') {
if (color[0] == '\0') { /* incomplete colormap entry */
return 0;
} else { /* end of entry, still store the last color */
new_key = 1;
}
} else if (key > 0 && color[0] == '\0') {
/* next word must be a color name part */
new_key = 0;
} else {
if (strcmp(word, "c") == 0) {
new_key = 5;
} else if (strcmp(word, "g") == 0) {
new_key = 4;
} else if (strcmp(word, "g4") == 0) {
new_key = 3;
} else if (strcmp(word, "m") == 0) {
new_key = 2;
} else if (strcmp(word, "s") == 0) {
new_key = 1;
} else {
new_key = 0;
}
}
if (new_key == 0) { /* word is a color name part */
if (key == 0) { /* key expected */
return 0;
}
/* accumulate color name */
int len = strlen(color);
if (len && len < (int)sizeof(color) - 1) {
color[len++] = ' ';
}
g_strlcpy(color + len, word, sizeof(color) - len);
} else { /* word is a key */
if (key > current_key) {
current_key = key;
g_strlcpy(current_color, color, sizeof(current_color));
}
color[0] = '\0';
key = new_key;
if (*p == '\0') {
break;
}
}
}
uint32_t argb;
if (current_key > 1 && (g_ascii_strcasecmp(current_color, "None") != 0)
&& parse_color(current_color, &argb)) {
return argb;
} else {
return 0;
}
}
static const char *
file_buffer(enum buf_op op, struct file_handle *h)
{
switch (op) {
case op_header:
if (!xpm_seek_string(h->infile, "XPM")) {
break;
}
if (!xpm_seek_char(h->infile, '{')) {
break;
}
/* Fall through to the next xpm_seek_char. */
case op_cmap:
xpm_seek_char(h->infile, '"');
if (fseek(h->infile, -1, SEEK_CUR) != 0) {
return NULL;
}
/* Fall through to the xpm_read_string. */
case op_body:
if (!xpm_read_string(h->infile, &h->buf)) {
return NULL;
}
return h->buf.data;
default:
g_assert_not_reached();
}
return NULL;
}
static cairo_surface_t *
xpm_load_to_surface(struct file_handle *handle)
{
const char *buffer = file_buffer(op_header, handle);
if (!buffer) {
wlr_log(WLR_DEBUG, "No XPM header found");
return NULL;
}
int w, h, n_col, cpp, x_hot, y_hot;
int items = sscanf(buffer, "%d %d %d %d %d %d", &w, &h, &n_col, &cpp,
&x_hot, &y_hot);
if (items != 4 && items != 6) {
wlr_log(WLR_DEBUG, "Invalid XPM header");
return NULL;
}
if (w <= 0) {
wlr_log(WLR_DEBUG, "XPM file has image width <= 0");
return NULL;
}
if (h <= 0) {
wlr_log(WLR_DEBUG, "XPM file has image height <= 0");
return NULL;
}
/* Limits (width, height, colors) modified for labwc */
if (h > 1024 || w > 1024) {
wlr_log(WLR_DEBUG, "XPM file is larger than 1024x1024");
return NULL;
}
if (cpp <= 0 || cpp >= 32) {
wlr_log(WLR_DEBUG, "XPM has invalid number of chars per pixel");
return NULL;
}
if (n_col <= 0 || n_col > 1024) {
wlr_log(WLR_DEBUG, "XPM file has invalid number of colors");
return NULL;
}
/* The hash is used for fast lookups of color from chars */
GHashTable *color_hash = g_hash_table_new(g_str_hash, g_str_equal);
char *name_buf = xzalloc(n_col * (cpp + 1));
struct xpm_color *colors = znew_n(struct xpm_color, n_col);
cairo_surface_t *surface = NULL;
struct xpm_color *fallbackcolor = NULL;
char pixel_str[32]; /* cpp < 32 */
for (int cnt = 0; cnt < n_col; cnt++) {
buffer = file_buffer(op_cmap, handle);
if (!buffer) {
wlr_log(WLR_DEBUG, "Cannot read XPM colormap");
goto out;
}
struct xpm_color *color = &colors[cnt];
color->color_string = &name_buf[cnt * (cpp + 1)];
g_strlcpy(color->color_string, buffer, cpp + 1);
buffer += strlen(color->color_string);
color->argb = xpm_extract_color(buffer);
g_hash_table_insert(color_hash, color->color_string, color);
if (cnt == 0) {
fallbackcolor = color;
}
}
surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
uint32_t *data = (uint32_t *)cairo_image_surface_get_data(surface);
int stride = cairo_image_surface_get_stride(surface) / sizeof(uint32_t);
for (int ycnt = 0; ycnt < h; ycnt++) {
uint32_t *pixtmp = data + stride * ycnt;
int wbytes = w * cpp;
buffer = file_buffer(op_body, handle);
if (!buffer || (strlen(buffer) < (size_t)wbytes)) {
/* Advertised width doesn't match pixels */
wlr_log(WLR_DEBUG, "Dimensions do not match data");
cairo_surface_destroy(surface);
surface = NULL;
goto out;
}
for (int n = 0, xcnt = 0; n < wbytes; n += cpp, xcnt++) {
g_strlcpy(pixel_str, &buffer[n], cpp + 1);
struct xpm_color *color =
g_hash_table_lookup(color_hash, pixel_str);
/* Bad XPM...punt */
if (!color) {
color = fallbackcolor;
}
*pixtmp++ = color->argb;
}
}
/* let cairo know pixel data has been modified */
cairo_surface_mark_dirty(surface);
out:
g_hash_table_destroy(color_hash);
free(colors);
free(name_buf);
return surface;
}
struct lab_data_buffer *
img_xpm_load(const char *filename)
{
struct file_handle h = {0};
h.infile = fopen(filename, "rb");
if (!h.infile) {
wlr_log(WLR_ERROR, "error opening '%s'", filename);
return NULL;
}
cairo_surface_t *surface = xpm_load_to_surface(&h);
struct lab_data_buffer *buffer = NULL;
if (surface) {
buffer = buffer_adopt_cairo_surface(surface);
} else {
wlr_log(WLR_ERROR, "error loading '%s'", filename);
}
fclose(h.infile);
buf_reset(&h.buf);
return buffer;
}