// Copyright (c) the JPEG XL Project Authors. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. #include "lib/extras/codec_psd.h" #include #include #include #include #include #include #include #include #include "lib/jxl/base/bits.h" #include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/base/file_io.h" #include "lib/jxl/color_management.h" #include "lib/jxl/common.h" #include "lib/jxl/fields.h" // AllDefault #include "lib/jxl/image.h" #include "lib/jxl/image_bundle.h" #include "lib/jxl/image_ops.h" #include "lib/jxl/luminance.h" namespace jxl { namespace extras { namespace { uint64_t get_be_int(int bytes, const uint8_t*& pos, const uint8_t* maxpos) { uint64_t r = 0; if (pos + bytes <= maxpos) { if (bytes == 1) { r = *pos; } else if (bytes == 2) { r = LoadBE16(pos); } else if (bytes == 4) { r = LoadBE32(pos); } else if (bytes == 8) { r = LoadBE64(pos); } } pos += bytes; return r; } // Copies up to n bytes, without reading from maxpos (the STL-style end). void safe_copy(const uint8_t* JXL_RESTRICT pos, const uint8_t* JXL_RESTRICT maxpos, char* JXL_RESTRICT out, size_t n) { for (size_t i = 0; i < n; ++i) { if (pos + i >= maxpos) return; out[i] = pos[i]; } } // maxpos is the STL-style end! The valid range is up to [pos, maxpos). int safe_strncmp(const uint8_t* pos, const uint8_t* maxpos, const char* s2, size_t n) { if (pos + n > maxpos) return 1; return strncmp((const char*)pos, s2, n); } constexpr int PSD_VERBOSITY = 1; Status decode_layer(const uint8_t*& pos, const uint8_t* maxpos, ImageBundle& layer, std::vector chans, std::vector invert, int w, int h, int version, int colormodel, bool is_layer, int depth) { int compression_method = 2; int nb_channels = chans.size(); JXL_DEBUG_V(PSD_VERBOSITY, "Trying to decode layer with dimensions %ix%i and %i channels", w, h, nb_channels); if (w <= 0 || h <= 0) return JXL_FAILURE("PSD: empty layer"); for (int c = 0; c < nb_channels; c++) { // skip nop byte padding while (pos < maxpos && *pos == 128) pos++; JXL_DEBUG_V(PSD_VERBOSITY, "Channel %i (pos %zu)", c, (size_t)pos); // Merged image stores all channels together (same compression method) // Layers store channel per channel if (is_layer || c == 0) { compression_method = get_be_int(2, pos, maxpos); JXL_DEBUG_V(PSD_VERBOSITY, "compression method: %i", compression_method); if (compression_method > 1 || compression_method < 0) { return JXL_FAILURE("PSD: can't handle compression method %i", compression_method); } } if (!is_layer && c < colormodel) { // skip to the extra channels if (compression_method == 0) { pos += w * h * (depth >> 3) * colormodel; c = colormodel - 1; continue; } size_t skip_amount = 0; for (int i = 0; i < nb_channels; i++) { if (i < colormodel) { for (int y = 0; y < h; y++) { skip_amount += get_be_int(2 * version, pos, maxpos); } } else { pos += h * 2 * version; } } pos += skip_amount; c = colormodel - 1; continue; } if (is_layer || c == 0) { // skip the line-counts, we don't need them if (compression_method == 1) { pos += h * (is_layer ? 1 : nb_channels) * 2 * version; // PSB uses 4 bytes per rowsize instead of 2 } } int c_id = chans[c]; if (c_id < 0) continue; // skip if (static_cast(c_id) >= 3 + layer.extra_channels().size()) return JXL_FAILURE("PSD: can't handle channel id %i", c_id); ImageF& ch = (c_id < 3 ? layer.color()->Plane(c_id) : layer.extra_channels()[c_id - 3]); for (int y = 0; y < h; y++) { if (pos > maxpos) return JXL_FAILURE("PSD: premature end of input"); float* const JXL_RESTRICT row = ch.Row(y); if (compression_method == 0) { // uncompressed is easy if (depth == 8) { for (int x = 0; x < w; x++) { row[x] = get_be_int(1, pos, maxpos) * (1.f / 255.f); } } else if (depth == 16) { for (int x = 0; x < w; x++) { row[x] = get_be_int(2, pos, maxpos) * (1.f / 65535.f); } } else if (depth == 32) { for (int x = 0; x < w; x++) { uint32_t f = get_be_int(4, pos, maxpos); memcpy(&row[x], &f, 4); } } } else { // RLE is not that hard if (depth != 8) return JXL_FAILURE("PSD: did not expect RLE with depth>1"); for (int x = 0; x < w;) { if (pos >= maxpos) return JXL_FAILURE("PSD: out of bounds"); int8_t rle = *pos++; if (rle <= 0) { if (rle == -128) continue; // nop int count = 1 - rle; float v = get_be_int(1, pos, maxpos) * (1.f / 255.f); while (count && x < w) { row[x] = v; count--; x++; } if (count) return JXL_FAILURE("PSD: row overflow"); } else { int count = 1 + rle; while (count && x < w) { row[x] = get_be_int(1, pos, maxpos) * (1.f / 255.f); count--; x++; } if (count) return JXL_FAILURE("PSD: row overflow"); } } } if (invert[c]) { // sometimes 0 means full ink for (int x = 0; x < w; x++) { row[x] = 1.f - row[x]; } } } JXL_DEBUG_V(PSD_VERBOSITY, "Channel %i read.", c); } return true; } } // namespace Status DecodeImagePSD(const Span bytes, const ColorHints& color_hints, ThreadPool* pool, CodecInOut* io) { const uint8_t* pos = bytes.data(); const uint8_t* maxpos = bytes.data() + bytes.size(); if (safe_strncmp(pos, maxpos, "8BPS", 4)) return false; // not a PSD file JXL_DEBUG_V(PSD_VERBOSITY, "trying psd decode"); pos += 4; int version = get_be_int(2, pos, maxpos); JXL_DEBUG_V(PSD_VERBOSITY, "Version=%i", version); if (version < 1 || version > 2) return JXL_FAILURE("PSD: unknown format version"); // PSD = version 1, PSB = version 2 pos += 6; int nb_channels = get_be_int(2, pos, maxpos); size_t ysize = get_be_int(4, pos, maxpos); size_t xsize = get_be_int(4, pos, maxpos); const SizeConstraints* constraints = &io->constraints; JXL_RETURN_IF_ERROR(VerifyDimensions(constraints, xsize, ysize)); uint64_t total_pixel_count = static_cast(xsize) * ysize; int bitdepth = get_be_int(2, pos, maxpos); if (bitdepth != 8 && bitdepth != 16 && bitdepth != 32) { return JXL_FAILURE("PSD: bit depth %i invalid or not supported", bitdepth); } if (bitdepth == 32) { io->metadata.m.SetFloat32Samples(); } else { io->metadata.m.SetUintSamples(bitdepth); } int colormodel = get_be_int(2, pos, maxpos); // 1 = Grayscale, 3 = RGB, 4 = CMYK if (colormodel != 1 && colormodel != 3 && colormodel != 4) return JXL_FAILURE("PSD: unsupported color model"); int real_nb_channels = colormodel; std::vector> spotcolor; if (get_be_int(4, pos, maxpos)) return JXL_FAILURE("PSD: Unsupported color mode section"); bool hasmergeddata = true; bool have_alpha = false; bool merged_has_alpha = false; bool color_already_set = false; size_t metalength = get_be_int(4, pos, maxpos); const uint8_t* metaoffset = pos; while (pos < metaoffset + metalength) { char header[5] = "????"; safe_copy(pos, maxpos, header, 4); if (memcmp(header, "8BIM", 4) != 0) { return JXL_FAILURE("PSD: Unexpected image resource header: %s", header); } pos += 4; int id = get_be_int(2, pos, maxpos); int namelength = get_be_int(1, pos, maxpos); pos += namelength; if (!(namelength & 1)) pos++; // padding to even length size_t blocklength = get_be_int(4, pos, maxpos); // JXL_DEBUG_V(PSD_VERBOSITY, "block id: %i | block length: %zu",id, // blocklength); if (pos > maxpos) return JXL_FAILURE("PSD: Unexpected end of file"); if (id == 1039) { // ICC profile size_t delta = maxpos - pos; if (delta < blocklength) { return JXL_FAILURE("PSD: Invalid block length"); } PaddedBytes icc; icc.resize(blocklength); memcpy(icc.data(), pos, blocklength); if (!io->metadata.m.color_encoding.SetICC(std::move(icc))) { return JXL_FAILURE("PSD: Invalid color profile"); } color_already_set = true; } else if (id == 1057) { // compatibility mode or not? if (get_be_int(4, pos, maxpos) != 1) { return JXL_FAILURE("PSD: expected version=1 in id=1057 resource block"); } hasmergeddata = get_be_int(1, pos, maxpos); pos++; blocklength -= 6; // already skipped these bytes } else if (id == 1077) { // spot colors int version = get_be_int(4, pos, maxpos); if (version != 1) { return JXL_FAILURE( "PSD: expected DisplayInfo version 1, got version %i", version); } int spotcolorcount = nb_channels - colormodel; JXL_DEBUG_V(PSD_VERBOSITY, "Reading %i spot colors. %zu", spotcolorcount, blocklength); for (int k = 0; k < spotcolorcount; k++) { int colorspace = get_be_int(2, pos, maxpos); if ((colormodel == 3 && colorspace != 0) || (colormodel == 4 && colorspace != 2)) { return JXL_FAILURE( "PSD: cannot handle spot colors in different color spaces than " "image itself"); } if (colorspace == 2) JXL_WARNING("PSD: K ignored in CMYK spot color"); std::vector color; color.push_back(get_be_int(2, pos, maxpos) / 65535.f); // R or C color.push_back(get_be_int(2, pos, maxpos) / 65535.f); // G or M color.push_back(get_be_int(2, pos, maxpos) / 65535.f); // B or Y color.push_back(get_be_int(2, pos, maxpos) / 65535.f); // ignored or K color.push_back(get_be_int(2, pos, maxpos) / 100.f); // solidity (alpha, basically) int kind = get_be_int(1, pos, maxpos); JXL_DEBUG_V(PSD_VERBOSITY, "Kind=%i", kind); color.push_back(kind); spotcolor.push_back(color); if (kind == 2) { JXL_DEBUG_V(PSD_VERBOSITY, "Actual spot color"); } else if (kind == 1) { JXL_DEBUG_V(PSD_VERBOSITY, "Mask (alpha) channel"); } else if (kind == 0) { JXL_DEBUG_V(PSD_VERBOSITY, "Selection (alpha) channel"); } else { return JXL_FAILURE("PSD: Unknown extra channel type"); } } if (blocklength & 1) pos++; blocklength = 0; } pos += blocklength; if (blocklength & 1) pos++; // padding again } JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, color_already_set, /*is_gray=*/false, io)); size_t layerlength = get_be_int(4 * version, pos, maxpos); const uint8_t* after_layers_pos = pos + layerlength; if (after_layers_pos < pos) return JXL_FAILURE("PSD: invalid layer length"); if (layerlength) { pos += 4 * version; // don't care about layerinfolength JXL_DEBUG_V(PSD_VERBOSITY, "Layer section length: %zu", layerlength); int layercount = static_cast(get_be_int(2, pos, maxpos)); JXL_DEBUG_V(PSD_VERBOSITY, "Layer count: %i", layercount); io->frames.clear(); if (layercount == 0) { if (get_be_int(2, pos, maxpos) != 0) { return JXL_FAILURE( "PSD: Expected zero padding before additional layer info"); } while (pos < after_layers_pos) { if (safe_strncmp(pos, maxpos, "8BIM", 4) && safe_strncmp(pos, maxpos, "8B64", 4)) return JXL_FAILURE("PSD: Unexpected layer info signature"); pos += 4; const uint8_t* tpos = pos; pos += 4; size_t blocklength = get_be_int(4 * version, pos, maxpos); JXL_DEBUG_V(PSD_VERBOSITY, "Length=%zu", blocklength); if (blocklength > 0) { if (pos >= maxpos) return JXL_FAILURE("PSD: Unexpected end of file"); size_t delta = maxpos - pos; if (delta < blocklength) { return JXL_FAILURE("PSD: Invalid block length"); } } if (!safe_strncmp(tpos, maxpos, "Layr", 4) || !safe_strncmp(tpos, maxpos, "Lr16", 4) || !safe_strncmp(tpos, maxpos, "Lr32", 4)) { layercount = static_cast(get_be_int(2, pos, maxpos)); if (layercount < 0) { return JXL_FAILURE("PSD: Invalid layer count"); } JXL_DEBUG_V(PSD_VERBOSITY, "Real layer count: %i", layercount); if (layercount > 1) have_alpha = true; break; } if (!safe_strncmp(tpos, maxpos, "Mtrn", 4) || !safe_strncmp(tpos, maxpos, "Mt16", 4) || !safe_strncmp(tpos, maxpos, "Mt32", 4)) { JXL_DEBUG_V(PSD_VERBOSITY, "Merged layer has transparency channel"); if (nb_channels > real_nb_channels) { have_alpha = true; merged_has_alpha = true; } } pos += blocklength; } } else if (layercount < 0) { // negative layer count indicates merged has alpha and it is to be shown if (nb_channels > real_nb_channels) { have_alpha = true; merged_has_alpha = true; } layercount = -layercount; } else { // multiple layers implies there is alpha have_alpha = true; } ExtraChannelInfo info; info.bit_depth.bits_per_sample = bitdepth; info.dim_shift = 0; if (colormodel == 4) { // cmyk info.type = ExtraChannel::kBlack; io->metadata.m.extra_channel_info.push_back(info); } if (have_alpha) { JXL_DEBUG_V(PSD_VERBOSITY, "Have alpha"); real_nb_channels++; info.type = ExtraChannel::kAlpha; info.alpha_associated = false; // true? PSD is not consistent with this, need to check io->metadata.m.extra_channel_info.push_back(info); } if (merged_has_alpha && !spotcolor.empty() && spotcolor[0][5] == 1) { // first alpha channel spotcolor.erase(spotcolor.begin()); } for (size_t i = 0; i < spotcolor.size(); i++) { real_nb_channels++; if (spotcolor[i][5] == 2) { info.type = ExtraChannel::kSpotColor; info.spot_color[0] = spotcolor[i][0]; info.spot_color[1] = spotcolor[i][1]; info.spot_color[2] = spotcolor[i][2]; info.spot_color[3] = spotcolor[i][4]; } else if (spotcolor[i][5] == 1) { info.type = ExtraChannel::kAlpha; } else if (spotcolor[i][5] == 0) { info.type = ExtraChannel::kSelectionMask; } else return JXL_FAILURE("PSD: unhandled extra channel"); io->metadata.m.extra_channel_info.push_back(info); } std::vector> layer_chan_id; std::vector layer_offsets(layercount + 1, 0); std::vector is_real_layer(layercount, false); for (int l = 0; l < layercount; l++) { ImageBundle layer(&io->metadata.m); layer.duration = 0; layer.blend = (l > 0); layer.use_for_next_frame = (l + 1 < layercount); layer.origin.y0 = get_be_int(4, pos, maxpos); layer.origin.x0 = get_be_int(4, pos, maxpos); size_t height = get_be_int(4, pos, maxpos) - layer.origin.y0; size_t width = get_be_int(4, pos, maxpos) - layer.origin.x0; JXL_DEBUG_V(PSD_VERBOSITY, "Layer %i: %zu x %zu at origin (%i, %i)", l, width, height, layer.origin.x0, layer.origin.y0); int nb_chs = get_be_int(2, pos, maxpos); JXL_DEBUG_V(PSD_VERBOSITY, " channels: %i", nb_chs); std::vector chan_ids; layer_offsets[l + 1] = layer_offsets[l]; for (int lc = 0; lc < nb_chs; lc++) { int id = get_be_int(2, pos, maxpos); JXL_DEBUG_V(PSD_VERBOSITY, " id=%i", id); if (id == 65535) { chan_ids.push_back(colormodel); // alpha } else if (id == 65534) { chan_ids.push_back(-1); // layer mask, ignored } else { chan_ids.push_back(id); // color channel } layer_offsets[l + 1] += get_be_int(4 * version, pos, maxpos); } layer_chan_id.push_back(chan_ids); if (safe_strncmp(pos, maxpos, "8BIM", 4)) return JXL_FAILURE("PSD: Layer %i: Unexpected signature (not 8BIM)", l); pos += 4; if (safe_strncmp(pos, maxpos, "norm", 4)) { return JXL_FAILURE( "PSD: Layer %i: Cannot handle non-default blend mode", l); } pos += 4; int opacity = get_be_int(1, pos, maxpos); if (opacity < 100) { JXL_WARNING( "PSD: ignoring opacity of semi-transparent layer %i (opacity=%i)", l, opacity); } pos++; // clipping int flags = get_be_int(1, pos, maxpos); pos++; bool invisible = (flags & 2); if (invisible) { if (l + 1 < layercount) { layer.blend = false; layer.use_for_next_frame = false; } else { // TODO: instead add dummy last frame? JXL_WARNING("PSD: invisible top layer was made visible"); } } size_t extradata = get_be_int(4, pos, maxpos); JXL_DEBUG_V(PSD_VERBOSITY, " extradata: %zu bytes", extradata); const uint8_t* after_extra = pos + extradata; // TODO: deal with non-empty layer masks pos += get_be_int(4, pos, maxpos); // skip layer mask data pos += get_be_int(4, pos, maxpos); // skip layer blend range data size_t namelength = get_be_int(1, pos, maxpos); size_t delta = maxpos - pos; if (delta < namelength) return JXL_FAILURE("PSD: Invalid block length"); char lname[256] = {}; memcpy(lname, pos, namelength); lname[namelength] = 0; JXL_DEBUG_V(PSD_VERBOSITY, " name: %s", lname); pos = after_extra; if (width == 0 || height == 0) { JXL_DEBUG_V(PSD_VERBOSITY, " NOT A REAL LAYER"); // probably layer group continue; } is_real_layer[l] = true; JXL_RETURN_IF_ERROR(VerifyDimensions(constraints, width, height)); uint64_t pixel_count = static_cast(width) * height; if (!SafeAdd(total_pixel_count, pixel_count, total_pixel_count)) { return JXL_FAILURE("Image too big"); } if (total_pixel_count > constraints->dec_max_pixels) { return JXL_FAILURE("Image too big"); } Image3F rgb(width, height); layer.SetFromImage(std::move(rgb), io->metadata.m.color_encoding); std::vector ec; for (const auto& ec_meta : layer.metadata()->extra_channel_info) { ImageF extra(width, height); if (ec_meta.type == ExtraChannel::kAlpha) { FillPlane(1.0f, &extra, Rect(extra)); // opaque } else { ZeroFillPlane(&extra, Rect(extra)); // zeroes } ec.push_back(std::move(extra)); } if (!ec.empty()) layer.SetExtraChannels(std::move(ec)); layer.name = lname; io->dec_pixels += layer.xsize() * layer.ysize(); io->frames.push_back(std::move(layer)); } std::vector invert(real_nb_channels, false); int il = 0; const uint8_t* bpos = pos; for (int l = 0; l < layercount; l++) { if (!is_real_layer[l]) continue; pos = bpos + layer_offsets[l]; if (pos < bpos) return JXL_FAILURE("PSD: invalid layer offset"); JXL_DEBUG_V(PSD_VERBOSITY, "At position %i (%zu)", (int)(pos - bytes.data()), (size_t)pos); ImageBundle& layer = io->frames[il++]; std::vector& chan_id = layer_chan_id[l]; if (chan_id.size() > invert.size()) invert.resize(chan_id.size(), false); JXL_RETURN_IF_ERROR(decode_layer(pos, maxpos, layer, chan_id, invert, layer.xsize(), layer.ysize(), version, colormodel, true, bitdepth)); } } else return JXL_FAILURE("PSD: no layer data found"); if (!hasmergeddata && !spotcolor.empty()) { return JXL_FAILURE("PSD: extra channel data declared but not found"); } if (!spotcolor.empty() || (hasmergeddata && io->frames.empty())) { // PSD only has spot colors / extra alpha/mask data in the merged image // We don't redundantly store the merged image, so we put it in the first // layer (the next layers will kAdd zeroes to it) pos = after_layers_pos; bool have_only_merged = false; if (io->frames.empty()) { // There is only the merged image, no layers ImageBundle nlayer(&io->metadata.m); Image3F rgb(xsize, ysize); nlayer.SetFromImage(std::move(rgb), io->metadata.m.color_encoding); std::vector ec; for (const auto& ec_meta : nlayer.metadata()->extra_channel_info) { ImageF extra(xsize, ysize); if (ec_meta.type == ExtraChannel::kAlpha) { FillPlane(1.0f, &extra, Rect(extra)); // opaque } else { ZeroFillPlane(&extra, Rect(extra)); // zeroes } ec.push_back(std::move(extra)); } if (!ec.empty()) nlayer.SetExtraChannels(std::move(ec)); io->dec_pixels += nlayer.xsize() * nlayer.ysize(); io->frames.push_back(std::move(nlayer)); have_only_merged = true; } ImageBundle& layer = io->frames[0]; std::vector chan_id(real_nb_channels); std::iota(chan_id.begin(), chan_id.end(), 0); std::vector invert(real_nb_channels, false); if (static_cast(spotcolor.size()) + colormodel + 1 < real_nb_channels) { return JXL_FAILURE("Inconsistent layer configuration"); } if (!merged_has_alpha) { if (colormodel >= real_nb_channels) { return JXL_FAILURE("Inconsistent layer configuration"); } chan_id.erase(chan_id.begin() + colormodel); invert.erase(invert.begin() + colormodel); } else { colormodel++; } for (size_t i = colormodel; i < invert.size(); i++) { if (spotcolor[i - colormodel][5] == 2) invert[i] = true; if (spotcolor[i - colormodel][5] == 0) invert[i] = true; } JXL_RETURN_IF_ERROR(decode_layer( pos, maxpos, layer, chan_id, invert, layer.xsize(), layer.ysize(), version, (have_only_merged ? 0 : colormodel), false, bitdepth)); } if (io->frames.empty()) return JXL_FAILURE("PSD: no layers"); io->SetSize(xsize, ysize); SetIntensityTarget(io); return true; } Status EncodeImagePSD(const CodecInOut* io, const ColorEncoding& c_desired, size_t bits_per_sample, ThreadPool* pool, PaddedBytes* bytes) { return JXL_FAILURE("PSD encoding not yet implemented"); } } // namespace extras } // namespace jxl