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

View File

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

View File

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

View File

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

View File

@ -79,7 +79,6 @@ namespace app {
const bool adjustByTag);
const Doc* document() const { return m_document; }
const gfx::Rect& bounds() const { return m_bounds; }
doc::Slice* slice() const { return m_slice; }
doc::Tag* tag() const { return m_tag; }
doc::frame_t fromFrame() const { return m_selFrames.firstFrame(); }
@ -90,6 +89,15 @@ namespace app {
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:
const Doc* m_document;
gfx::Rect m_bounds;
@ -108,6 +116,7 @@ namespace app {
virtual int width() const { return spec().width(); }
virtual int height() const { return spec().height(); }
// Spec (width/height) to save the file.
virtual const doc::ImageSpec& spec() const = 0;
virtual os::ColorSpaceRef osColorSpace() 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 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;
// In case the file format can encode scanline by scanline
// (e.g. PNG format).
// In case that the file format can encode scanline by scanline
// (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;
// In case that the encoder needs full frame renders (or compare
// between frames), e.g. GIF format.
virtual void renderFrame(const doc::frame_t frame, doc::Image* dst) const = 0;
// In case that the encoder supports animation and needs to render
// a full frame renders.
virtual void renderFrame(const doc::frame_t frame,
const gfx::Rect& frameBounds,
doc::Image* dst) const = 0;
};
// Structure to load & save files.
@ -155,6 +170,7 @@ namespace app {
bool isSequence() const { return !m_seq.filename_list.empty(); }
bool isOneFrame() const { return m_oneframe; }
bool preserveColorProfile() const { return m_config.preserveColorProfile; }
const FileFormat* fileFormat() const { return m_format; }
const std::string& filename() const { return m_filename; }
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 sequenceSetAlpha(int index, int a);
void sequenceGetAlpha(int index, int* a) const;
ImageRef sequenceImage(PixelFormat pixelFormat, int w, int h);
const ImageRef sequenceImage() const { return m_seq.image; }
ImageRef sequenceImageToLoad(PixelFormat pixelFormat, int w, int h);
const ImageRef sequenceImageToSave() const { return m_seq.image; }
const Palette* sequenceGetPalette() const { return m_seq.palette; }
bool sequenceGetHasAlpha() const {
return m_seq.has_alpha;
@ -241,8 +257,12 @@ namespace app {
}
// Can be used to encode sequences/static files (e.g. png files)
// or animations (e.g. gif) resizing the result on the fly.
FileAbstractImage* abstractImage();
// or animations (e.g. gif) resizing the result on the fly. This
// 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);
const std::string& error() const { return m_error; }

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// 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)
{
const FileAbstractImage* sprite = fop->abstractImage();
const FileAbstractImage* sprite = fop->abstractImageToSave();
// Open the file to write in binary mode
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
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
// time that it has in the sprite

View File

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

View File

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

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2022-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// 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 */
fgetc(f);
ImageRef image = fop->sequenceImage(bpp == 8 ?
IMAGE_INDEXED:
IMAGE_RGB,
width, height);
ImageRef image = fop->sequenceImageToLoad(
(bpp == 8 ? IMAGE_INDEXED:
IMAGE_RGB),
width, height);
if (!image) {
return false;
}
@ -192,7 +192,7 @@ bool PcxFormat::onLoad(FileOp* fop)
#ifdef ENABLE_SAVE
bool PcxFormat::onSave(FileOp* fop)
{
const FileAbstractImage* img = fop->abstractImage();
const FileAbstractImage* img = fop->abstractImageToSave();
const ImageSpec spec = img->spec();
int c, r, g, b;
int x, y;

View File

@ -278,7 +278,8 @@ bool PngFormat::onLoad(FileOp* fop)
int imageWidth = png_get_image_width(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)
return false;
@ -551,7 +552,7 @@ bool PngFormat::onSave(FileOp* fop)
png_init_io(png, fp);
const FileAbstractImage* img = fop->abstractImage();
const FileAbstractImage* img = fop->abstractImageToSave();
const ImageSpec spec = img->spec();
switch (spec.colorMode()) {

View File

@ -76,9 +76,10 @@ bool QoiFormat::onLoad(FileOp* fop)
if (!pixels)
return false;
ImageRef image = fop->sequenceImage(IMAGE_RGB,
desc.width,
desc.height);
ImageRef image = fop->sequenceImageToLoad(
IMAGE_RGB,
desc.width,
desc.height);
if (!image)
return false;
@ -136,7 +137,7 @@ bool QoiFormat::onLoad(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"));
FILE* f = handle.get();
doc::ImageRef image = img->getScaledImage();

View File

@ -1,5 +1,5 @@
// 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
// the End-User License Agreement for Aseprite.
@ -81,7 +81,7 @@ bool SvgFormat::onLoad(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;
const auto svg_options = std::static_pointer_cast<SvgOptions>(fop->formatOptions());
const int pixelScaleValue = std::clamp(svg_options->pixelScale, 0, 10000);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -165,9 +165,10 @@ bool TgaFormat::onLoad(FileOp* fop)
if (decoder.hasAlpha())
fop->sequenceSetHasAlpha(true);
ImageRef image = fop->sequenceImage((doc::PixelFormat)spec.colorMode(),
spec.width(),
spec.height());
ImageRef image = fop->sequenceImageToLoad(
(doc::PixelFormat)spec.colorMode(),
spec.width(),
spec.height());
if (!image)
return false;
@ -288,7 +289,7 @@ void prepare_header(tga::Header& header,
bool TgaFormat::onSave(FileOp* fop)
{
const FileAbstractImage* img = fop->abstractImage();
const FileAbstractImage* img = fop->abstractImageToSave();
const Palette* palette = fop->sequenceGetPalette();
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"));
FILE* fp = handle.get();
const FileAbstractImage* sprite = fop->abstractImage();
const FileAbstractImage* sprite = fop->abstractImageToSave();
const int w = sprite->width();
const int h = sprite->height();
@ -319,7 +319,7 @@ bool WebPFormat::onSave(FileOp* fop)
for (frame_t frame : fop->roi().selectedFrames()) {
// Render the frame in the bitmap
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()
// expects MODE_BGRA pictures.

View File

@ -178,7 +178,7 @@ cd $oldwd
if [[ "$(uname)" =~ "MINGW" ]] || [[ "$(uname)" =~ "MSYS" ]] ; then
# 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
d=$t/save-as-groups-and-hidden
$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
EOF
$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.
-- 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
-- compare totally transparent pixels (alpha=0) with black.
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)
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,
ColorMode.GRAYSCALE,
ColorMode.INDEXED } do
@ -59,44 +85,26 @@ for _,cm in ipairs{ ColorMode.RGB,
assert(spr.filename == "_test_b.png")
-- Scale
for _,fn in ipairs{ "_test_c_scaled.png",
"_test_c_scaled.gif",
for _,fn in ipairs{ "_test_c_scaled.bmp",
"_test_c_scaled.fli",
"_test_c_scaled.gif",
"_test_c_scaled.png",
"_test_c_scaled.tga",
"_test_c_scaled.bmp" } do
"_test_c_scaled.webp" } do
local fileExt = app.fs.fileExtension(fn)
-- TODO support saving any color mode to FLI files on the fly
if (fileExt ~= "fli" or cm == ColorMode.INDEXED) and
-- TODO Review grayscale support in bmp files
(fileExt ~= "bmp" or cm ~= ColorMode.GRAYSCALE) then
for _,scale in ipairs({ 1, 2, 3, 4 }) do
if compatible_modes(fileExt, cm) then
for _,scale in ipairs({ 0.25, 0.5, 1, 2, 3, 4 }) do
print(fn, scale, cm)
app.activeSprite = spr
app.sprite = spr
app.command.SaveFileCopyAs{ filename=fn, scale=scale }
local c = app.open(fn)
assert(c.width == spr.width*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)
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
c.cels[1].image:saveAs("_testA.png")
testImg:saveAs("_testB.png")
@ -110,26 +118,26 @@ for _,cm in ipairs{ ColorMode.RGB,
-- Scale + Slices
local slice = spr:newSlice(Rectangle(1, 2, 8, 15))
slice.name = "small_slice"
for _,fn in ipairs({ "_test_c_small_slice.png",
-- TODO slices aren't supported in gif/fli yet
--"_test_c_small_slice.gif",
--"_test_c_small_slice.fli",
for _,fn in ipairs({ "_test_c_small_slice.bmp",
"_test_c_small_slice.fli",
"_test_c_small_slice.gif",
"_test_c_small_slice.png",
"_test_c_small_slice.tga",
"_test_c_small_slice.bmp" }) do
"_test_c_small_slice.webp" }) do
local fileExt = app.fs.fileExtension(fn)
if (fileExt ~= "bmp" or cm ~= ColorMode.GRAYSCALE) then
for _,scale in ipairs({ 1, 2, 3, 4 }) do
if compatible_modes(fileExt, cm) then
for _,scale in ipairs({ 0.25, 0.5, 1, 2, 3, 4 }) do
print(fn, scale, cm)
app.activeSprite = spr
app.sprite = spr
app.command.SaveFileCopyAs{ filename=fn, slice="small_slice", scale=scale }
local c = app.open(fn)
assert(c.width == slice.bounds.width*scale)
assert(c.height == slice.bounds.height*scale)
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
c.cels[1].image:saveAs("_testA.png")
testImg:saveAs("_testB.png")

View File

@ -28,3 +28,6 @@
* `file-tests-props.aseprite`: Indexed, 64x64, 6 frames, 4 layers (one
of them is a tilemap), 13 cels, 1 tag.
* `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.