mirror of
https://github.com/aseprite/aseprite.git
synced 2025-01-26 21:35:44 +00:00
Fix exporting selection to gif/fli/webp files (fix #3827)
This commit is contained in:
parent
bd91a6430f
commit
35e64ad2f3
@ -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,
|
||||
|
@ -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(),
|
||||
|
@ -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();
|
||||
|
@ -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"));
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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()) {
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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"));
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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).
|
||||
|
BIN
tests/sprites/slices-moving.aseprite
Normal file
BIN
tests/sprites/slices-moving.aseprite
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user