Fix exporting selection to gif/fli/webp files (fix #3827)

This commit is contained in:
David Capello 2023-07-11 13:33:45 -03:00
parent bd91a6430f
commit 35e64ad2f3
19 changed files with 283 additions and 143 deletions

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A. // Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2016-2017 David Capello // Copyright (C) 2016-2017 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -28,7 +28,7 @@ FileOpROI CliOpenFile::roi() const
selFrames.insert(fromFrame, toFrame); selFrames.insert(fromFrame, toFrame);
return FileOpROI(document, return FileOpROI(document,
gfx::Rect(), document->sprite()->bounds(),
slice, slice,
tag, tag,
selFrames, selFrames,

View File

@ -228,8 +228,14 @@ void SaveFileBaseCommand::saveDocumentInBackground(
} }
gfx::Rect bounds; gfx::Rect bounds;
if (params().bounds.isSet()) if (params().bounds.isSet()) {
// Export the specific given bounds (e.g. the selection bounds)
bounds = params().bounds(); bounds = params().bounds();
}
else {
// Export the whole sprite canvas.
bounds = document->sprite()->bounds();
}
FileOpROI roi(document, bounds, FileOpROI roi(document, bounds,
params().slice(), params().tag(), params().slice(), params().tag(),

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A. // Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -1089,9 +1089,10 @@ bool BmpFormat::onLoad(FileOp *fop)
else else
rmask = gmask = bmask = amask = 0; rmask = gmask = bmask = amask = 0;
ImageRef image = fop->sequenceImage(pixelFormat, ImageRef image = fop->sequenceImageToLoad(
infoheader.biWidth, pixelFormat,
ABS((int)infoheader.biHeight)); infoheader.biWidth,
ABS((int)infoheader.biHeight));
if (!image) { if (!image) {
return false; return false;
} }
@ -1166,7 +1167,7 @@ bool BmpFormat::onLoad(FileOp *fop)
#ifdef ENABLE_SAVE #ifdef ENABLE_SAVE
bool BmpFormat::onSave(FileOp *fop) bool BmpFormat::onSave(FileOp *fop)
{ {
const FileAbstractImage* img = fop->abstractImage(); const FileAbstractImage* img = fop->abstractImageToSave();
const ImageSpec spec = img->spec(); const ImageSpec spec = img->spec();
const int w = spec.width(); const int w = spec.width();
const int h = spec.height(); const int h = spec.height();

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (c) 2018-2019 Igara Studio S.A. // Copyright (c) 2018-2023 Igara Studio S.A.
// //
// This program is distributed under the terms of // This program is distributed under the terms of
// the End-User License Agreement for Aseprite. // the End-User License Agreement for Aseprite.
@ -88,7 +88,7 @@ bool CssFormat::onLoad(FileOp* fop)
bool CssFormat::onSave(FileOp* fop) bool CssFormat::onSave(FileOp* fop)
{ {
const ImageRef image = fop->sequenceImage(); const ImageRef image = fop->sequenceImageToSave();
int x, y, c, r, g, b, a, alpha; int x, y, c, r, g, b, a, alpha;
const auto css_options = std::static_pointer_cast<CssOptions>(fop->formatOptions()); const auto css_options = std::static_pointer_cast<CssOptions>(fop->formatOptions());
FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb")); FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb"));

View File

@ -47,6 +47,7 @@
#include "ask_for_color_profile.xml.h" #include "ask_for_color_profile.xml.h"
#include "open_sequence.xml.h" #include "open_sequence.xml.h"
#include <algorithm>
#include <cstring> #include <cstring>
#include <cstdarg> #include <cstdarg>
@ -60,24 +61,39 @@ public:
: m_doc(fop->document()) : m_doc(fop->document())
, m_sprite(m_doc->sprite()) , m_sprite(m_doc->sprite())
, m_spec(m_sprite->spec()) , m_spec(m_sprite->spec())
, m_newBlend(fop->newBlend()) { , m_supportAnimation(fop->fileFormat()->support(FILE_SUPPORT_FRAMES))
, m_newBlend(fop->newBlend())
{
ASSERT(m_doc && m_sprite); ASSERT(m_doc && m_sprite);
} }
void setSpecSize(const gfx::Size& size) { void setSpecSize(const gfx::Size& fullCanvasSize,
m_spec.setWidth(size.w * m_scale.x); const gfx::Size& frameSize) {
m_spec.setHeight(size.h * m_scale.y); if (m_supportAnimation) {
m_spec.setSize(std::max<int>(1, fullCanvasSize.w*m_scale.x),
std::max<int>(1, fullCanvasSize.h*m_scale.y));
}
else {
m_spec.setSize(std::max<int>(1, frameSize.w*m_scale.x),
std::max<int>(1, frameSize.h*m_scale.y));
}
} }
void setUnscaledImage(const doc::frame_t frame, void setUnscaledImageToSave(const doc::frame_t frame,
const doc::ImageRef& image) { const doc::ImageRef& image) {
if (m_spec.width() == image->width() && // If we don't need to rescale the input "image", we can just
m_spec.height() == image->height()) { // reference the same exact image to encode (as we don't need to
// call resize_image()).
if (!needResize()) {
m_tmpScaledImage = image; m_tmpScaledImage = image;
} }
else { else {
if (!m_tmpScaledImage) // In other case we need to create a temporal image to resize
// the input "image" to "m_tmpScaledImage" for the encoder.
if (!m_tmpScaledImage ||
m_tmpScaledImage->spec() != m_spec) {
m_tmpScaledImage.reset(doc::Image::create(m_spec)); m_tmpScaledImage.reset(doc::Image::create(m_spec));
}
doc::algorithm::resize_image( doc::algorithm::resize_image(
image.get(), image.get(),
@ -132,13 +148,16 @@ public:
return m_tmpScaledImage->getPixelAddress(0, y); return m_tmpScaledImage->getPixelAddress(0, y);
} }
void renderFrame(const doc::frame_t frame, doc::Image* dst) const override { void renderFrame(const doc::frame_t frame,
const bool needResize = const gfx::Rect& frameBounds,
(dst->width() != m_sprite->width() || doc::Image* dst) const override {
dst->height() != m_sprite->height()); const bool needResize = this->needResize();
if (needResize && !m_tmpUnscaledRender) { if (needResize &&
(!m_tmpUnscaledRender ||
m_tmpUnscaledRender->size() != frameBounds.size())) {
auto spec = m_sprite->spec(); auto spec = m_sprite->spec();
spec.setSize(frameBounds.size());
spec.setColorMode(dst->colorMode()); spec.setColorMode(dst->colorMode());
m_tmpUnscaledRender.reset(doc::Image::create(spec)); m_tmpUnscaledRender.reset(doc::Image::create(spec));
} }
@ -148,7 +167,8 @@ public:
render.setBgOptions(render::BgOptions::MakeNone()); render.setBgOptions(render::BgOptions::MakeNone());
render.renderSprite( render.renderSprite(
(needResize ? m_tmpUnscaledRender.get(): dst), (needResize ? m_tmpUnscaledRender.get(): dst),
m_sprite, frame); m_sprite, frame,
gfx::Clip(gfx::Point(0, 0), frameBounds));
if (needResize) { if (needResize) {
doc::algorithm::resize_image( doc::algorithm::resize_image(
@ -168,10 +188,15 @@ public:
} }
private: private:
bool needResize() const {
return (m_scale != gfx::PointF(1.0, 1.0));
}
const Doc* m_doc; const Doc* m_doc;
const doc::Sprite* m_sprite; const doc::Sprite* m_sprite;
doc::ImageSpec m_spec; doc::ImageSpec m_spec;
bool m_newBlend; const bool m_supportAnimation;
const bool m_newBlend;
doc::ImageRef m_tmpScaledImage = nullptr; doc::ImageRef m_tmpScaledImage = nullptr;
mutable doc::ImageRef m_tmpUnscaledRender = nullptr; mutable doc::ImageRef m_tmpUnscaledRender = nullptr;
gfx::PointF m_scale = gfx::PointF(1.0, 1.0); gfx::PointF m_scale = gfx::PointF(1.0, 1.0);
@ -232,7 +257,8 @@ int save_document(Context* context, Doc* document)
std::unique_ptr<FileOp> fop( std::unique_ptr<FileOp> fop(
FileOp::createSaveDocumentOperation( FileOp::createSaveDocumentOperation(
context, context,
FileOpROI(document, gfx::Rect(), "", "", SelectedFrames(), false), FileOpROI(document, document->sprite()->bounds(),
"", "", SelectedFrames(), false),
document->filename(), "", document->filename(), "",
false)); false));
if (!fop) if (!fop)
@ -303,6 +329,37 @@ FileOpROI::FileOpROI(const Doc* doc,
} }
} }
gfx::Rect FileOpROI::frameBounds(const frame_t frame) const
{
// Export bounds of specific slice
if (m_slice) {
const SliceKey* key = m_slice->getByFrame(frame);
if (!key || key->isEmpty())
return gfx::Rect(); // Return an empty rectangle
return key->bounds();
}
else {
// Export specific bounds
ASSERT(!m_bounds.isEmpty());
return m_bounds;
}
}
gfx::Size FileOpROI::fileCanvasSize() const
{
if (m_slice) {
gfx::Size size;
for (auto frame : m_selFrames)
size |= frameBounds(frame).size();
return size;
}
else {
ASSERT(!m_bounds.isEmpty());
return m_bounds.size();
}
}
// static // static
FileOp* FileOp::createLoadDocumentOperation(Context* context, FileOp* FileOp::createLoadDocumentOperation(Context* context,
const std::string& filename, const std::string& filename,
@ -859,7 +916,7 @@ void FileOp::operate(IFileOpProgress* progress)
} }
// We don't need this image // We don't need this image
else { else {
delete m_seq.image; m_seq.image.reset();
// But add a link frame // But add a link frame
m_seq.last_cel->image = image_index; m_seq.last_cel->image = image_index;
@ -949,8 +1006,8 @@ void FileOp::operate(IFileOpProgress* progress)
// Create a temporary bitmap // Create a temporary bitmap
m_seq.image.reset(Image::create(sprite->pixelFormat(), m_seq.image.reset(Image::create(sprite->pixelFormat(),
sprite->width(), m_roi.fileCanvasSize().w,
sprite->height())); m_roi.fileCanvasSize().h));
m_seq.progress_offset = 0.0f; m_seq.progress_offset = 0.0f;
m_seq.progress_fraction = 1.0f / (double)sprite->totalFrames(); m_seq.progress_fraction = 1.0f / (double)sprite->totalFrames();
@ -961,39 +1018,19 @@ void FileOp::operate(IFileOpProgress* progress)
frame_t outputFrame = 0; frame_t outputFrame = 0;
for (frame_t frame : m_roi.selectedFrames()) { for (frame_t frame : m_roi.selectedFrames()) {
gfx::Rect bounds; gfx::Rect bounds = m_roi.frameBounds(frame);
if (bounds.isEmpty())
continue; // Skip frame because there is no slice key
// Export bounds of specific slice if (m_abstractImage) {
if (m_roi.slice()) { m_abstractImage->setSpecSize(m_roi.fileCanvasSize(),
const SliceKey* key = m_roi.slice()->getByFrame(frame); bounds.size());
if (!key || key->isEmpty())
continue; // Skip frame because there is no slice key
bounds = key->bounds();
}
// Export specific bounds
else if (!m_roi.bounds().isEmpty()) {
bounds = m_roi.bounds();
} }
// Draw the "frame" in "m_seq.image" with the given bounds // Render the (unscaled) sequenced image.
// (bounds can be the selection bounds or a slice key bounds) render.renderSprite(
if (!bounds.isEmpty()) { m_seq.image.get(), sprite, frame,
if (m_abstractImage) gfx::Clip(gfx::Point(0, 0), bounds));
m_abstractImage->setSpecSize(bounds.size());
m_seq.image.reset(
Image::create(sprite->pixelFormat(),
bounds.w,
bounds.h));
render.renderSprite(
m_seq.image.get(), sprite, frame,
gfx::Clip(gfx::Point(0, 0), bounds));
}
else {
render.renderSprite(m_seq.image.get(), sprite, frame);
}
bool save = true; bool save = true;
@ -1035,6 +1072,11 @@ void FileOp::operate(IFileOpProgress* progress)
else { else {
makeDirectories(); makeDirectories();
if (m_abstractImage) {
m_abstractImage->setSpecSize(m_roi.fileCanvasSize(),
m_roi.fileCanvasSize());
}
// Call the "save" procedure. // Call the "save" procedure.
if (!m_format->save(this)) { if (!m_format->save(this)) {
setError("Error saving the sprite in the file \"%s\"\n", setError("Error saving the sprite in the file \"%s\"\n",
@ -1308,7 +1350,9 @@ void FileOp::sequenceGetAlpha(int index, int* a) const
*a = 0; *a = 0;
} }
ImageRef FileOp::sequenceImage(PixelFormat pixelFormat, int w, int h) ImageRef FileOp::sequenceImageToLoad(
const PixelFormat pixelFormat,
const int w, const int h)
{ {
Sprite* sprite; Sprite* sprite;
@ -1340,7 +1384,7 @@ ImageRef FileOp::sequenceImage(PixelFormat pixelFormat, int w, int h)
} }
if (m_seq.last_cel) { if (m_seq.last_cel) {
setError("Error: called two times FileOp::sequenceImage()\n"); setError("Error: called two times FileOp::sequenceImageToLoad()\n");
return nullptr; return nullptr;
} }
@ -1358,15 +1402,17 @@ void FileOp::makeAbstractImage()
m_abstractImage = std::make_unique<FileAbstractImageImpl>(this); m_abstractImage = std::make_unique<FileAbstractImageImpl>(this);
} }
FileAbstractImage* FileOp::abstractImage() FileAbstractImage* FileOp::abstractImageToSave()
{ {
ASSERT(m_format->support(FILE_ENCODE_ABSTRACT_IMAGE)); ASSERT(m_format->support(FILE_ENCODE_ABSTRACT_IMAGE));
makeAbstractImage(); makeAbstractImage();
// Use sequenceImage() to fill the current image // Use sequenceImageToSave() to fill the current image
if (m_format->support(FILE_SUPPORT_SEQUENCES)) if (m_format->support(FILE_SUPPORT_SEQUENCES)) {
m_abstractImage->setUnscaledImage(m_seq.frame, sequenceImage()); m_abstractImage->setUnscaledImageToSave(m_seq.frame++,
m_seq.image);
}
return m_abstractImage.get(); return m_abstractImage.get();
} }

View File

@ -79,7 +79,6 @@ namespace app {
const bool adjustByTag); const bool adjustByTag);
const Doc* document() const { return m_document; } const Doc* document() const { return m_document; }
const gfx::Rect& bounds() const { return m_bounds; }
doc::Slice* slice() const { return m_slice; } doc::Slice* slice() const { return m_slice; }
doc::Tag* tag() const { return m_tag; } doc::Tag* tag() const { return m_tag; }
doc::frame_t fromFrame() const { return m_selFrames.firstFrame(); } doc::frame_t fromFrame() const { return m_selFrames.firstFrame(); }
@ -90,6 +89,15 @@ namespace app {
return (doc::frame_t)m_selFrames.size(); return (doc::frame_t)m_selFrames.size();
} }
// Returns an empty rectangle only when exporting a slice and the
// slice doesn't have a slice key in this specific frame.
gfx::Rect frameBounds(const frame_t frame) const;
// Canvas size required to store all frames (e.g. if a slice
// changes size on each frame, we have to keep the biggest of
// those sizes).
gfx::Size fileCanvasSize() const;
private: private:
const Doc* m_document; const Doc* m_document;
gfx::Rect m_bounds; gfx::Rect m_bounds;
@ -108,6 +116,7 @@ namespace app {
virtual int width() const { return spec().width(); } virtual int width() const { return spec().width(); }
virtual int height() const { return spec().height(); } virtual int height() const { return spec().height(); }
// Spec (width/height) to save the file.
virtual const doc::ImageSpec& spec() const = 0; virtual const doc::ImageSpec& spec() const = 0;
virtual os::ColorSpaceRef osColorSpace() const = 0; virtual os::ColorSpaceRef osColorSpace() const = 0;
virtual bool needAlpha() const = 0; virtual bool needAlpha() const = 0;
@ -118,15 +127,21 @@ namespace app {
virtual const doc::Palette* palette(doc::frame_t frame) const = 0; virtual const doc::Palette* palette(doc::frame_t frame) const = 0;
virtual doc::PalettesList palettes() const = 0; virtual doc::PalettesList palettes() const = 0;
// Returns the whole image to be saved (for encoders that needs
// all the rows at once).
virtual const doc::ImageRef getScaledImage() const = 0; virtual const doc::ImageRef getScaledImage() const = 0;
// In case the file format can encode scanline by scanline // In case that the file format can encode scanline by scanline
// (e.g. PNG format). // (e.g. PNG format) we can request each row to encode (without
// the need to call getScaledImage()). Each scanline depends on
// the spec() width.
virtual const uint8_t* getScanline(int y) const = 0; virtual const uint8_t* getScanline(int y) const = 0;
// In case that the encoder needs full frame renders (or compare // In case that the encoder supports animation and needs to render
// between frames), e.g. GIF format. // a full frame renders.
virtual void renderFrame(const doc::frame_t frame, doc::Image* dst) const = 0; virtual void renderFrame(const doc::frame_t frame,
const gfx::Rect& frameBounds,
doc::Image* dst) const = 0;
}; };
// Structure to load & save files. // Structure to load & save files.
@ -155,6 +170,7 @@ namespace app {
bool isSequence() const { return !m_seq.filename_list.empty(); } bool isSequence() const { return !m_seq.filename_list.empty(); }
bool isOneFrame() const { return m_oneframe; } bool isOneFrame() const { return m_oneframe; }
bool preserveColorProfile() const { return m_config.preserveColorProfile; } bool preserveColorProfile() const { return m_config.preserveColorProfile; }
const FileFormat* fileFormat() const { return m_format; }
const std::string& filename() const { return m_filename; } const std::string& filename() const { return m_filename; }
const base::paths& filenames() const { return m_seq.filename_list; } const base::paths& filenames() const { return m_seq.filename_list; }
@ -227,8 +243,8 @@ namespace app {
void sequenceGetColor(int index, int* r, int* g, int* b) const; void sequenceGetColor(int index, int* r, int* g, int* b) const;
void sequenceSetAlpha(int index, int a); void sequenceSetAlpha(int index, int a);
void sequenceGetAlpha(int index, int* a) const; void sequenceGetAlpha(int index, int* a) const;
ImageRef sequenceImage(PixelFormat pixelFormat, int w, int h); ImageRef sequenceImageToLoad(PixelFormat pixelFormat, int w, int h);
const ImageRef sequenceImage() const { return m_seq.image; } const ImageRef sequenceImageToSave() const { return m_seq.image; }
const Palette* sequenceGetPalette() const { return m_seq.palette; } const Palette* sequenceGetPalette() const { return m_seq.palette; }
bool sequenceGetHasAlpha() const { bool sequenceGetHasAlpha() const {
return m_seq.has_alpha; return m_seq.has_alpha;
@ -241,8 +257,12 @@ namespace app {
} }
// Can be used to encode sequences/static files (e.g. png files) // Can be used to encode sequences/static files (e.g. png files)
// or animations (e.g. gif) resizing the result on the fly. // or animations (e.g. gif) resizing the result on the fly. This
FileAbstractImage* abstractImage(); // function is called for each frame to be saved for sequence-like
// files, or just once to encode animation formats.
// The file format needs the FILE_ENCODE_ABSTRACT_IMAGE flag to
// use this.
FileAbstractImage* abstractImageToSave();
void setOnTheFlyScale(const gfx::PointF& scale); void setOnTheFlyScale(const gfx::PointF& scale);
const std::string& error() const { return m_error; } const std::string& error() const { return m_error; }

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2018-2022 Igara Studio S.A. // Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -206,7 +206,7 @@ static int get_time_precision(const FileAbstractImage* sprite,
bool FliFormat::onSave(FileOp* fop) bool FliFormat::onSave(FileOp* fop)
{ {
const FileAbstractImage* sprite = fop->abstractImage(); const FileAbstractImage* sprite = fop->abstractImageToSave();
// Open the file to write in binary mode // Open the file to write in binary mode
FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb")); FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb"));
@ -251,7 +251,7 @@ bool FliFormat::onSave(FileOp* fop)
} }
// Render the frame in the bitmap // Render the frame in the bitmap
sprite->renderFrame(frame, bmp.get()); sprite->renderFrame(frame, fop->roi().frameBounds(frame), bmp.get());
// How many times this frame should be written to get the same // How many times this frame should be written to get the same
// time that it has in the sprite // time that it has in the sprite

View File

@ -970,7 +970,7 @@ public:
: m_fop(fop) : m_fop(fop)
, m_gifFile(gifFile) , m_gifFile(gifFile)
, m_sprite(fop->document()->sprite()) , m_sprite(fop->document()->sprite())
, m_img(fop->abstractImage()) , m_img(fop->abstractImageToSave())
, m_spec(m_img->spec()) , m_spec(m_img->spec())
, m_spriteBounds(m_spec.bounds()) , m_spriteBounds(m_spec.bounds())
, m_hasBackground(m_img->isOpaque()) , m_hasBackground(m_img->isOpaque())
@ -1570,7 +1570,7 @@ private:
clear_image(dst, m_bgIndex); clear_image(dst, m_bgIndex);
else else
clear_image(dst, 0); clear_image(dst, 0);
m_img->renderFrame(frame, dst); m_img->renderFrame(frame, m_fop->roi().frameBounds(frame), dst);
} }
private: private:

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2018-2022 Igara Studio S.A. // Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -178,7 +178,7 @@ bool JpegFormat::onLoad(FileOp* fop)
jpeg_start_decompress(&dinfo); jpeg_start_decompress(&dinfo);
// Create the image. // Create the image.
ImageRef image = fop->sequenceImage( ImageRef image = fop->sequenceImageToLoad(
(dinfo.out_color_space == JCS_RGB ? IMAGE_RGB: (dinfo.out_color_space == JCS_RGB ? IMAGE_RGB:
IMAGE_GRAYSCALE), IMAGE_GRAYSCALE),
dinfo.output_width, dinfo.output_width,
@ -353,7 +353,7 @@ bool JpegFormat::onSave(FileOp* fop)
{ {
struct jpeg_compress_struct cinfo; struct jpeg_compress_struct cinfo;
struct error_mgr jerr; struct error_mgr jerr;
const FileAbstractImage* img = fop->abstractImage(); const FileAbstractImage* img = fop->abstractImageToSave();
const ImageSpec spec = img->spec(); const ImageSpec spec = img->spec();
JSAMPARRAY buffer; JSAMPARRAY buffer;
JDIMENSION buffer_height; JDIMENSION buffer_height;

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2022 Igara Studio S.A. // Copyright (C) 2022-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -106,10 +106,10 @@ bool PcxFormat::onLoad(FileOp* fop)
for (c=0; c<60; c++) /* skip some more junk */ for (c=0; c<60; c++) /* skip some more junk */
fgetc(f); fgetc(f);
ImageRef image = fop->sequenceImage(bpp == 8 ? ImageRef image = fop->sequenceImageToLoad(
IMAGE_INDEXED: (bpp == 8 ? IMAGE_INDEXED:
IMAGE_RGB, IMAGE_RGB),
width, height); width, height);
if (!image) { if (!image) {
return false; return false;
} }
@ -192,7 +192,7 @@ bool PcxFormat::onLoad(FileOp* fop)
#ifdef ENABLE_SAVE #ifdef ENABLE_SAVE
bool PcxFormat::onSave(FileOp* fop) bool PcxFormat::onSave(FileOp* fop)
{ {
const FileAbstractImage* img = fop->abstractImage(); const FileAbstractImage* img = fop->abstractImageToSave();
const ImageSpec spec = img->spec(); const ImageSpec spec = img->spec();
int c, r, g, b; int c, r, g, b;
int x, y; int x, y;

View File

@ -278,7 +278,8 @@ bool PngFormat::onLoad(FileOp* fop)
int imageWidth = png_get_image_width(png, info); int imageWidth = png_get_image_width(png, info);
int imageHeight = png_get_image_height(png, info); int imageHeight = png_get_image_height(png, info);
ImageRef image = fop->sequenceImage(pixelFormat, imageWidth, imageHeight); ImageRef image = fop->sequenceImageToLoad(
pixelFormat, imageWidth, imageHeight);
if (!image) if (!image)
return false; return false;
@ -551,7 +552,7 @@ bool PngFormat::onSave(FileOp* fop)
png_init_io(png, fp); png_init_io(png, fp);
const FileAbstractImage* img = fop->abstractImage(); const FileAbstractImage* img = fop->abstractImageToSave();
const ImageSpec spec = img->spec(); const ImageSpec spec = img->spec();
switch (spec.colorMode()) { switch (spec.colorMode()) {

View File

@ -76,9 +76,10 @@ bool QoiFormat::onLoad(FileOp* fop)
if (!pixels) if (!pixels)
return false; return false;
ImageRef image = fop->sequenceImage(IMAGE_RGB, ImageRef image = fop->sequenceImageToLoad(
desc.width, IMAGE_RGB,
desc.height); desc.width,
desc.height);
if (!image) if (!image)
return false; return false;
@ -136,7 +137,7 @@ bool QoiFormat::onLoad(FileOp* fop)
bool QoiFormat::onSave(FileOp* fop) bool QoiFormat::onSave(FileOp* fop)
{ {
const FileAbstractImage* img = fop->abstractImage(); const FileAbstractImage* img = fop->abstractImageToSave();
FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb")); FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb"));
FILE* f = handle.get(); FILE* f = handle.get();
doc::ImageRef image = img->getScaledImage(); doc::ImageRef image = img->getScaledImage();

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (c) 2018-2022 Igara Studio S.A. // Copyright (c) 2018-2023 Igara Studio S.A.
// //
// This program is distributed under the terms of // This program is distributed under the terms of
// the End-User License Agreement for Aseprite. // the End-User License Agreement for Aseprite.
@ -81,7 +81,7 @@ bool SvgFormat::onLoad(FileOp* fop)
bool SvgFormat::onSave(FileOp* fop) bool SvgFormat::onSave(FileOp* fop)
{ {
const ImageRef image = fop->sequenceImage(); const ImageRef image = fop->sequenceImageToSave();
int x, y, c, r, g, b, a, alpha; int x, y, c, r, g, b, a, alpha;
const auto svg_options = std::static_pointer_cast<SvgOptions>(fop->formatOptions()); const auto svg_options = std::static_pointer_cast<SvgOptions>(fop->formatOptions());
const int pixelScaleValue = std::clamp(svg_options->pixelScale, 0, 10000); const int pixelScaleValue = std::clamp(svg_options->pixelScale, 0, 10000);

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A. // Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -165,9 +165,10 @@ bool TgaFormat::onLoad(FileOp* fop)
if (decoder.hasAlpha()) if (decoder.hasAlpha())
fop->sequenceSetHasAlpha(true); fop->sequenceSetHasAlpha(true);
ImageRef image = fop->sequenceImage((doc::PixelFormat)spec.colorMode(), ImageRef image = fop->sequenceImageToLoad(
spec.width(), (doc::PixelFormat)spec.colorMode(),
spec.height()); spec.width(),
spec.height());
if (!image) if (!image)
return false; return false;
@ -288,7 +289,7 @@ void prepare_header(tga::Header& header,
bool TgaFormat::onSave(FileOp* fop) bool TgaFormat::onSave(FileOp* fop)
{ {
const FileAbstractImage* img = fop->abstractImage(); const FileAbstractImage* img = fop->abstractImageToSave();
const Palette* palette = fop->sequenceGetPalette(); const Palette* palette = fop->sequenceGetPalette();
FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb")); FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb"));

View File

@ -257,7 +257,7 @@ bool WebPFormat::onSave(FileOp* fop)
FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb")); FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb"));
FILE* fp = handle.get(); FILE* fp = handle.get();
const FileAbstractImage* sprite = fop->abstractImage(); const FileAbstractImage* sprite = fop->abstractImageToSave();
const int w = sprite->width(); const int w = sprite->width();
const int h = sprite->height(); const int h = sprite->height();
@ -319,7 +319,7 @@ bool WebPFormat::onSave(FileOp* fop)
for (frame_t frame : fop->roi().selectedFrames()) { for (frame_t frame : fop->roi().selectedFrames()) {
// Render the frame in the bitmap // Render the frame in the bitmap
clear_image(image.get(), image->maskColor()); clear_image(image.get(), image->maskColor());
sprite->renderFrame(frame, image.get()); sprite->renderFrame(frame, fop->roi().frameBounds(frame), image.get());
// Switch R <-> B channels because WebPAnimEncoderAssemble() // Switch R <-> B channels because WebPAnimEncoderAssemble()
// expects MODE_BGRA pictures. // expects MODE_BGRA pictures.

View File

@ -178,7 +178,7 @@ cd $oldwd
if [[ "$(uname)" =~ "MINGW" ]] || [[ "$(uname)" =~ "MSYS" ]] ; then if [[ "$(uname)" =~ "MINGW" ]] || [[ "$(uname)" =~ "MSYS" ]] ; then
# Ignore this test on Windows because we cannot give * as a parameter (?) # Ignore this test on Windows because we cannot give * as a parameter (?)
echo Do nothing echo Skip one -save-as test because Windows does not support using asterisk in arguments without listing files
else else
d=$t/save-as-groups-and-hidden d=$t/save-as-groups-and-hidden
$ASEPRITE -b sprites/groups2.aseprite -layer \* -save-as "$d/g2-all.png" || exit 1 $ASEPRITE -b sprites/groups2.aseprite -layer \* -save-as "$d/g2-all.png" || exit 1
@ -354,3 +354,56 @@ for f = 1,#b.frames do
end end
EOF EOF
$ASEPRITE -b -script "$d/compare.lua" || exit 1 $ASEPRITE -b -script "$d/compare.lua" || exit 1
# Test -save-as selection to gif
# https://github.com/aseprite/aseprite/issues/3827
d=$t/save-selection-to-gif
mkdir $d
cat >$d/save.lua <<EOF
local a = app.open("sprites/tags3.aseprite")
assert(a.width == 4)
assert(a.height == 4)
app.command.SaveFileCopyAs{
filename="$d/output.gif",
bounds=Rectangle(1, 2, 3, 2)
}
local b = app.open("$d/output.gif")
assert(b.width == 3)
assert(b.height == 2)
EOF
"$ASEPRITE" -b -script "$d/save.lua" || exit 1
# Saving moving slice
d=$t/save-moving-slice
$ASEPRITE -b sprites/slices-moving.aseprite -slice square -save-as $d/output.gif || exit 1
$ASEPRITE -b sprites/slices-moving.aseprite -slice square -save-as $d/output.png || exit 1
$ASEPRITE -b sprites/slices-moving.aseprite -scale 2 -slice square -save-as $d/scaled.gif || exit 1
$ASEPRITE -b sprites/slices-moving.aseprite -scale 2 -slice square -save-as $d/scaled.png || exit 1
cat >$d/compare.lua <<EOF
local a = app.open("$d/output.gif")
local b = app.open("$d/output1.png")
app.command.OpenFile{ filename="$d/output1.png", oneframe=1 } local b1 = app.sprite
app.command.OpenFile{ filename="$d/output2.png", oneframe=1 } local b2 = app.sprite
app.command.OpenFile{ filename="$d/output3.png", oneframe=1 } local b3 = app.sprite
app.command.OpenFile{ filename="$d/output4.png", oneframe=1 } local b4 = app.sprite
assert(a.bounds == Rectangle(0, 0, 4, 2))
assert(b.bounds == Rectangle(0, 0, 4, 2))
assert(b1.bounds == Rectangle(0, 0, 2, 2))
assert(b2.bounds == Rectangle(0, 0, 4, 2))
assert(b3.bounds == Rectangle(0, 0, 3, 2))
assert(b4.bounds == Rectangle(0, 0, 4, 2))
local c = app.open("$d/scaled.gif")
local d = app.open("$d/scaled1.png")
app.command.OpenFile{ filename="$d/scaled1.png", oneframe=1 } local d1 = app.sprite
app.command.OpenFile{ filename="$d/scaled2.png", oneframe=1 } local d2 = app.sprite
app.command.OpenFile{ filename="$d/scaled3.png", oneframe=1 } local d3 = app.sprite
app.command.OpenFile{ filename="$d/scaled4.png", oneframe=1 } local d4 = app.sprite
assert(c.bounds == Rectangle(0, 0, 8, 4))
assert(d.bounds == Rectangle(0, 0, 8, 4))
assert(d1.bounds == Rectangle(0, 0, 4, 4))
assert(d2.bounds == Rectangle(0, 0, 8, 4))
assert(d3.bounds == Rectangle(0, 0, 6, 4))
assert(d4.bounds == Rectangle(0, 0, 8, 4))
EOF
$ASEPRITE -b -script "$d/compare.lua" || exit 1

View File

@ -1,9 +1,25 @@
-- Copyright (C) 2022 Igara Studio S.A. -- Copyright (C) 2022-2023 Igara Studio S.A.
-- --
-- This file is released under the terms of the MIT license. -- This file is released under the terms of the MIT license.
-- Read LICENSE.txt for more information. -- Read LICENSE.txt for more information.
function fix_test_img(testImg, scale, fileExt, cm, c1) function fix_images(testImg, scale, fileExt, c, cm, c1)
-- GIF file is loaded as indexed, so we have to convert from indexed
-- to the ColorMode
if c.colorMode ~= cm then
assert(fileExt == "gif" or fileExt == "bmp")
if cm == ColorMode.RGB then
app.sprite = c
app.command.ChangePixelFormat{ format="rgb" }
elseif cm == ColorMode.GRAYSCALE then
app.sprite = c
app.command.ChangePixelFormat{ format="grayscale" }
else
assert(false)
end
end
-- With file formats that don't support alpha channel, we -- With file formats that don't support alpha channel, we
-- compare totally transparent pixels (alpha=0) with black. -- compare totally transparent pixels (alpha=0) with black.
if fileExt == "tga" and cm == ColorMode.GRAYSCALE then if fileExt == "tga" and cm == ColorMode.GRAYSCALE then
@ -22,6 +38,16 @@ function fix_test_img(testImg, scale, fileExt, cm, c1)
testImg:resize(testImg.width*scale, testImg.height*scale) testImg:resize(testImg.width*scale, testImg.height*scale)
end end
function compatible_modes(fileExt, cm)
return
-- TODO support saving any color mode to FLI files on the fly
(fileExt ~= "fli" or cm == ColorMode.INDEXED) and
-- TODO Review grayscale support in bmp files
(fileExt ~= "bmp" or cm ~= ColorMode.GRAYSCALE) and
-- TODO Review grayscale/indexed support in webp files
(fileExt ~= "webp" or cm == ColorMode.RGB)
end
for _,cm in ipairs{ ColorMode.RGB, for _,cm in ipairs{ ColorMode.RGB,
ColorMode.GRAYSCALE, ColorMode.GRAYSCALE,
ColorMode.INDEXED } do ColorMode.INDEXED } do
@ -59,44 +85,26 @@ for _,cm in ipairs{ ColorMode.RGB,
assert(spr.filename == "_test_b.png") assert(spr.filename == "_test_b.png")
-- Scale -- Scale
for _,fn in ipairs{ "_test_c_scaled.png", for _,fn in ipairs{ "_test_c_scaled.bmp",
"_test_c_scaled.gif",
"_test_c_scaled.fli", "_test_c_scaled.fli",
"_test_c_scaled.gif",
"_test_c_scaled.png",
"_test_c_scaled.tga", "_test_c_scaled.tga",
"_test_c_scaled.bmp" } do "_test_c_scaled.webp" } do
local fileExt = app.fs.fileExtension(fn) local fileExt = app.fs.fileExtension(fn)
-- TODO support saving any color mode to FLI files on the fly if compatible_modes(fileExt, cm) then
if (fileExt ~= "fli" or cm == ColorMode.INDEXED) and for _,scale in ipairs({ 0.25, 0.5, 1, 2, 3, 4 }) do
-- TODO Review grayscale support in bmp files
(fileExt ~= "bmp" or cm ~= ColorMode.GRAYSCALE) then
for _,scale in ipairs({ 1, 2, 3, 4 }) do
print(fn, scale, cm) print(fn, scale, cm)
app.activeSprite = spr app.sprite = spr
app.command.SaveFileCopyAs{ filename=fn, scale=scale } app.command.SaveFileCopyAs{ filename=fn, scale=scale }
local c = app.open(fn) local c = app.open(fn)
assert(c.width == spr.width*scale) assert(c.width == spr.width*scale)
assert(c.height == spr.height*scale) assert(c.height == spr.height*scale)
-- GIF file is loaded as indexed, so we have to convert from
-- indexed to the ColorMode
if c.colorMode ~= cm then
assert(fileExt == "gif" or fileExt == "bmp")
if cm == ColorMode.RGB then
app.activeSprite = c
app.command.ChangePixelFormat{ format="rgb" }
elseif cm == ColorMode.GRAYSCALE then
app.activeSprite = c
app.command.ChangePixelFormat{ format="grayscale" }
else
assert(false)
end
end
local testImg = Image(spr.cels[1].image) local testImg = Image(spr.cels[1].image)
fix_test_img(testImg, scale, fileExt, cm, c1) fix_images(testImg, scale, fileExt, c, cm, c1)
if not c.cels[1].image:isEqual(testImg) then if not c.cels[1].image:isEqual(testImg) then
c.cels[1].image:saveAs("_testA.png") c.cels[1].image:saveAs("_testA.png")
testImg:saveAs("_testB.png") testImg:saveAs("_testB.png")
@ -110,26 +118,26 @@ for _,cm in ipairs{ ColorMode.RGB,
-- Scale + Slices -- Scale + Slices
local slice = spr:newSlice(Rectangle(1, 2, 8, 15)) local slice = spr:newSlice(Rectangle(1, 2, 8, 15))
slice.name = "small_slice" slice.name = "small_slice"
for _,fn in ipairs({ "_test_c_small_slice.png", for _,fn in ipairs({ "_test_c_small_slice.bmp",
-- TODO slices aren't supported in gif/fli yet "_test_c_small_slice.fli",
--"_test_c_small_slice.gif", "_test_c_small_slice.gif",
--"_test_c_small_slice.fli", "_test_c_small_slice.png",
"_test_c_small_slice.tga", "_test_c_small_slice.tga",
"_test_c_small_slice.bmp" }) do "_test_c_small_slice.webp" }) do
local fileExt = app.fs.fileExtension(fn) local fileExt = app.fs.fileExtension(fn)
if (fileExt ~= "bmp" or cm ~= ColorMode.GRAYSCALE) then if compatible_modes(fileExt, cm) then
for _,scale in ipairs({ 1, 2, 3, 4 }) do for _,scale in ipairs({ 0.25, 0.5, 1, 2, 3, 4 }) do
print(fn, scale, cm) print(fn, scale, cm)
app.activeSprite = spr app.sprite = spr
app.command.SaveFileCopyAs{ filename=fn, slice="small_slice", scale=scale } app.command.SaveFileCopyAs{ filename=fn, slice="small_slice", scale=scale }
local c = app.open(fn) local c = app.open(fn)
assert(c.width == slice.bounds.width*scale) assert(c.width == slice.bounds.width*scale)
assert(c.height == slice.bounds.height*scale) assert(c.height == slice.bounds.height*scale)
local testImg = Image(spr.cels[1].image, spr.slices[1].bounds) local testImg = Image(spr.cels[1].image, spr.slices[1].bounds)
fix_test_img(testImg, scale, fileExt, cm, c1) fix_images(testImg, scale, fileExt, c, cm, c1)
if not c.cels[1].image:isEqual(testImg) then if not c.cels[1].image:isEqual(testImg) then
c.cels[1].image:saveAs("_testA.png") c.cels[1].image:saveAs("_testA.png")
testImg:saveAs("_testB.png") testImg:saveAs("_testB.png")

View File

@ -28,3 +28,6 @@
* `file-tests-props.aseprite`: Indexed, 64x64, 6 frames, 4 layers (one * `file-tests-props.aseprite`: Indexed, 64x64, 6 frames, 4 layers (one
of them is a tilemap), 13 cels, 1 tag. of them is a tilemap), 13 cels, 1 tag.
* `slices.aseprite`: Indexed, 4x4, background layer, 2 slices. * `slices.aseprite`: Indexed, 4x4, background layer, 2 slices.
* `slices-moving.aseprite`: Indexed, 4x4, 1 linked cel in 4 frames,
background layer, 1 slice with 4 keyframes (each keyframe with a
different position/size).

Binary file not shown.