From e8b123e0560824d256538c51d7a1cb7853fe6f8d Mon Sep 17 00:00:00 2001 From: luboslenco Date: Thu, 16 Apr 2026 20:42:30 +0200 Subject: [PATCH] paint: import psd layers --- paint/plugins/io_psd/io_psd.c | 489 +++++++++++++++++++++++++++++++++ paint/plugins/plugins.c | 42 ++- paint/plugins/project.js | 1 + paint/sources/import_texture.c | 6 +- 4 files changed, 532 insertions(+), 6 deletions(-) create mode 100644 paint/plugins/io_psd/io_psd.c diff --git a/paint/plugins/io_psd/io_psd.c b/paint/plugins/io_psd/io_psd.c new file mode 100644 index 00000000..2ee88f5d --- /dev/null +++ b/paint/plugins/io_psd/io_psd.c @@ -0,0 +1,489 @@ +#include "iron_array.h" +#include "iron_gpu.h" +#include +#include +#include + +void *gpu_create_texture_from_bytes(void *buffer, int width, int height, int format); +void console_info(char *s); +void io_psd_import_layer(char *file_name, char *layer_name, void *tex); + +static uint16_t psd_r16(uint8_t *buf, size_t pos) { + return (uint16_t)(buf[pos] << 8 | buf[pos + 1]); +} + +static uint32_t psd_r32(uint8_t *buf, size_t pos) { + return (uint32_t)(buf[pos] << 24 | buf[pos + 1] << 16 | buf[pos + 2] << 8 | buf[pos + 3]); +} + +static uint64_t psd_r64(uint8_t *buf, size_t pos) { + return ((uint64_t)psd_r32(buf, pos) << 32) | psd_r32(buf, pos + 4); +} + +static void packbits_decode(uint8_t *in, size_t in_len, uint8_t *out, size_t out_len) { + size_t ip = 0, op = 0; + while (ip < in_len && op < out_len) { + int8_t n = (int8_t)in[ip++]; + if (n >= 0) { + size_t cnt = (size_t)(n + 1); + if (op + cnt > out_len) + cnt = out_len - op; + memcpy(out + op, in + ip, cnt); + ip += (size_t)(n + 1); + op += cnt; + } + else if (n != -128) { + size_t cnt = (size_t)(-n + 1); + if (op + cnt > out_len) + cnt = out_len - op; + memset(out + op, in[ip++], cnt); + op += cnt; + } + } +} + +// Assemble one pixel from planar channel buffers into RGBA8 +// bufs[c] is the plane for channel c; r/g/b/a_c are channel indices (-1 = missing) +static void psd_pixel(uint8_t *dst, uint8_t **bufs, int r_c, int g_c, int b_c, int a_c, size_t src_i, uint16_t depth, uint16_t color_mode) { + uint8_t r = 0, g = 0, b = 0, a = 255; +#define CHAN8(c) ((c) >= 0 && bufs[c] ? bufs[c][src_i] : 0) +#define CHAN16(c) ((c) >= 0 && bufs[c] ? (uint8_t)(psd_r16(bufs[c], src_i) / 257) : 0) +#define CHANF(c) ((c) >= 0 && bufs[c] ? *(float *)(bufs[c] + src_i) : 0.0f) +#define CLAMP01(v) ((v) < 0.0f ? 0.0f : (v) > 1.0f ? 1.0f : (v)) + + if (color_mode == 1) { // Grayscale + uint8_t v = (depth == 16) ? (uint8_t)(psd_r16(bufs[0], src_i) / 257) : bufs[0][src_i]; + if (depth == 32) { + float fv = *(float *)(bufs[0] + src_i); + v = (uint8_t)(CLAMP01(fv) * 255.0f); + } + r = g = b = v; + if (a_c >= 0 && bufs[a_c]) { + if (depth == 8) + a = bufs[a_c][src_i]; + else if (depth == 16) + a = (uint8_t)(psd_r16(bufs[a_c], src_i) / 257); + else if (depth == 32) + a = (uint8_t)(CLAMP01(*(float *)(bufs[a_c] + src_i)) * 255.0f); + } + } + else { + if (depth == 8) { + r = CHAN8(r_c); + g = CHAN8(g_c); + b = CHAN8(b_c); + a = (a_c >= 0 && bufs[a_c]) ? bufs[a_c][src_i] : 255; + } + else if (depth == 16) { + r = CHAN16(r_c); + g = CHAN16(g_c); + b = CHAN16(b_c); + a = (a_c >= 0 && bufs[a_c]) ? (uint8_t)(psd_r16(bufs[a_c], src_i) / 257) : 255; + } + else if (depth == 32) { + r = (uint8_t)(CLAMP01(CHANF(r_c)) * 255.0f); + g = (uint8_t)(CLAMP01(CHANF(g_c)) * 255.0f); + b = (uint8_t)(CLAMP01(CHANF(b_c)) * 255.0f); + a = (a_c >= 0 && bufs[a_c]) ? (uint8_t)(CLAMP01(*(float *)(bufs[a_c] + src_i)) * 255.0f) : 255; + } + } + dst[0] = r; + dst[1] = g; + dst[2] = b; + dst[3] = a; +#undef CHAN8 +#undef CHAN16 +#undef CHANF +#undef CLAMP01 +} + +// Decode all channels for one layer/strip and assemble into an RGBA8 buffer +// bufs/chan_ids/num_chans describe the decoded planar data +static uint8_t *psd_assemble_rgba(uint32_t w, uint32_t h, uint32_t depth_bytes, uint16_t depth, uint16_t color_mode, uint8_t **bufs, int16_t *chan_ids, + uint16_t num_chans) { + int r_c = -1, g_c = -1, b_c = -1, a_c = -1; + for (uint16_t c = 0; c < num_chans; c++) { + if (chan_ids[c] == 0) + r_c = c; + else if (chan_ids[c] == 1) + g_c = c; + else if (chan_ids[c] == 2) + b_c = c; + else if (chan_ids[c] == -1) + a_c = c; + } + // Grayscale: treat channel 0 as luminance when no explicit R/G/B + if (color_mode == 1 && r_c < 0 && num_chans > 0) + r_c = 0; + + size_t row_bytes = (size_t)w * depth_bytes; + uint8_t *rgba = (uint8_t *)malloc((size_t)w * h * 4); + if (!rgba) + return NULL; + + for (uint32_t y = 0; y < h; y++) { + for (uint32_t x = 0; x < w; x++) { + size_t src_i = (size_t)y * row_bytes + (size_t)x * depth_bytes; + psd_pixel(rgba + ((size_t)y * w + x) * 4, bufs, r_c, g_c, b_c, a_c, src_i, depth, color_mode); + } + } + return rgba; +} + +// Decode one channel plane from the current file position +static uint8_t *psd_decode_channel(uint8_t *buf, size_t buf_size, size_t *pos, uint64_t chan_len, uint32_t rows, size_t row_bytes, uint16_t version) { + if (*pos + chan_len > buf_size || chan_len < 2) + return NULL; + + uint16_t comp = psd_r16(buf, *pos); + size_t data_off = *pos + 2; + size_t plane_size = rows * row_bytes; + *pos += chan_len; + + uint8_t *plane = (uint8_t *)calloc(plane_size, 1); + if (!plane) + return NULL; + + if (comp == 0) { + // Raw + size_t copy_len = chan_len - 2; + if (copy_len > plane_size) + copy_len = plane_size; + memcpy(plane, buf + data_off, copy_len); + } + else if (comp == 1) { + // PackBits RLE — row byte counts then data + size_t cnt_bytes = (version == 2) ? 4 : 2; + size_t rdata = data_off + (size_t)rows * cnt_bytes; + for (uint32_t row = 0; row < rows; row++) { + size_t count_pos = data_off + (size_t)row * cnt_bytes; + uint32_t rlen = (cnt_bytes == 4) ? psd_r32(buf, count_pos) : psd_r16(buf, count_pos); + if (rdata + rlen <= buf_size) + packbits_decode(buf + rdata, rlen, plane + (size_t)row * row_bytes, row_bytes); + rdata += rlen; + } + } + else { + free(plane); + return NULL; // Unsupported per-channel compression + } + + return plane; +} + +#define PSD_MAX_CHAN 6 + +typedef struct { + int32_t top, left, bottom, right; + uint16_t num_channels; + int16_t chan_ids[PSD_MAX_CHAN]; + uint64_t chan_lengths[PSD_MAX_CHAN]; // Includes 2-byte compression header + char name[256]; +} psd_layer_t; + +void *io_psd_parse(uint8_t *buf, size_t buf_size, const char *file_name) { + if (buf_size < 26) + return NULL; + if (buf[0] != '8' || buf[1] != 'B' || buf[2] != 'P' || buf[3] != 'S') + return NULL; + + uint16_t version = psd_r16(buf, 4); + if (version != 1 && version != 2) + return NULL; + + uint16_t channels = psd_r16(buf, 12); + uint32_t height = psd_r32(buf, 14); + uint32_t width = psd_r32(buf, 18); + uint16_t depth = psd_r16(buf, 22); + uint16_t color_mode = psd_r16(buf, 24); + + if (width == 0 || height == 0) + return NULL; + + size_t pos = 26; + + // Section 2: Color mode data + if (pos + 4 > buf_size) + return NULL; + pos += 4 + psd_r32(buf, pos); + + // Section 3: Image resources + if (pos + 4 > buf_size) + return NULL; + pos += 4 + psd_r32(buf, pos); + + // Section 4: Layer and mask information + size_t section4_start = pos; + uint64_t section4_len = 0; + if (version == 2) { + if (pos + 8 > buf_size) + return NULL; + section4_len = psd_r64(buf, pos); + pos += 8; + } + else { + if (pos + 4 > buf_size) + return NULL; + section4_len = psd_r32(buf, pos); + pos += 4; + } + size_t section4_end = (version == 2) ? section4_start + 8 + section4_len : section4_start + 4 + section4_len; + + uint32_t depth_bytes = (depth + 7) / 8; + + // Parse layers + void *result = NULL; + int num_layers_found = 0; + + if (section4_len > 0) { + // Layer info sub-section + uint64_t layer_info_len = 0; + if (version == 2) { + if (pos + 8 > buf_size) + goto fallback; + layer_info_len = psd_r64(buf, pos); + pos += 8; + } + else { + if (pos + 4 > buf_size) + goto fallback; + layer_info_len = psd_r32(buf, pos); + pos += 4; + } + + if (layer_info_len == 0) + goto fallback; + + size_t layer_info_end = pos + layer_info_len; + + // Layer count (signed: negative means first alpha = merged transparency) + if (pos + 2 > buf_size) + goto fallback; + int16_t layer_count_raw = (int16_t)psd_r16(buf, pos); + uint16_t layer_count = (uint16_t)(layer_count_raw < 0 ? -layer_count_raw : layer_count_raw); + pos += 2; + + if (layer_count == 0) + goto fallback; + + psd_layer_t *layers = (psd_layer_t *)calloc(layer_count, sizeof(psd_layer_t)); + if (!layers) + goto fallback; + + // Pass 1: read layer records + for (uint16_t i = 0; i < layer_count; i++) { + if (pos + 18 > buf_size) + break; + layers[i].top = (int32_t)psd_r32(buf, pos); + pos += 4; + layers[i].left = (int32_t)psd_r32(buf, pos); + pos += 4; + layers[i].bottom = (int32_t)psd_r32(buf, pos); + pos += 4; + layers[i].right = (int32_t)psd_r32(buf, pos); + pos += 4; + layers[i].num_channels = psd_r16(buf, pos); + pos += 2; + if (layers[i].num_channels > PSD_MAX_CHAN) + layers[i].num_channels = PSD_MAX_CHAN; + + for (uint16_t c = 0; c < layers[i].num_channels; c++) { + layers[i].chan_ids[c] = (int16_t)psd_r16(buf, pos); + pos += 2; + if (version == 2) { + layers[i].chan_lengths[c] = psd_r64(buf, pos); + pos += 8; + } + else { + layers[i].chan_lengths[c] = psd_r32(buf, pos); + pos += 4; + } + } + + pos += 4; // blend mode signature "8BIM" + pos += 4; // blend mode key + pos += 1; // opacity + pos += 1; // clipping + pos += 1; // flags + pos += 1; // filler + + uint32_t extra_len = psd_r32(buf, pos); + pos += 4; + size_t extra_end = pos + extra_len; + + // Layer mask data + if (pos + 4 <= buf_size) { + uint32_t mask_len = psd_r32(buf, pos); + pos += 4 + mask_len; + } + // Layer blending ranges + if (pos + 4 <= buf_size) { + uint32_t blend_len = psd_r32(buf, pos); + pos += 4 + blend_len; + } + // Layer name (pascal string, padded to 4-byte boundary) + if (pos < extra_end && pos < buf_size) { + uint8_t name_len = buf[pos++]; + if (name_len > 255) + name_len = 255; + if (pos + name_len <= buf_size) + memcpy(layers[i].name, buf + pos, name_len); + layers[i].name[name_len] = 0; + pos += name_len; + // pad to 4-byte boundary counting from the length byte + uint32_t padded = ((uint32_t)(name_len + 1) + 3) & ~3u; + pos += padded - (name_len + 1); + } + + pos = extra_end; // skip any remaining extra data + } + + // Pass 2: read channel image data (immediately follows all records) + for (uint16_t i = 0; i < layer_count; i++) { + int32_t lw = layers[i].right - layers[i].left; + int32_t lh = layers[i].bottom - layers[i].top; + + if (lw <= 0 || lh <= 0) { + // Skip channel data for empty/invisible layers + for (uint16_t c = 0; c < layers[i].num_channels; c++) + pos += (size_t)layers[i].chan_lengths[c]; + continue; + } + + size_t row_bytes = (size_t)lw * depth_bytes; + uint8_t *bufs[PSD_MAX_CHAN]; + memset(bufs, 0, sizeof(bufs)); + + for (uint16_t c = 0; c < layers[i].num_channels; c++) { + // Skip layer mask channels (-2, -3) + if (layers[i].chan_ids[c] < -1) { + pos += (size_t)layers[i].chan_lengths[c]; + continue; + } + bufs[c] = psd_decode_channel(buf, buf_size, &pos, layers[i].chan_lengths[c], (uint32_t)lh, row_bytes, version); + } + + uint8_t *layer_rgba = + psd_assemble_rgba((uint32_t)lw, (uint32_t)lh, depth_bytes, depth, color_mode, bufs, layers[i].chan_ids, layers[i].num_channels); + + for (uint16_t c = 0; c < layers[i].num_channels; c++) + free(bufs[c]); + + if (!layer_rgba) + continue; + + // Place the layer into a full-image canvas at its (left, top) offset + uint8_t *rgba = (uint8_t *)calloc((size_t)width * height * 4, 1); + if (!rgba) { + free(layer_rgba); + continue; + } + + int32_t ox = layers[i].left; + int32_t oy = layers[i].top; + for (int32_t y = 0; y < lh; y++) { + int32_t dy = oy + y; + if (dy < 0 || dy >= (int32_t)height) + continue; + for (int32_t x = 0; x < lw; x++) { + int32_t dx = ox + x; + if (dx < 0 || dx >= (int32_t)width) + continue; + uint8_t *src = layer_rgba + ((size_t)y * lw + x) * 4; + uint8_t *dst = rgba + ((size_t)dy * width + dx) * 4; + dst[0] = src[0]; + dst[1] = src[1]; + dst[2] = src[2]; + dst[3] = src[3]; + } + } + free(layer_rgba); + + buffer_t *b = (buffer_t *)malloc(sizeof(buffer_t)); + b->buffer = rgba; + b->length = b->capacity = (uint32_t)((size_t)width * height * 4); + void *tex = gpu_create_texture_from_bytes(b, (int)width, (int)height, GPU_TEXTURE_FORMAT_RGBA32); + + if (num_layers_found == 0) { + result = tex; + } + else { + char *layer_name = layers[i].name[0] != 0 ? layers[i].name : "layer"; + io_psd_import_layer((char *)file_name, layer_name, tex); + } + num_layers_found++; + } + + free(layers); + + if (result != NULL) + return result; + } + +fallback: + // No layers (or all empty) — decode the merged composite image in section 5 + pos = section4_end; + if (pos + 2 > buf_size) + return NULL; + + uint16_t compression = psd_r16(buf, pos); + pos += 2; + + if (compression != 0 && compression != 1) { + console_info("Error: This psd compression type is not yet implemented"); + return NULL; + } + + size_t row_bytes = (size_t)width * depth_bytes; + size_t plane_size = (size_t)height * row_bytes; + uint16_t read_chans = channels > 4 ? 4 : channels; + + uint8_t *planes = (uint8_t *)calloc((size_t)read_chans * plane_size, 1); + if (!planes) + return NULL; + + // Build synthetic chan_ids for the composite (RGB order: 0, 1, 2; alpha: -1) + int16_t comp_chan_ids[4] = {0, 1, 2, -1}; + + if (compression == 0) { + for (uint16_t c = 0; c < channels; c++) { + if (pos + plane_size > buf_size) { + free(planes); + return NULL; + } + if (c < read_chans) + memcpy(planes + (size_t)c * plane_size, buf + pos, plane_size); + pos += plane_size; + } + } + else { + size_t cnt_bytes = (version == 2) ? 4 : 2; + size_t rle_counts_pos = pos; + size_t rle_data_pos = pos + (size_t)channels * height * cnt_bytes; + + for (uint16_t c = 0; c < channels; c++) { + for (uint32_t row = 0; row < height; row++) { + size_t count_pos = rle_counts_pos + ((size_t)c * height + row) * cnt_bytes; + uint32_t rlen = (cnt_bytes == 4) ? psd_r32(buf, count_pos) : psd_r16(buf, count_pos); + if (c < read_chans && rle_data_pos + rlen <= buf_size) + packbits_decode(buf + rle_data_pos, rlen, planes + (size_t)c * plane_size + (size_t)row * row_bytes, row_bytes); + rle_data_pos += rlen; + } + } + } + + uint8_t *planes_ptrs[4] = {NULL, NULL, NULL, NULL}; + for (uint16_t c = 0; c < read_chans; c++) + planes_ptrs[c] = planes + (size_t)c * plane_size; + + uint8_t *rgba = psd_assemble_rgba(width, height, depth_bytes, depth, color_mode, planes_ptrs, comp_chan_ids, read_chans); + free(planes); + + if (!rgba) + return NULL; + + buffer_t *b = (buffer_t *)malloc(sizeof(buffer_t)); + b->buffer = rgba; + b->length = b->capacity = (uint32_t)((size_t)width * height * 4); + return gpu_create_texture_from_bytes(b, (int)width, (int)height, GPU_TEXTURE_FORMAT_RGBA32); +} diff --git a/paint/plugins/plugins.c b/paint/plugins/plugins.c index b7608460..2832de57 100644 --- a/paint/plugins/plugins.c +++ b/paint/plugins/plugins.c @@ -1,19 +1,26 @@ #ifdef WITH_PLUGINS +#include "engine.h" #include "iron_array.h" #include "iron_map.h" #include "iron_ui.h" -#include "engine.h" void *io_svg_parse(char *buf); void *io_exr_parse(char *buf, size_t len); +void *io_psd_parse(uint8_t *buf, size_t len, const char *filename); void *io_tiff_parse(uint8_t *buf, size_t len); void *io_gltf_parse(char *buf, size_t size, const char *path); void *io_gltf_parse_skinned(char *buf, size_t size, const char *path, int frame); void *io_fbx_parse(char *buf, size_t size); void *io_fbx_parse_skinned(char *buf, size_t size, int frame); -void proc_uv_unwrap(void *mesh); +void proc_uv_unwrap(void *mesh); + +typedef struct asset { + i32 id; + char *name; + char *file; +} asset_t; extern any_map_t *import_mesh_importers; extern string_array_t *_path_mesh_formats; @@ -21,16 +28,43 @@ string_array_t *path_mesh_formats(void); extern any_map_t *import_texture_importers; extern string_array_t *_path_texture_formats; string_array_t *path_texture_formats(void); -extern any_map_t *util_mesh_unwrappers; +extern any_map_t *util_mesh_unwrappers; +extern any_map_t *data_cached_images; +void import_texture_run(char *path, bool hdr_as_envmap); +extern any_array_t *project_assets; +void tab_textures_delete_texture(asset_t *asset); int plugins_skinning_frame = -1; +void io_psd_import_layer(char *file_name, char *layer_name, void *tex) { + char *path = string("%s.%s.png", file_name, layer_name); + any_map_set(data_cached_images, path, tex); + import_texture_run(path, false); +} + static void *import_exr(char *path) { buffer_t *b = data_get_blob(path); data_delete_blob(path); return io_exr_parse((char *)b->buffer, b->length); } +static void *import_psd(char *path) { + char *filename = substring(path, string_last_index_of(path, PATH_SEP) + 1, string_length(path)); + + // Delete existing layers so they can be re-imported + char *prefix = string("%s.", filename); + for (int i = project_assets->length - 1; i >= 0; --i) { + asset_t *a = project_assets->buffer[i]; + if (starts_with(a->name, prefix)) { + tab_textures_delete_texture(a); + } + } + + buffer_t *b = data_get_blob(path); + data_delete_blob(path); + return io_psd_parse((uint8_t *)b->buffer, b->length, filename); +} + static void *import_tiff(char *path) { buffer_t *b = data_get_blob(path); data_delete_blob(path); @@ -69,6 +103,8 @@ void plugins_init() { path_texture_formats(); // Init array any_map_set(import_texture_importers, "exr", import_exr); any_array_push(_path_texture_formats, "exr"); + any_map_set(import_texture_importers, "psd", import_psd); + any_array_push(_path_texture_formats, "psd"); any_map_set(import_texture_importers, "svg", import_svg); any_array_push(_path_texture_formats, "svg"); any_map_set(import_texture_importers, "tiff", import_tiff); diff --git a/paint/plugins/project.js b/paint/plugins/project.js index fa5485b5..9ab34900 100644 --- a/paint/plugins/project.js +++ b/paint/plugins/project.js @@ -4,6 +4,7 @@ project.add_cfiles("plugins.c"); project.add_cfiles("uv_unwrap/**"); project.add_cfiles("io_svg/**"); project.add_cfiles("io_exr/**"); +project.add_cfiles("io_psd/**"); project.add_cfiles("io_gltf/**"); project.add_cfiles("io_fbx/**"); project.add_cfiles("io_tiff/**"); diff --git a/paint/sources/import_texture.c b/paint/sources/import_texture.c index 142182c0..e8bfe587 100644 --- a/paint/sources/import_texture.c +++ b/paint/sources/import_texture.c @@ -37,7 +37,7 @@ void import_texture_run(char *path, bool hdr_as_envmap) { } } - char *ext = substring(path, string_last_index_of(path, ".") + 1, string_length(path)); + char *ext = substring(path, string_last_index_of(path, ".") + 1, string_length(path)); gpu_texture_t *(*importer)(char *path) = any_map_get(import_texture_importers, ext); bool cached = any_map_get(data_cached_images, path) != NULL; // Already loaded or pink texture for missing file @@ -55,8 +55,8 @@ void import_texture_run(char *path, bool hdr_as_envmap) { any_map_set(data_cached_images, path, image); string_array_t *ar = string_split(path, PATH_SEP); - char *name = ar->buffer[ar->length - 1]; - asset_t *asset = GC_ALLOC_INIT(asset_t, {.name = name, .file = path, .id = project_asset_id++}); + char *name = ar->buffer[ar->length - 1]; + asset_t *asset = GC_ALLOC_INIT(asset_t, {.name = name, .file = path, .id = project_asset_id++}); any_array_push(project_assets, asset); if (g_context->texture == NULL) { g_context->texture = asset;