diff --git a/.gitmodules b/.gitmodules index cd9959e98..094d5b604 100644 --- a/.gitmodules +++ b/.gitmodules @@ -75,3 +75,6 @@ [submodule "third_party/cityhash"] path = third_party/cityhash url = https://github.com/aseprite/cityhash.git +[submodule "src/psd"] + path = src/psd + url = https://github.com/aseprite/psd.git diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cb25aeac6..2fe626a3b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -102,6 +102,7 @@ add_subdirectory(doc) add_subdirectory(filters) add_subdirectory(fixmath) add_subdirectory(flic) +add_subdirectory(psd) add_subdirectory(tga) add_subdirectory(render) add_subdirectory(dio) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 7266316e7..37ae36465 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -132,6 +132,7 @@ set(file_formats file/jpeg_format.cpp file/pcx_format.cpp file/png_format.cpp + file/psd_format.cpp file/svg_format.cpp file/tga_format.cpp) if(WITH_WEBP_SUPPORT) @@ -676,6 +677,7 @@ target_link_libraries(app-lib render-lib laf-ft laf-os + psd ui-lib ver-lib undo diff --git a/src/app/file/file_formats_manager.cpp b/src/app/file/file_formats_manager.cpp index 22d91ae8a..b7ca65342 100644 --- a/src/app/file/file_formats_manager.cpp +++ b/src/app/file/file_formats_manager.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2021 Igara Studio S.A. // Copyright (C) 2001-2017 David Capello // // This program is distributed under the terms of @@ -29,6 +30,7 @@ extern FileFormat* CreateIcoFormat(); extern FileFormat* CreateJpegFormat(); extern FileFormat* CreatePcxFormat(); extern FileFormat* CreatePngFormat(); +extern FileFormat* CreatePsdFormat(); extern FileFormat* CreateSvgFormat(); extern FileFormat* CreateTgaFormat(); @@ -65,6 +67,7 @@ FileFormatsManager::FileFormatsManager() registerFormat(CreateJpegFormat()); registerFormat(CreatePcxFormat()); registerFormat(CreatePngFormat()); + registerFormat(CreatePsdFormat()); registerFormat(CreateSvgFormat()); registerFormat(CreateTgaFormat()); diff --git a/src/app/file/psd_format.cpp b/src/app/file/psd_format.cpp new file mode 100644 index 000000000..84d0e4a65 --- /dev/null +++ b/src/app/file/psd_format.cpp @@ -0,0 +1,478 @@ +// Aseprite +// Copyright (C) 2021 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#include "app/file/file.h" +#include "app/file/file_format.h" +#include "base/file_handle.h" +#include "doc/blend_mode.h" +#include "doc/image_impl.h" +#include "doc/layer.h" +#include "doc/palette.h" +#include "doc/slice.h" +#include "doc/sprite.h" +#include "psd/psd.h" + +namespace app { + +doc::PixelFormat psd_cmode_to_ase_format(const psd::ColorMode mode) +{ + switch (mode) { + case psd::ColorMode::Grayscale: + return doc::PixelFormat::IMAGE_GRAYSCALE; + case psd::ColorMode::RGB: + return doc::PixelFormat::IMAGE_RGB; + default: + return doc::PixelFormat::IMAGE_INDEXED; + } +} + +doc::BlendMode psd_blendmode_to_ase(const psd::LayerBlendMode mode) +{ + switch (mode) { + case psd::LayerBlendMode::Multiply: + return doc::BlendMode::MULTIPLY; + case psd::LayerBlendMode::Darken: + return doc::BlendMode::DARKEN; + case psd::LayerBlendMode::ColorBurn: + return doc::BlendMode::COLOR_BURN; + case psd::LayerBlendMode::Lighten: + return doc::BlendMode::LIGHTEN; + case psd::LayerBlendMode::Screen: + return doc::BlendMode::SCREEN; + case psd::LayerBlendMode::ColorDodge: + return doc::BlendMode::COLOR_DODGE; + case psd::LayerBlendMode::Overlay: + return doc::BlendMode::OVERLAY; + case psd::LayerBlendMode::SoftLight: + return doc::BlendMode::SOFT_LIGHT; + case psd::LayerBlendMode::HardLight: + return doc::BlendMode::HARD_LIGHT; + case psd::LayerBlendMode::Difference: + return doc::BlendMode::DIFFERENCE; + case psd::LayerBlendMode::Exclusion: + return doc::BlendMode::EXCLUSION; + case psd::LayerBlendMode::Subtract: + return doc::BlendMode::SUBTRACT; + case psd::LayerBlendMode::Divide: + return doc::BlendMode::DIVIDE; + case psd::LayerBlendMode::Hue: + return doc::BlendMode::HSL_HUE; + case psd::LayerBlendMode::Saturation: + return doc::BlendMode::HSL_SATURATION; + case psd::LayerBlendMode::Color: + return doc::BlendMode::HSL_COLOR; + case psd::LayerBlendMode::Luminosity: + return doc::BlendMode::HSL_LUMINOSITY; + case psd::LayerBlendMode::Normal: + default: + return doc::BlendMode::NORMAL; + } +} + +class PsdFormat : public FileFormat { + const char* onGetName() const override { return "psd"; } + + void onGetExtensions(base::paths& exts) const override + { + exts.push_back("psb"); + exts.push_back("psd"); + } + + dio::FileFormat onGetDioFormat() const override + { + return dio::FileFormat::PSD_IMAGE; + } + + int onGetFlags() const override { return FILE_SUPPORT_LOAD; } + + bool onLoad(FileOp* fop) override; + bool onSave(FileOp* fop) override; +}; + +FileFormat* CreatePsdFormat() +{ + return new PsdFormat(); +} + +class PsdDecoderDelegate : public psd::DecoderDelegate { +public: + PsdDecoderDelegate() + : m_currentImage(nullptr) + , m_currentLayer(nullptr) + , m_layerGroup(nullptr) + , m_sprite(nullptr) + , m_activeFrameIndex(0) + , m_pixelFormat(PixelFormat::IMAGE_INDEXED) + , m_layerHasTransparentChannel(false) + { } + + Sprite* getSprite() { return assembleDocument(); } + + void onFileHeader(const psd::FileHeader& header) override + { + m_pixelFormat = psd_cmode_to_ase_format(header.colorMode); + m_sprite = new Sprite( + ImageSpec(ColorMode(m_pixelFormat), header.width, header.width)); + m_layerHasTransparentChannel = hasTransparency(header.nchannels); + } + + void onSlicesData(const psd::Slices& slices) override + { + auto& spriteSlices = m_sprite->slices(); + for (const auto& slice : slices.slices) { + const gfx::Rect rect(slice.bound.left, + slice.bound.top, + slice.bound.right - slice.bound.left, + slice.bound.bottom - slice.bound.top); + if (!rect.isEmpty() && slice.groupID > 0) { + auto slice = new doc::Slice; + slice->insert(0, doc::SliceKey(rect)); + + // TO-DO: the slices color should come from the app preference + slice->userData().setColor(doc::rgba(0, 0, 255, 255)); + spriteSlices.add(slice); + } + } + } + + void onFramesData(const std::vector& frameInfo, + const uint32_t activeFrameIndex) override + { + m_framesInfo = frameInfo; + m_activeFrameIndex = activeFrameIndex; + if (frameInfo.empty()) { + m_sprite->setTotalFrames(frame_t(1)); + psd::FrameInformation frameInfo; + frameInfo.id = 1; + frameInfo.duration = 10; + frameInfo.ga = 0; + m_framesInfo.push_back(std::move(frameInfo)); + } + else + m_sprite->setTotalFrames(frame_t(frameInfo.size())); + } + + // Emitted when a new layer has been chosen and its channel image data + // is about to be read + void onBeginLayer(const psd::LayerRecord& layerRecord) override + { + if (layerRecord.isOpenGroup()) { + LayerGroup* layerGroup = new LayerGroup(m_sprite); + if (m_groups.empty()) + m_sprite->root()->addLayer(layerGroup); + else + m_layerGroup->addLayer(layerGroup); + + m_layerGroup = layerGroup; + m_groups.push_back(layerGroup); + } + else if (layerRecord.isCloseGroup()) { + if (!m_layerGroup) + throw std::runtime_error("unexpected end of a group layer"); + + m_layerGroup->setName(layerRecord.name); + if (!m_groups.empty()) + m_groups.pop_back(); + + if (m_groups.empty()) + m_layerGroup = m_sprite->root(); + else + m_layerGroup = m_groups.back(); + } + else { + auto findIter = std::find_if( + m_layers.begin(), m_layers.end(), [&layerRecord](doc::Layer* layer) { + return layer->name() == layerRecord.name; + }); + if (findIter == m_layers.end()) { + if (!m_layerGroup) // In this case, there are no layer groups + m_layerGroup = m_sprite->root(); + + createNewLayer(layerRecord.name); + //m_currentLayer->setVisible(layerRecord.isVisible()); + m_layerHasTransparentChannel = + hasTransparency(layerRecord.channels.size()); + } + else { + m_currentLayer = *findIter; + m_currentImage = m_currentLayer->cel(frame_t(0))->imageRef(); + } + } + } + + void onEndLayer(const psd::LayerRecord& layerRecord) override + { + if (!m_framesInfo.empty() && + (layerRecord.inFrames.size() == m_framesInfo.size()) && + m_currentImage) { + std::unique_ptr layerCel(m_currentLayer->cel(frame_t(0))); + LayerImage* imageLayer = static_cast(m_currentLayer); + imageLayer->removeCel(layerCel.get()); + + for (const auto& inFrame : layerRecord.inFrames) { + if (inFrame.isVisibleInFrame) { + const auto findIter = std::find_if( + m_framesInfo.cbegin(), + m_framesInfo.cend(), + [id = inFrame.frameID](const psd::FrameInformation& frameInfo) { + return id == frameInfo.id; + }); + if (findIter != m_framesInfo.cend()) { + const size_t index = std::distance(m_framesInfo.cbegin(), findIter); + auto newCel = Cel::MakeCopy(index, layerCel.get()); + imageLayer->addCel(newCel); + } + } + } + } + + m_currentImage.reset(); + m_currentLayer = nullptr; + m_layerHasTransparentChannel = false; + } + + // Emitted only if there's a palette in an image + void onColorModeData(const psd::ColorModeData& colorModeD) override + { + if (!colorModeD.colors.empty()) { + for (int i = 0; i < colorModeD.colors.size(); ++i) { + const psd::IndexColor iColor = colorModeD.colors[i]; + m_palette.setEntry(i, rgba(iColor.r, iColor.g, iColor.b, 255)); + } + } + } + + // Emitted when an image data is about to be transmitted + void onBeginImage(const psd::ImageData& imageData) override + { + if (!m_currentImage) { + // Only occurs where there's an image with no layer + if (m_layers.empty()) { + m_layerGroup = m_sprite->root(); + createNewLayer("Layer 1"); + m_layerHasTransparentChannel = + hasTransparency(imageData.channels.size()); + } + if (m_currentLayer) { + createNewImage(imageData.width, imageData.height); + linkNewCel(m_currentLayer, m_currentImage); + } + } + } + + // Emitted when all layers and their masks have been processed + void onLayersAndMask(const psd::LayersInformation& layersInfo) override + { + if (layersInfo.layers.size() == m_layers.size()) { + for (int i = 0; i < m_layers.size(); ++i) { + const psd::LayerRecord& layerRecord = layersInfo.layers[i]; + + LayerImage* layer = static_cast(m_layers[i]); + layer->setBlendMode(psd_blendmode_to_ase(layerRecord.blendMode)); + + for (size_t i = 0; i < m_sprite->totalFrames(); ++i) { + Cel* cel = layer->cel(frame_t(i)); + if (cel) { + cel->setOpacity(layerRecord.opacity); + cel->setPosition(gfx::Point(layerRecord.left, layerRecord.top)); + } + } + } + } + } + + void onImageScanline(const psd::ImageData& img, + const int y, + const psd::ChannelID chanID, + const uint8_t* data, + const int bytes) override + { + if (!m_currentImage || y >= m_currentImage->height()) + return; + + const int dataCount = bytes / (img.depth >= 8 ? (img.depth / 8) : 1); + uint8_t* dstGenericAddress = m_currentImage->getPixelAddress(0, y); + + if (m_pixelFormat == doc::PixelFormat::IMAGE_INDEXED) { + IndexedTraits::address_t dstAddress = + (IndexedTraits::address_t)dstGenericAddress; + for (int x = 0; x < dataCount && x < m_currentImage->width(); ++x) { + *(dstAddress)++ = getNormalizedPixelValue(data, img.depth); + } + } + else if (m_pixelFormat == doc::PixelFormat::IMAGE_GRAYSCALE) { + GrayscaleTraits::address_t dstAddress = + (GrayscaleTraits::address_t)dstGenericAddress; + uint8_t v = 0, a = 0; + for (int x = 0; x < dataCount && x < m_currentImage->width(); ++x) { + const GrayscaleTraits::pixel_t pixel = *dstAddress; + const uint8_t newPixelValue = getNormalizedPixelValue(data, img.depth); + if (chanID == psd::ChannelID::Red) { + v = newPixelValue; + a = m_layerHasTransparentChannel ? graya_geta(pixel) : 255; + } + else if (chanID == psd::ChannelID::Alpha || + chanID == psd::ChannelID::TransparencyMask) { + a = newPixelValue; + v = graya_getv(pixel); + } + *(dstAddress++) = graya(v, a); + } + } + else if (m_pixelFormat == doc::PixelFormat::IMAGE_RGB) { + RgbTraits::address_t dstAddress = (RgbTraits::address_t)dstGenericAddress; + uint8_t r, g, b, a; + for (int x = 0; x < dataCount && x < m_currentImage->width(); ++x) { + const uint8_t newPixelValue = getNormalizedPixelValue(data, img.depth); + const color_t c = *(dstAddress); + r = rgba_getr(c); + g = rgba_getg(c); + b = rgba_getb(c); + a = m_layerHasTransparentChannel ? rgba_geta(c) : 255; + if (chanID == psd::ChannelID::Red) { + r = newPixelValue; + } + else if (chanID == psd::ChannelID::Green) { + g = newPixelValue; + } + else if (chanID == psd::ChannelID::Blue) { + b = newPixelValue; + } + else if (chanID == psd::ChannelID::Alpha || + chanID == psd::ChannelID::TransparencyMask) { + a = newPixelValue; + } + *(dstAddress++) = rgba(r, g, b, a); + } + } + } + +private: + inline bool hasTransparency(const size_t nchannels) + { + // RGBA or grayscale image with alpha channel + return nchannels == 4 || nchannels == 2; + } + + void linkNewCel(Layer* layer, doc::ImageRef image) + { + if (!image) + return; + std::unique_ptr cel(new doc::Cel(frame_t(0), image)); + cel->setPosition(0, 0); + static_cast(layer)->addCel(cel.release()); + } + + void createNewLayer(const std::string& layerName) + { + m_currentLayer = new LayerImage(m_sprite); + m_layerGroup->addLayer(m_currentLayer); + m_layers.push_back(m_currentLayer); + m_currentLayer->setName(layerName); + } + + Sprite* assembleDocument() + { + if (m_palette.getModifications() > 1) + m_sprite->setPalette(&m_palette, true); + + for (size_t i = 0; i < m_framesInfo.size(); ++i) { + const psd::FrameInformation& frameInfo = m_framesInfo[i]; + const int timeMs = frameInfo.duration * 10; + m_sprite->setFrameDuration(frame_t(i), timeMs); + } + // at this point, we're supposed to be able to set the activeFrame + return m_sprite; + } + + std::uint8_t getNormalizedPixelValue(const std::uint8_t*& data, + const int depth) + { + if (depth == 1 || depth == 8) { + return *(data++); + } + else if (depth == 16) { + const uint16_t value = int(data[0]) | int(data[1] << 8); + data += 2; + return value >> 8; + } + else if (depth == 32) { + const uint32_t value = int(data[0] << 24) | int(data[1] << 16) | + int(data[2] << 8) | int(data[3]); + data += 4; + return value >> 24; + } + else + throw std::runtime_error("invalid image depth"); + } + + void createNewImage(const int width, const int height) + { + if (width <= 0 || height <= 0) + return; //throw std::runtime_error("invalid image width/height"); + + m_currentImage.reset(Image::create(m_pixelFormat, width, height)); + clear_image(m_currentImage.get(), 0); + } + + doc::ImageRef m_currentImage; + doc::Layer* m_currentLayer; + doc::LayerGroup* m_layerGroup; + Sprite* m_sprite; + uint32_t m_activeFrameIndex; + PixelFormat m_pixelFormat; + std::vector m_layers; + std::vector m_groups; + std::vector m_framesInfo; + Palette m_palette; + bool m_layerHasTransparentChannel; +}; + +bool PsdFormat::onLoad(FileOp* fop) +{ + base::FileHandle fileHandle = + base::open_file_with_exception(fop->filename(), "rb"); + FILE* f = fileHandle.get(); + psd::StdioFileInterface fileInterface(f); + PsdDecoderDelegate pDelegate; + psd::Decoder decoder(&fileInterface, &pDelegate); + + if (!decoder.readFileHeader()) { + fop->setError("The file doesn't have a valid PSD header\n"); + return false; + } + + const psd::FileHeader header = decoder.fileHeader(); + + if (header.colorMode != psd::ColorMode::RGB && + header.colorMode != psd::ColorMode::Indexed && + header.colorMode != psd::ColorMode::Grayscale) { + fop->setError("This preliminary work only supports " + "RGB, Grayscale & Indexed images\n"); + return false; + } + + try { + decoder.readColorModeData(); + decoder.readImageResources(); + decoder.readLayersAndMask(); + decoder.readImageData(); + } + catch (const std::runtime_error& e) { + fop->setError(e.what()); + return false; + } + + fop->createDocument(pDelegate.getSprite()); + return true; +} + +bool PsdFormat::onSave(FileOp* fop) +{ + return false; +} + +} // namespace app diff --git a/src/dio/detect_format.cpp b/src/dio/detect_format.cpp index e0ed3155f..8f7236eff 100644 --- a/src/dio/detect_format.cpp +++ b/src/dio/detect_format.cpp @@ -23,6 +23,7 @@ #define PNG_MAGIC_DWORD2 0x0A1A0A0D #define WEBP_STAMP_1 "RIFF" // "RIFFnnnnWEBP" #define WEBP_STAMP_2 "WEBP" +#define PSD_STAMP "8BPS" namespace dio { @@ -65,6 +66,9 @@ FileFormat detect_format_by_file_content_bytes(const uint8_t* buf, std::strncmp((const char*)buf, GIF_89_STAMP, 6) == 0) return FileFormat::GIF_ANIMATION; + if (std::strncmp((const char*)buf, PSD_STAMP, 4) == 0) + return FileFormat::PSD_IMAGE; + if (IS_MAGIC_WORD(4, ASE_MAGIC_NUMBER)) return FileFormat::ASE_ANIMATION; @@ -156,6 +160,10 @@ FileFormat detect_format_by_file_extension(const std::string& filename) if (ext == "webp") return FileFormat::WEBP_ANIMATION; + if (ext == "psd" || + ext == "psb") + return FileFormat::PSD_IMAGE; + return FileFormat::UNKNOWN; } diff --git a/src/dio/file_format.h b/src/dio/file_format.h index b8598644c..457827ac8 100644 --- a/src/dio/file_format.h +++ b/src/dio/file_format.h @@ -1,4 +1,5 @@ // Aseprite Document IO Library +// Copyright (c) 2021 Igara Studio S.A. // Copyright (c) 2016-2017 David Capello // // This file is released under the terms of the MIT license. @@ -32,6 +33,7 @@ enum class FileFormat { TARGA_IMAGE, WEBP_ANIMATION, CSS_STYLE, + PSD_IMAGE, }; } // namespace dio diff --git a/src/psd b/src/psd new file mode 160000 index 000000000..5a357fb96 --- /dev/null +++ b/src/psd @@ -0,0 +1 @@ +Subproject commit 5a357fb964ef158bce4b1774157dc16a8bcd1f8d