mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-30 04:20:23 +00:00
Initial to decode PSD files
This commit is contained in:
parent
ff851157cc
commit
9a12eb584b
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
|
||||
|
394
src/app/file/psd_format.cpp
Normal file
394
src/app/file/psd_format.cpp
Normal file
@ -0,0 +1,394 @@
|
||||
// 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/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_sprite(nullptr)
|
||||
, 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_sprite->setTotalFrames(frame_t(1));
|
||||
}
|
||||
|
||||
// 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
|
||||
{
|
||||
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()) {
|
||||
createNewLayer(layerRecord.name);
|
||||
m_currentLayer->setVisible(layerRecord.isVisible());
|
||||
m_layerHasTransparentChannel = hasTransparency(layerRecord.channels);
|
||||
}
|
||||
else {
|
||||
m_currentLayer = *findIter;
|
||||
m_currentImage = m_currentLayer->cel(frame_t(0))->imageRef();
|
||||
}
|
||||
}
|
||||
|
||||
void onEndLayer(const psd::LayerRecord& layerRecord) override
|
||||
{
|
||||
ASSERT(m_currentLayer);
|
||||
ASSERT(m_currentImage);
|
||||
|
||||
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()) {
|
||||
createNewLayer("Layer 1");
|
||||
m_layerHasTransparentChannel = hasTransparency(imageData.channels);
|
||||
}
|
||||
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<LayerImage*>(m_layers[i]);
|
||||
layer->setBlendMode(psd_blendmode_to_ase(layerRecord.blendMode));
|
||||
|
||||
Cel* cel = layer->cel(frame_t(0));
|
||||
cel->setOpacity(layerRecord.opacity);
|
||||
cel->setPosition(gfx::Point(layerRecord.left, layerRecord.top));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is called on images stored in raw data, most likely called when
|
||||
// the first alpha channel contains the transparency data for the merged
|
||||
// result. This channel is almost always stored in raw data format
|
||||
void onImageScanline(const psd::ImageData& imgData,
|
||||
const int y,
|
||||
const psd::ChannelID chanID,
|
||||
const std::vector<uint32_t>& data) override
|
||||
{
|
||||
if (!m_currentImage)
|
||||
return;
|
||||
|
||||
for (int x = 0; x < data.size(); ++x) {
|
||||
const color_t c = m_currentImage->getPixel(x, y);
|
||||
const uint32_t pixel = normalizeValue(data[x], imgData.depth);
|
||||
putPixel(x, y, c, pixel, chanID, m_pixelFormat);
|
||||
}
|
||||
}
|
||||
|
||||
// This method is called on images stored in RLE format
|
||||
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;
|
||||
|
||||
for (int x = 0; x < bytes && x < m_currentImage->width(); ++x) {
|
||||
const color_t c = m_currentImage->getPixel(x, y);
|
||||
const uint32_t pixel = normalizeValue(data[x], img.depth);
|
||||
putPixel(x, y, c, pixel, chanID, m_pixelFormat);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// TODO: This is meant to convert values of a channel based on its depth
|
||||
std::uint32_t normalizeValue(const uint32_t value, const int depth)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
bool hasTransparency(const std::vector<psd::ChannelID>& channelIDs)
|
||||
{
|
||||
return std::any_of(channelIDs.cbegin(),
|
||||
channelIDs.cend(),
|
||||
[](const psd::ChannelID& channelID) {
|
||||
return channelID == psd::ChannelID::TransparencyMask ||
|
||||
channelID == psd::ChannelID::Alpha;
|
||||
});
|
||||
}
|
||||
|
||||
bool hasTransparency(const std::vector<psd::Channel>& channels)
|
||||
{
|
||||
return std::any_of(
|
||||
channels.cbegin(), channels.cend(), [](const psd::Channel& channel) {
|
||||
return channel.channelID == psd::ChannelID::TransparencyMask ||
|
||||
channel.channelID == psd::ChannelID::Alpha;
|
||||
});
|
||||
}
|
||||
|
||||
void linkNewCel(Layer* layer, doc::ImageRef image)
|
||||
{
|
||||
std::unique_ptr<Cel> cel(new doc::Cel(frame_t(0), image));
|
||||
cel->setPosition(0, 0);
|
||||
static_cast<LayerImage*>(layer)->addCel(cel.release());
|
||||
}
|
||||
|
||||
void createNewLayer(const std::string& layerName)
|
||||
{
|
||||
m_currentLayer = new LayerImage(m_sprite);
|
||||
m_sprite->root()->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);
|
||||
|
||||
return m_sprite;
|
||||
}
|
||||
|
||||
template<typename NewPixel>
|
||||
void putPixel(const int x,
|
||||
const int y,
|
||||
const color_t prevPixelValue,
|
||||
const NewPixel pixelValue,
|
||||
const psd::ChannelID chanID,
|
||||
const PixelFormat pixelFormat)
|
||||
{
|
||||
if (pixelFormat == doc::PixelFormat::IMAGE_RGB) {
|
||||
int r = rgba_getr(prevPixelValue);
|
||||
int g = rgba_getg(prevPixelValue);
|
||||
int b = rgba_getb(prevPixelValue);
|
||||
int a = m_layerHasTransparentChannel ? rgba_geta(prevPixelValue) : 255;
|
||||
|
||||
if (chanID == psd::ChannelID::Red) {
|
||||
r = pixelValue;
|
||||
}
|
||||
else if (chanID == psd::ChannelID::Green) {
|
||||
g = pixelValue;
|
||||
}
|
||||
else if (chanID == psd::ChannelID::Blue) {
|
||||
b = pixelValue;
|
||||
}
|
||||
else if (chanID == psd::ChannelID::Alpha ||
|
||||
chanID == psd::ChannelID::TransparencyMask) {
|
||||
a = pixelValue;
|
||||
}
|
||||
m_currentImage->putPixel(x, y, rgba(r, g, b, a));
|
||||
}
|
||||
else if (pixelFormat == doc::PixelFormat::IMAGE_GRAYSCALE) {
|
||||
int v = graya_getv(prevPixelValue);
|
||||
int a = m_layerHasTransparentChannel ? graya_geta(prevPixelValue) : 255;
|
||||
if (chanID == psd::ChannelID::Red) {
|
||||
v = pixelValue;
|
||||
}
|
||||
else if (chanID == psd::ChannelID::Alpha ||
|
||||
chanID == psd::ChannelID::TransparencyMask) {
|
||||
a = pixelValue;
|
||||
}
|
||||
m_currentImage->putPixel(x, y, graya(v, a));
|
||||
}
|
||||
else if (pixelFormat == doc::PixelFormat::IMAGE_INDEXED) {
|
||||
m_currentImage->putPixel(x, y, (color_t)pixelValue);
|
||||
}
|
||||
else {
|
||||
throw std::runtime_error(
|
||||
"Only RGB/Grayscale/Indexed format is supported");
|
||||
}
|
||||
}
|
||||
|
||||
void createNewImage(const int width, const int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
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;
|
||||
Sprite* m_sprite;
|
||||
PixelFormat m_pixelFormat;
|
||||
std::vector<doc::Layer*> m_layers;
|
||||
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\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// This would be removed when support for 32bit per channel is supported
|
||||
if (header.depth >= 32) {
|
||||
fop->setError("Support for 32bit per channel isn't supported yet");
|
||||
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
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
1
src/psd
Submodule
1
src/psd
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 10acfc1c3439f5319fb013abd41f73f1892c98af
|
Loading…
x
Reference in New Issue
Block a user