Merge branch 'main' into beta

This commit is contained in:
David Capello 2022-06-15 13:44:34 -03:00
commit caf475b2dc
28 changed files with 605 additions and 259 deletions

View File

@ -1,4 +1,5 @@
<!-- Aseprite -->
<!-- Copyright (C) 2022 by Igara Studio S.A. -->
<!-- Copyright (C) 2016-2018 by David Capello -->
<gui>
<window id="export_file" text="@.title">
@ -8,19 +9,20 @@
<button id="output_filename_browse" text="..." style="mini_button" />
<label id="resize_label" text="@.resize" />
<combobox id="resize" cell_align="horizontal" cell_hspan="2">
<listitem text="25%" value="0.25" />
<listitem text="50%" value="0.5" />
<listitem text="100%" value="1" />
<listitem text="200%" value="2" />
<listitem text="300%" value="3" />
<listitem text="400%" value="4" />
<listitem text="500%" value="5" />
<listitem text="600%" value="6" />
<listitem text="700%" value="7" />
<listitem text="800%" value="8" />
<listitem text="900%" value="9" />
<listitem text="1000%" value="10" />
<combobox id="resize" editable="true" suffix="%"
cell_align="horizontal" cell_hspan="2">
<listitem text="25" />
<listitem text="50" />
<listitem text="100" />
<listitem text="200" />
<listitem text="300" />
<listitem text="400" />
<listitem text="500" />
<listitem text="600" />
<listitem text="700" />
<listitem text="800" />
<listitem text="900" />
<listitem text="1000" />
</combobox>
<label id="layers_label" text="@.layers" />

View File

@ -554,7 +554,7 @@ bool AppMenus::rebuildRecentList()
else {
std::unique_ptr<AppMenuItem> menuitem(
new AppMenuItem(
Strings::main_menu_file_no_recent_file(), nullptr));
Strings::main_menu_file_no_recent_file()));
menuitem->setIsRecentFileItem(true);
menuitem->setEnabled(false);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -86,24 +86,18 @@ private:
//////////////////////////////////////////////////////////////////////
SaveFileBaseCommand::SaveFileBaseCommand(const char* id, CommandFlags flags)
: Command(id, flags)
: CommandWithNewParams<SaveFileParams>(id, flags)
{
m_useUI = true;
m_ignoreEmpty = false;
}
void SaveFileBaseCommand::onLoadParams(const Params& params)
{
m_filename = params.get("filename");
m_filenameFormat = params.get("filename-format");
m_tag = params.get("frame-tag");
m_aniDir = params.get("ani-dir");
m_slice = params.get("slice");
CommandWithNewParams<SaveFileParams>::onLoadParams(params);
if (params.has_param("from-frame") ||
params.has_param("to-frame")) {
doc::frame_t fromFrame = params.get_as<doc::frame_t>("from-frame");
doc::frame_t toFrame = params.get_as<doc::frame_t>("to-frame");
if (this->params().fromFrame.isSet() ||
this->params().toFrame.isSet()) {
doc::frame_t fromFrame = this->params().fromFrame();
doc::frame_t toFrame = this->params().toFrame();
m_selFrames.insert(fromFrame, toFrame);
m_adjustFramesByTag = true;
}
@ -111,11 +105,6 @@ void SaveFileBaseCommand::onLoadParams(const Params& params)
m_selFrames.clear();
m_adjustFramesByTag = false;
}
std::string useUI = params.get("useUI");
m_useUI = (useUI.empty() || (useUI == "true"));
m_ignoreEmpty = params.get_as<bool>("ignoreEmpty");
}
// Returns true if there is a current sprite to save.
@ -129,8 +118,8 @@ std::string SaveFileBaseCommand::saveAsDialog(
Context* context,
const std::string& dlgTitle,
const std::string& initialFilename,
const bool markAsSaved,
const bool saveInBackground,
const MarkAsSaved markAsSaved,
const SaveInBackground saveInBackground,
const std::string& forbiddenFilename)
{
Doc* document = context->activeDocument();
@ -141,36 +130,38 @@ std::string SaveFileBaseCommand::saveAsDialog(
// preferences.
Preferences::instance().save();
std::string filename;
if (!m_filename.empty()) {
filename = m_filename;
}
else {
std::string filename = params().filename();
if (filename.empty() || params().ui()) {
base::paths exts = get_writable_extensions();
filename = initialFilename;
#ifdef ENABLE_UI
again:;
base::paths newfilename;
if (!m_useUI ||
!app::show_file_selector(
dlgTitle, filename, exts,
FileSelectorType::Save,
newfilename))
return std::string();
if (context->isUIAvailable()) {
again:;
base::paths newfilename;
if (!params().ui() ||
!app::show_file_selector(
dlgTitle, filename, exts,
FileSelectorType::Save,
newfilename)) {
return std::string();
}
filename = newfilename.front();
if (!forbiddenFilename.empty() &&
base::normalize_path(forbiddenFilename) ==
base::normalize_path(filename)) {
ui::Alert::show(Strings::alerts_cannot_file_overwrite_on_export());
goto again;
filename = newfilename.front();
if (!forbiddenFilename.empty() &&
base::normalize_path(forbiddenFilename) ==
base::normalize_path(filename)) {
ui::Alert::show(Strings::alerts_cannot_file_overwrite_on_export());
goto again;
}
}
#endif // ENABLE_UI
}
if (saveInBackground) {
if (filename.empty())
return std::string();
if (saveInBackground == SaveInBackground::On) {
saveDocumentInBackground(
context, document,
filename, markAsSaved);
@ -198,10 +189,12 @@ void SaveFileBaseCommand::saveDocumentInBackground(
const Context* context,
Doc* document,
const std::string& filename,
const bool markAsSaved)
const MarkAsSaved markAsSaved,
const ResizeOnTheFly resizeOnTheFly,
const gfx::PointF& scale)
{
if (!m_aniDir.empty()) {
switch (convert_string_to_anidir(m_aniDir)) {
if (params().aniDir.isSet()) {
switch (params().aniDir()) {
case AniDir::REVERSE:
m_selFrames = m_selFrames.makeReverse();
break;
@ -211,7 +204,7 @@ void SaveFileBaseCommand::saveDocumentInBackground(
}
}
FileOpROI roi(document, m_slice, m_tag,
FileOpROI roi(document, params().slice(), params().tag(),
m_selFrames, m_adjustFramesByTag);
std::unique_ptr<FileOp> fop(
@ -219,11 +212,14 @@ void SaveFileBaseCommand::saveDocumentInBackground(
context,
roi,
filename,
m_filenameFormat,
m_ignoreEmpty));
params().filenameFormat(),
params().ignoreEmpty()));
if (!fop)
return;
if (resizeOnTheFly == ResizeOnTheFly::On)
fop->setOnTheFlyScale(scale);
SaveFileJob job(fop.get());
job.showProgressWindow();
@ -239,17 +235,22 @@ void SaveFileBaseCommand::saveDocumentInBackground(
else if (fop->isStop()) {
document->impossibleToBackToSavedState();
}
else if (context->isUIAvailable()) {
App::instance()->recentFiles()->addRecentFile(filename);
if (markAsSaved) {
else {
if (context->isUIAvailable() && params().ui())
App::instance()->recentFiles()->addRecentFile(filename);
if (markAsSaved == MarkAsSaved::On) {
document->markAsSaved();
document->setFilename(filename);
document->incrementVersion();
}
#ifdef ENABLE_UI
StatusBar::instance()->setStatusText(
2000, fmt::format("File <{}> saved.",
base::get_file_name(filename)));
if (context->isUIAvailable() && params().ui()) {
StatusBar::instance()->setStatusText(
2000, fmt::format("File <{}> saved.",
base::get_file_name(filename)));
}
#endif
}
}
@ -283,14 +284,18 @@ void SaveFileCommand::onExecute(Context* context)
saveDocumentInBackground(
context, document,
documentReader->filename(), true);
(params().filename.isSet() ? params().filename():
documentReader->filename()),
MarkAsSaved::On);
}
// If the document isn't associated to a file, we must to show the
// save-as dialog to the user to select for first time the file-name
// for this document.
else {
saveAsDialog(context, "Save File",
document->filename(), true);
(params().filename.isSet() ? params().filename():
document->filename()),
MarkAsSaved::On);
}
}
@ -311,7 +316,9 @@ void SaveFileAsCommand::onExecute(Context* context)
{
Doc* document = context->activeDocument();
saveAsDialog(context, "Save As",
document->filename(), true);
(params().filename.isSet() ? params().filename():
document->filename()),
MarkAsSaved::On);
}
class SaveFileCopyAsCommand : public SaveFileBaseCommand {
@ -334,17 +341,16 @@ SaveFileCopyAsCommand::SaveFileCopyAsCommand()
void SaveFileCopyAsCommand::onExecute(Context* context)
{
Doc* doc = context->activeDocument();
std::string outputFilename = m_filename;
std::string outputFilename = params().filename();
std::string layers = kAllLayers;
std::string frames = kAllFrames;
double xscale = 1.0;
double yscale = 1.0;
bool applyPixelRatio = false;
doc::AniDir aniDirValue = convert_string_to_anidir(m_aniDir);
double scale = params().scale();
doc::AniDir aniDirValue = params().aniDir();
bool isForTwitter = false;
#if ENABLE_UI
if (m_useUI && context->isUIAvailable()) {
if (params().ui() && context->isUIAvailable()) {
ExportFileWindow win(doc);
bool askOverwrite = true;
@ -353,7 +359,9 @@ void SaveFileCopyAsCommand::onExecute(Context* context)
std::string result =
saveAsDialog(
context, "Export",
win.outputFilenameValue(), false, false,
win.outputFilenameValue(),
MarkAsSaved::Off,
SaveInBackground::Off,
(doc->isAssociatedToFile() ? doc->filename():
std::string()));
if (!result.empty())
@ -362,6 +370,18 @@ void SaveFileCopyAsCommand::onExecute(Context* context)
return result;
});
if (params().filename.isSet()) {
std::string outputPath = base::get_file_path(outputFilename);
if (outputPath.empty()) {
outputPath = base::get_file_path(doc->filename());
outputFilename = base::join_path(outputPath, outputFilename);
}
win.setOutputFilename(outputFilename);
}
if (params().scale.isSet()) win.setResizeScale(scale);
if (params().aniDir.isSet()) win.setAniDir(aniDirValue);
win.remapWindow();
load_window_pos(&win, "ExportFile");
again:;
@ -389,31 +409,36 @@ void SaveFileCopyAsCommand::onExecute(Context* context)
layers = win.layersValue();
frames = win.framesValue();
xscale = yscale = win.resizeValue();
scale = win.resizeValue();
applyPixelRatio = win.applyPixelRatio();
aniDirValue = win.aniDirValue();
isForTwitter = win.isForTwitter();
}
#endif
gfx::PointF scaleXY(scale, scale);
// Pixel ratio
if (applyPixelRatio) {
doc::PixelRatio pr = doc->sprite()->pixelRatio();
xscale *= pr.w;
yscale *= pr.h;
scaleXY.x *= pr.w;
scaleXY.y *= pr.h;
}
// Apply scale
// First of all we'll try to use the "on the fly" scaling, to avoid
// using a resize command to apply the export scale.
const undo::UndoState* undoState = nullptr;
bool undoResize = false;
if (xscale != 1.0 || yscale != 1.0) {
const bool resizeOnTheFly = FileOp::checkIfFormatSupportResizeOnTheFly(outputFilename);
if (!resizeOnTheFly && (scaleXY.x != 1.0 ||
scaleXY.y != 1.0)) {
Command* resizeCmd = Commands::instance()->byId(CommandId::SpriteSize());
ASSERT(resizeCmd);
if (resizeCmd) {
int width = doc->sprite()->width();
int height = doc->sprite()->height();
int newWidth = int(double(width) * xscale);
int newHeight = int(double(height) * yscale);
int newWidth = int(double(width) * scaleXY.x);
int newHeight = int(double(height) * scaleXY.y);
if (newWidth < 1) newWidth = 1;
if (newHeight < 1) newHeight = 1;
if (width != newWidth || height != newHeight) {
@ -441,27 +466,33 @@ void SaveFileCopyAsCommand::onExecute(Context* context)
layers,
layersVisibility);
// Selected frames to export
SelectedFrames selFrames;
Tag* tag = calculate_selected_frames(
site, frames, selFrames);
if (tag)
m_tag = tag->name();
m_selFrames = selFrames;
// m_selFrames is not empty if fromFrame/toFrame parameters are
// specified.
if (m_selFrames.empty()) {
// Selected frames to export
SelectedFrames selFrames;
Tag* tag = calculate_selected_frames(
site, frames, selFrames);
if (tag)
params().tag(tag->name());
m_selFrames = selFrames;
}
m_adjustFramesByTag = false;
}
base::ScopedValue<std::string> restoreAniDir(
m_aniDir,
convert_anidir_to_string(aniDirValue), // New value
m_aniDir); // Restore old value
// Set ani dir
params().aniDir(aniDirValue);
// TODO This should be set as options for the specific encoder
GifEncoderDurationFix fixGif(isForTwitter);
PngEncoderOneAlphaPixel fixPng(isForTwitter);
saveDocumentInBackground(
context, doc, outputFilename, false);
context, doc, outputFilename,
MarkAsSaved::Off,
(resizeOnTheFly ? ResizeOnTheFly::On:
ResizeOnTheFly::Off),
scaleXY);
}
// Undo resize

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2021-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -9,15 +10,35 @@
#pragma once
#include "app/commands/command.h"
#include "app/commands/new_params.h"
#include "doc/anidir.h"
#include "doc/selected_frames.h"
#include "gfx/point.h"
#include <string>
namespace app {
class Doc;
class SaveFileBaseCommand : public Command {
struct SaveFileParams : public NewParams {
Param<bool> ui { this, true, { "ui", "useUI" } };
Param<std::string> filename { this, std::string(), "filename" };
Param<std::string> filenameFormat { this, std::string(), { "filenameFormat", "filename-format" } };
Param<std::string> tag { this, std::string(), { "tag", "frame-tag" } };
Param<doc::AniDir> aniDir { this, doc::AniDir::FORWARD, { "aniDir", "ani-dir" } };
Param<std::string> slice { this, std::string(), "slice" };
Param<doc::frame_t> fromFrame { this, 0, { "fromFrame", "from-frame" } };
Param<doc::frame_t> toFrame { this, 0, { "toFrame", "to-frame" } };
Param<bool> ignoreEmpty { this, false, "ignoreEmpty" };
Param<double> scale { this, 1.0, "scale" };
};
class SaveFileBaseCommand : public CommandWithNewParams<SaveFileParams> {
public:
enum class MarkAsSaved { Off, On };
enum class SaveInBackground { Off, On };
enum class ResizeOnTheFly { Off, On };
SaveFileBaseCommand(const char* id, CommandFlags flags);
protected:
@ -28,24 +49,19 @@ namespace app {
Context* context,
const std::string& dlgTitle,
const std::string& filename,
const bool markAsSaved,
const bool saveInBackground = true,
const MarkAsSaved markAsSaved,
const SaveInBackground saveInBackground = SaveInBackground::On,
const std::string& forbiddenFilename = std::string());
void saveDocumentInBackground(
const Context* context,
Doc* document,
const std::string& filename,
const bool markAsSaved);
const MarkAsSaved markAsSaved,
const ResizeOnTheFly resizeOnTheFly = ResizeOnTheFly::Off,
const gfx::PointF& scale = gfx::PointF(1.0, 1.0));
std::string m_filename;
std::string m_filenameFormat;
std::string m_tag;
std::string m_aniDir;
std::string m_slice;
doc::SelectedFrames m_selFrames;
bool m_adjustFramesByTag;
bool m_useUI;
bool m_ignoreEmpty;
};
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -18,6 +18,7 @@
#include "base/split_string.h"
#include "base/string.h"
#include "doc/algorithm/resize_image.h"
#include "doc/anidir.h"
#include "doc/color_mode.h"
#include "doc/rgbmap_algorithm.h"
#include "filters/color_curve.h"
@ -145,6 +146,12 @@ void Param<doc::ColorMode>::fromString(const std::string& value)
setValue(doc::ColorMode::RGB);
}
template<>
void Param<doc::AniDir>::fromString(const std::string& value)
{
setValue(convert_string_to_anidir(value));
}
template<>
void Param<app::Color>::fromString(const std::string& value)
{
@ -315,6 +322,15 @@ void Param<doc::ColorMode>::fromLua(lua_State* L, int index)
setValue((doc::ColorMode)lua_tointeger(L, index));
}
template<>
void Param<doc::AniDir>::fromLua(lua_State* L, int index)
{
if (lua_type(L, index) == LUA_TSTRING)
fromString(lua_tostring(L, index));
else
setValue((doc::AniDir)lua_tointeger(L, index));
}
template<>
void Param<app::Color>::fromLua(lua_State* L, int index)
{

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -67,7 +67,8 @@ class BmpFormat : public FileFormat {
FILE_SUPPORT_RGB |
FILE_SUPPORT_GRAY |
FILE_SUPPORT_INDEXED |
FILE_SUPPORT_SEQUENCES;
FILE_SUPPORT_SEQUENCES |
FILE_ENCODE_ABSTRACT_IMAGE;
}
bool onLoad(FileOp* fop) override;
@ -688,34 +689,34 @@ bool BmpFormat::onLoad(FileOp *fop)
else
rmask = gmask = bmask = 0;
Image* image = fop->sequenceImage(pixelFormat,
infoheader.biWidth,
ABS((int)infoheader.biHeight));
ImageRef image = fop->sequenceImage(pixelFormat,
infoheader.biWidth,
ABS((int)infoheader.biHeight));
if (!image) {
return false;
}
if (pixelFormat == IMAGE_RGB)
clear_image(image, rgba(0, 0, 0, 255));
clear_image(image.get(), rgba(0, 0, 0, 255));
else
clear_image(image, 0);
clear_image(image.get(), 0);
switch (infoheader.biCompression) {
case BI_RGB:
read_image(f, image, &infoheader, fop);
read_image(f, image.get(), &infoheader, fop);
break;
case BI_RLE8:
read_rle8_compressed_image(f, image, &infoheader);
read_rle8_compressed_image(f, image.get(), &infoheader);
break;
case BI_RLE4:
read_rle4_compressed_image(f, image, &infoheader);
read_rle4_compressed_image(f, image.get(), &infoheader);
break;
case BI_BITFIELDS:
if (read_bitfields_image(f, image, &infoheader, rmask, gmask, bmask) < 0) {
if (read_bitfields_image(f, image.get(), &infoheader, rmask, gmask, bmask) < 0) {
fop->setError("Unsupported bitfields in the BMP file.\n");
return false;
}
@ -751,21 +752,22 @@ bool BmpFormat::onLoad(FileOp *fop)
#ifdef ENABLE_SAVE
bool BmpFormat::onSave(FileOp *fop)
{
const Image* image = fop->sequenceImage();
const int w = image->width();
const int h = image->height();
const FileAbstractImage* img = fop->abstractImage();
const ImageSpec spec = img->spec();
const int w = spec.width();
const int h = spec.height();
int bfSize;
int biSizeImage;
int ncolors = fop->sequenceGetNColors();
int bpp = 0;
switch (image->pixelFormat()) {
case IMAGE_RGB:
switch (spec.colorMode()) {
case ColorMode::RGB:
bpp = 24;
break;
case IMAGE_GRAYSCALE:
case ColorMode::GRAYSCALE:
bpp = 8;
break;
case IMAGE_INDEXED: {
case ColorMode::INDEXED: {
if (ncolors > 16)
bpp = 8;
else if (ncolors > 2)
@ -776,7 +778,7 @@ bool BmpFormat::onSave(FileOp *fop)
break;
}
default:
// TODO save IMAGE_BITMAP as 1bpp bmp?
// TODO save ColorMode::BITMAP as 1bpp bmp?
// Invalid image format
fop->setError("Unsupported color mode.\n");
return false;
@ -851,31 +853,37 @@ bool BmpFormat::onSave(FileOp *fop)
// Save image pixels (from bottom to top)
for (i=h-1; i>=0; i--) {
switch (image->pixelFormat()) {
case IMAGE_RGB:
switch (spec.colorMode()) {
case ColorMode::RGB: {
auto scanline = (const uint32_t*)img->getScanline(i);
for (j=0; j<w; ++j) {
c = get_pixel_fast<RgbTraits>(image, j, i);
c = scanline[j];
fputc(rgba_getb(c), f);
fputc(rgba_getg(c), f);
fputc(rgba_getr(c), f);
}
break;
case IMAGE_GRAYSCALE:
}
case ColorMode::GRAYSCALE: {
auto scanline = (const uint16_t*)img->getScanline(i);
for (j=0; j<w; ++j) {
c = get_pixel_fast<GrayscaleTraits>(image, j, i);
c = scanline[j];
fputc(graya_getv(c), f);
}
break;
case IMAGE_INDEXED:
}
case ColorMode::INDEXED: {
auto scanline = (const uint8_t*)img->getScanline(i);
for (j=0; j<w; ) {
uint8_t value = 0;
for (int k=colorsPerByte-1; k>=0 && j<w; --k, ++j) {
c = get_pixel_fast<IndexedTraits>(image, j, i);
c = scanline[j];
value |= (c & colorMask) << (bpp*k);
}
fputc(value, f);
}
break;
}
}
for (j=0; j<filler; j++)

View File

@ -88,7 +88,7 @@ bool CssFormat::onLoad(FileOp* fop)
bool CssFormat::onSave(FileOp* fop)
{
const Image* image = fop->sequenceImage();
const ImageRef image = fop->sequenceImage();
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"));
@ -160,7 +160,7 @@ bool CssFormat::onSave(FileOp* fop)
case IMAGE_RGB: {
for (y=0; y<image->height(); y++) {
for (x=0; x<image->width(); x++) {
c = get_pixel_fast<RgbTraits>(image, x, y);
c = get_pixel_fast<RgbTraits>(image.get(), x, y);
alpha = rgba_geta(c);
if (alpha != 0x00) {
print_shadow_color(x, y, rgba_getr(c), rgba_getg(c), rgba_getb(c),
@ -175,7 +175,7 @@ bool CssFormat::onSave(FileOp* fop)
case IMAGE_GRAYSCALE: {
for (y=0; y<image->height(); y++) {
for (x=0; x<image->width(); x++) {
c = get_pixel_fast<GrayscaleTraits>(image, x, y);
c = get_pixel_fast<GrayscaleTraits>(image.get(), x, y);
auto v = graya_getv(c);
alpha = graya_geta(c);
if (alpha != 0x00) {
@ -204,7 +204,7 @@ bool CssFormat::onSave(FileOp* fop)
}
for (y=0; y<image->height(); y++) {
for (x=0; x<image->width(); x++) {
c = get_pixel_fast<IndexedTraits>(image, x, y);
c = get_pixel_fast<IndexedTraits>(image.get(), x, y);
if (c != mask_color) {
if (css_options->withVars) {
print_shadow_index(x, y, c, num_printed_pixels>0);

View File

@ -34,6 +34,7 @@
#include "base/scoped_lock.h"
#include "base/string.h"
#include "dio/detect_format.h"
#include "doc/algorithm/resize_image.h"
#include "doc/doc.h"
#include "fmt/format.h"
#include "render/quantization.h"
@ -53,6 +54,130 @@ namespace app {
using namespace base;
class FileOp::FileAbstractImageImpl : public FileAbstractImage {
public:
FileAbstractImageImpl(FileOp* fop)
: m_doc(fop->document())
, m_sprite(m_doc->sprite())
, m_spec(m_sprite->spec())
, m_newBlend(fop->newBlend()) {
ASSERT(m_doc && m_sprite);
}
void setSliceBounds(const gfx::Rect& sliceBounds) {
m_spec.setWidth(sliceBounds.w * m_scale.x);
m_spec.setHeight(sliceBounds.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()) {
m_tmpScaledImage = image;
}
else {
if (!m_tmpScaledImage)
m_tmpScaledImage.reset(doc::Image::create(m_spec));
doc::algorithm::resize_image(
image.get(),
m_tmpScaledImage.get(),
doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR,
palette(frame),
m_sprite->rgbMap(frame),
image->maskColor());
}
}
// FileAbstractImage impl
doc::ImageSpec spec() const override {
return m_spec;
}
os::ColorSpaceRef osColorSpace() const override {
return m_doc->osColorSpace();
}
bool needAlpha() const override {
return m_sprite->needAlpha();
}
bool isOpaque() const override {
return m_sprite->isOpaque();
}
int frames() const override {
return m_sprite->totalFrames();
}
int frameDuration(doc::frame_t frame) const override {
return m_sprite->frameDuration(frame);
}
const doc::Palette* palette(doc::frame_t frame) const override {
ASSERT(m_sprite);
return m_sprite->palette(frame);
}
doc::PalettesList palettes() const override {
ASSERT(m_sprite);
return m_sprite->getPalettes();
}
const doc::ImageRef getScaledImage() const override {
return m_tmpScaledImage;
}
const uint8_t* getScanline(int y) const override {
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());
if (needResize && !m_tmpUnscaledRender) {
auto spec = m_sprite->spec();
spec.setColorMode(dst->colorMode());
m_tmpUnscaledRender.reset(doc::Image::create(spec));
}
render::Render render;
render.setNewBlend(m_newBlend);
render.setBgType(render::BgType::NONE);
render.renderSprite(
(needResize ? m_tmpUnscaledRender.get(): dst),
m_sprite, frame);
if (needResize) {
doc::algorithm::resize_image(
m_tmpUnscaledRender.get(),
dst,
doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR,
palette(frame),
m_sprite->rgbMap(frame),
m_tmpUnscaledRender->maskColor());
}
}
void setScale(const gfx::PointF& scale) {
m_scale = scale;
m_spec.setWidth(m_spec.width() * m_scale.x);
m_spec.setHeight(m_spec.height() * m_scale.y);
}
private:
const Doc* m_doc;
const doc::Sprite* m_sprite;
doc::ImageSpec m_spec;
bool m_newBlend;
doc::ImageRef m_tmpScaledImage = nullptr;
mutable doc::ImageRef m_tmpUnscaledRender = nullptr;
gfx::PointF m_scale = gfx::PointF(1.0, 1.0);
};
base::paths get_readable_extensions()
{
base::paths paths;
@ -586,6 +711,18 @@ FileOp* FileOp::createSaveDocumentOperation(const Context* context,
return fop.release();
}
// static
bool FileOp::checkIfFormatSupportResizeOnTheFly(const std::string& filename)
{
// Get the format through the extension of the filename
FileFormat* fileFormat =
FileFormatsManager::instance()->getFileFormat(
dio::detect_format_by_file_extension(filename));
return (fileFormat &&
fileFormat->support(FILE_ENCODE_ABSTRACT_IMAGE));
}
// Executes the file operation: loads or saves the sprite.
//
// It can be called from a different thread of the one used
@ -775,6 +912,9 @@ void FileOp::operate(IFileOpProgress* progress)
if (!key || key->isEmpty())
continue; // Skip frame because there is no slice key
if (m_abstractImage)
m_abstractImage->setSliceBounds(key->bounds());
m_seq.image.reset(
Image::create(sprite->pixelFormat(),
key->bounds().w,
@ -1082,7 +1222,7 @@ void FileOp::sequenceGetAlpha(int index, int* a) const
*a = 0;
}
Image* FileOp::sequenceImage(PixelFormat pixelFormat, int w, int h)
ImageRef FileOp::sequenceImage(PixelFormat pixelFormat, int w, int h)
{
Sprite* sprite;
@ -1120,7 +1260,33 @@ Image* FileOp::sequenceImage(PixelFormat pixelFormat, int w, int h)
m_seq.image.reset(Image::create(pixelFormat, w, h));
m_seq.last_cel = new Cel(m_seq.frame++, ImageRef(nullptr));
return m_seq.image.get();
return m_seq.image;
}
void FileOp::makeAbstractImage()
{
ASSERT(m_format->support(FILE_ENCODE_ABSTRACT_IMAGE));
if (!m_abstractImage)
m_abstractImage = std::make_unique<FileAbstractImageImpl>(this);
}
FileAbstractImage* FileOp::abstractImage()
{
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());
return m_abstractImage.get();
}
void FileOp::setOnTheFlyScale(const gfx::PointF& scale)
{
makeAbstractImage();
m_abstractImage->setScale(scale);
}
void FileOp::setError(const char *format, ...)

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2021 Igara Studio S.A.
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -20,6 +20,7 @@
#include "doc/image_ref.h"
#include "doc/pixel_format.h"
#include "doc/selected_frames.h"
#include "os/color_space.h"
#include <cstdio>
#include <memory>
@ -94,6 +95,33 @@ namespace app {
doc::SelectedFrames m_selFrames;
};
// Used by file formats with FILE_ENCODE_ABSTRACT_IMAGE flag, to
// encode a sprite with an intermediate transformation on-the-fly
// (e.g. resizing).
class FileAbstractImage {
public:
virtual ~FileAbstractImage() { }
virtual doc::ImageSpec spec() const = 0;
virtual os::ColorSpaceRef osColorSpace() const = 0;
virtual bool needAlpha() const = 0;
virtual bool isOpaque() const = 0;
virtual int frames() const = 0;
virtual int frameDuration(doc::frame_t frame) const = 0;
virtual const doc::Palette* palette(doc::frame_t frame) const = 0;
virtual doc::PalettesList palettes() const = 0;
virtual const doc::ImageRef getScaledImage() const = 0;
// In case the file format can encode scanline by scanline
// (e.g. PNG format).
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;
};
// Structure to load & save files.
//
// TODO This class do to many things. There should be a previous
@ -113,6 +141,8 @@ namespace app {
const std::string& filenameFormat,
const bool ignoreEmptyFrames);
static bool checkIfFormatSupportResizeOnTheFly(const std::string& filename);
~FileOp();
bool isSequence() const { return !m_seq.filename_list.empty(); }
@ -189,8 +219,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;
Image* sequenceImage(PixelFormat pixelFormat, int w, int h);
const Image* sequenceImage() const { return m_seq.image.get(); }
ImageRef sequenceImage(PixelFormat pixelFormat, int w, int h);
const ImageRef sequenceImage() const { return m_seq.image; }
const Palette* sequenceGetPalette() const { return m_seq.palette; }
bool sequenceGetHasAlpha() const {
return m_seq.has_alpha;
@ -202,6 +232,11 @@ namespace app {
return m_seq.flags;
}
// 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();
void setOnTheFlyScale(const gfx::PointF& scale);
const std::string& error() const { return m_error; }
void setError(const char *error, ...);
bool hasError() const { return !m_error.empty(); }
@ -276,7 +311,11 @@ namespace app {
int flags;
} m_seq;
class FileAbstractImageImpl;
std::unique_ptr<FileAbstractImageImpl> m_abstractImage;
void prepareForSequence();
void makeAbstractImage();
};
// Available extensions for each load/save operation.

View File

@ -30,6 +30,7 @@
#define FILE_SUPPORT_TAGS 0x00001000
#define FILE_SUPPORT_BIG_PALETTES 0x00002000 // Palettes w/more than 256 colors
#define FILE_SUPPORT_PALETTE_WITH_ALPHA 0x00004000
#define FILE_ENCODE_ABSTRACT_IMAGE 0x00008000 // Use the new FileAbstractImage
namespace app {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2021 Igara Studio S.A.
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -93,7 +93,8 @@ class GifFormat : public FileFormat {
FILE_SUPPORT_INDEXED |
FILE_SUPPORT_FRAMES |
FILE_SUPPORT_PALETTES |
FILE_SUPPORT_GET_FORMAT_OPTIONS;
FILE_SUPPORT_GET_FORMAT_OPTIONS |
FILE_ENCODE_ABSTRACT_IMAGE;
}
bool onLoad(FileOp* fop) override;
@ -954,10 +955,11 @@ public:
GifEncoder(FileOp* fop, GifFileType* gifFile)
: m_fop(fop)
, m_gifFile(gifFile)
, m_document(fop->document())
, m_sprite(fop->document()->sprite())
, m_spriteBounds(m_sprite->bounds())
, m_hasBackground(m_sprite->isOpaque())
, m_img(fop->abstractImage())
, m_spec(m_img->spec())
, m_spriteBounds(m_spec.bounds())
, m_hasBackground(m_img->isOpaque())
, m_bitsPerPixel(1)
, m_globalColormap(nullptr)
, m_globalColormapPalette(*m_sprite->palette(0))
@ -973,7 +975,7 @@ public:
m_lastFrameBounds = m_spriteBounds;
m_lastDisposal = DisposalMethod::NONE;
if (m_sprite->pixelFormat() == IMAGE_INDEXED) {
if (m_spec.colorMode() == ColorMode::INDEXED) {
for (Palette* palette : m_sprite->getPalettes()) {
int bpp = GifBitSizeLimited(palette->size());
m_bitsPerPixel = std::max(m_bitsPerPixel, bpp);
@ -983,8 +985,8 @@ public:
m_bitsPerPixel = 8;
}
if (m_sprite->pixelFormat() == IMAGE_INDEXED &&
m_sprite->getPalettes().size() == 1) {
if (m_spec.colorMode() == ColorMode::INDEXED &&
m_img->palettes().size() == 1) {
// If some layer has opacity < 255 or a different blend mode, we
// need to create color palettes.
bool quantizeColormaps = false;
@ -1001,7 +1003,7 @@ public:
if (!quantizeColormaps) {
m_globalColormap = createColorMap(&m_globalColormapPalette);
m_bgIndex = m_sprite->transparentColor();
m_bgIndex = m_spec.maskColor();
// For indexed and opaque sprite, we can preserve the exact
// palette order without lossing compression rate.
if (m_hasBackground)
@ -1025,7 +1027,7 @@ public:
m_transparentIndex = (m_hasBackground ? -1: m_bgIndex);
if (m_globalColormap) {
// The variable m_globalColormap is != nullptr only on indexed images
ASSERT(m_sprite->pixelFormat() == IMAGE_INDEXED);
ASSERT(m_spec.colorMode() == ColorMode::INDEXED);
const Palette* pal = m_sprite->palette(0);
bool maskColorFounded = false;
@ -1331,7 +1333,7 @@ private:
const DisposalMethod disposalMethod,
const bool fixDuration) {
unsigned char extension_bytes[5];
int frameDelay = m_sprite->frameDuration(frame) / 10;
int frameDelay = m_img->frameDuration(frame) / 10;
// Fix duration for Twitter. It looks like the last frame must be
// 1/4 of its duration for some strange reason in the Twitter
@ -1550,15 +1552,11 @@ private:
}
void renderFrame(frame_t frame, Image* dst) {
render::Render render;
render.setNewBlend(m_fop->newBlend());
render.setBgType(render::BgType::NONE);
if (m_preservePaletteOrder)
clear_image(dst, m_bgIndex);
else
clear_image(dst, 0);
render.renderSprite(dst, m_sprite, frame);
m_img->renderFrame(frame, dst);
}
private:
@ -1568,8 +1566,7 @@ private:
ColorMapObject* colormap = GifMakeMapObject(n, nullptr);
// Color space conversions
ConvertCS convert = convert_from_custom_to_srgb(
m_document->osColorSpace());
ConvertCS convert = convert_from_custom_to_srgb(m_img->osColorSpace());
for (int i=0; i<n; ++i) {
color_t color;
@ -1590,8 +1587,9 @@ private:
FileOp* m_fop;
GifFileType* m_gifFile;
const Doc* m_document;
const Sprite* m_sprite;
const FileAbstractImage* m_img;
const ImageSpec m_spec;
gfx::Rect m_spriteBounds;
bool m_hasBackground;
int m_bgIndex;

View File

@ -64,7 +64,8 @@ class JpegFormat : public FileFormat {
FILE_SUPPORT_RGB |
FILE_SUPPORT_GRAY |
FILE_SUPPORT_SEQUENCES |
FILE_SUPPORT_GET_FORMAT_OPTIONS;
FILE_SUPPORT_GET_FORMAT_OPTIONS |
FILE_ENCODE_ABSTRACT_IMAGE;
}
bool onLoad(FileOp* fop) override;
@ -177,7 +178,7 @@ bool JpegFormat::onLoad(FileOp* fop)
jpeg_start_decompress(&dinfo);
// Create the image.
Image* image = fop->sequenceImage(
ImageRef image = fop->sequenceImage(
(dinfo.out_color_space == JCS_RGB ? IMAGE_RGB:
IMAGE_GRAYSCALE),
dinfo.output_width,
@ -352,7 +353,8 @@ bool JpegFormat::onSave(FileOp* fop)
{
struct jpeg_compress_struct cinfo;
struct error_mgr jerr;
const Image* image = fop->sequenceImage();
const FileAbstractImage* img = fop->abstractImage();
const ImageSpec spec = img->spec();
JSAMPARRAY buffer;
JDIMENSION buffer_height;
const auto jpeg_options = std::static_pointer_cast<JpegOptions>(fop->formatOptions());
@ -377,10 +379,10 @@ bool JpegFormat::onSave(FileOp* fop)
jpeg_stdio_dest(&cinfo, file);
// SET parameters for compression.
cinfo.image_width = image->width();
cinfo.image_height = image->height();
cinfo.image_width = spec.width();
cinfo.image_height = spec.height();
if (image->pixelFormat() == IMAGE_GRAYSCALE) {
if (spec.colorMode() == ColorMode::GRAYSCALE) {
cinfo.input_components = 1;
cinfo.in_color_space = JCS_GRAYSCALE;
}
@ -427,15 +429,15 @@ bool JpegFormat::onSave(FileOp* fop)
// Write each scan line.
while (cinfo.next_scanline < cinfo.image_height) {
// RGB
if (image->pixelFormat() == IMAGE_RGB) {
if (spec.colorMode() == ColorMode::RGB) {
uint32_t* src_address;
uint8_t* dst_address;
int x, y;
for (y=0; y<(int)buffer_height; y++) {
src_address = (uint32_t*)image->getPixelAddress(0, cinfo.next_scanline+y);
src_address = (uint32_t*)img->getScanline(cinfo.next_scanline+y);
dst_address = ((uint8_t**)buffer)[y];
for (x=0; x<image->width(); ++x) {
for (x=0; x<spec.width(); ++x) {
c = *(src_address++);
*(dst_address++) = rgba_getr(c);
*(dst_address++) = rgba_getg(c);
@ -449,9 +451,9 @@ bool JpegFormat::onSave(FileOp* fop)
uint8_t* dst_address;
int x, y;
for (y=0; y<(int)buffer_height; y++) {
src_address = (uint16_t*)image->getPixelAddress(0, cinfo.next_scanline+y);
src_address = (uint16_t*)img->getScanline(cinfo.next_scanline+y);
dst_address = ((uint8_t**)buffer)[y];
for (x=0; x<image->width(); ++x)
for (x=0; x<spec.width(); ++x)
*(dst_address++) = graya_getv(*(src_address++));
}
}

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -43,7 +44,8 @@ class PcxFormat : public FileFormat {
FILE_SUPPORT_RGB |
FILE_SUPPORT_GRAY |
FILE_SUPPORT_INDEXED |
FILE_SUPPORT_SEQUENCES;
FILE_SUPPORT_SEQUENCES |
FILE_ENCODE_ABSTRACT_IMAGE;
}
bool onLoad(FileOp* fop) override;
@ -104,16 +106,16 @@ bool PcxFormat::onLoad(FileOp* fop)
for (c=0; c<60; c++) /* skip some more junk */
fgetc(f);
Image* image = fop->sequenceImage(bpp == 8 ?
IMAGE_INDEXED:
IMAGE_RGB,
width, height);
ImageRef image = fop->sequenceImage(bpp == 8 ?
IMAGE_INDEXED:
IMAGE_RGB,
width, height);
if (!image) {
return false;
}
if (bpp == 24)
clear_image(image, rgba(0, 0, 0, 255));
clear_image(image.get(), rgba(0, 0, 0, 255));
for (y=0; y<height; y++) { /* read RLE encoded PCX data */
x = xx = 0;
@ -131,7 +133,7 @@ bool PcxFormat::onLoad(FileOp* fop)
if (bpp == 8) {
while (c--) {
if (x < image->width())
put_pixel_fast<IndexedTraits>(image, x, y, ch);
put_pixel_fast<IndexedTraits>(image.get(), x, y, ch);
x++;
}
@ -139,8 +141,8 @@ bool PcxFormat::onLoad(FileOp* fop)
else {
while (c--) {
if (xx < image->width())
put_pixel_fast<RgbTraits>(image, xx, y,
get_pixel_fast<RgbTraits>(image, xx, y) | ((ch & 0xff) << po));
put_pixel_fast<RgbTraits>(image.get(), xx, y,
get_pixel_fast<RgbTraits>(image.get(), xx, y) | ((ch & 0xff) << po));
x++;
if (x == bytes_per_line) {
@ -190,7 +192,8 @@ bool PcxFormat::onLoad(FileOp* fop)
#ifdef ENABLE_SAVE
bool PcxFormat::onSave(FileOp* fop)
{
const Image* image = fop->sequenceImage();
const FileAbstractImage* img = fop->abstractImage();
const ImageSpec spec = img->spec();
int c, r, g, b;
int x, y;
int runcount;
@ -201,7 +204,7 @@ bool PcxFormat::onSave(FileOp* fop)
FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb"));
FILE* f = handle.get();
if (image->pixelFormat() == IMAGE_RGB) {
if (spec.colorMode() == ColorMode::RGB) {
depth = 24;
planes = 3;
}
@ -216,8 +219,8 @@ bool PcxFormat::onSave(FileOp* fop)
fputc(8, f); /* 8 bits per pixel */
fputw(0, f); /* xmin */
fputw(0, f); /* ymin */
fputw(image->width()-1, f); /* xmax */
fputw(image->height()-1, f); /* ymax */
fputw(spec.width()-1, f); /* xmax */
fputw(spec.height()-1, f); /* ymax */
fputw(320, f); /* HDpi */
fputw(200, f); /* VDpi */
@ -230,36 +233,39 @@ bool PcxFormat::onSave(FileOp* fop)
fputc(0, f); /* reserved */
fputc(planes, f); /* one or three color planes */
fputw(image->width(), f); /* number of bytes per scanline */
fputw(spec.width(), f); /* number of bytes per scanline */
fputw(1, f); /* color palette */
fputw(image->width(), f); /* hscreen size */
fputw(image->height(), f); /* vscreen size */
fputw(spec.width(), f); /* hscreen size */
fputw(spec.height(), f); /* vscreen size */
for (c=0; c<54; c++) /* filler */
fputc(0, f);
for (y=0; y<image->height(); y++) { /* for each scanline... */
for (y=0; y<spec.height(); y++) { /* for each scanline... */
runcount = 0;
runchar = 0;
for (x=0; x<image->width()*planes; x++) { /* for each pixel... */
const uint8_t* scanline = img->getScanline(y);
for (x=0; x<spec.width()*planes; x++) { /* for each pixel... */
if (depth == 8) {
if (image->pixelFormat() == IMAGE_INDEXED)
ch = get_pixel_fast<IndexedTraits>(image, x, y);
else if (image->pixelFormat() == IMAGE_GRAYSCALE) {
c = get_pixel_fast<GrayscaleTraits>(image, x, y);
if (spec.colorMode() == ColorMode::INDEXED)
ch = scanline[x];
else if (spec.colorMode() == ColorMode::GRAYSCALE) {
c = ((const uint16_t*)scanline)[x];
ch = graya_getv(c);
}
}
else {
if (x < image->width()) {
c = get_pixel_fast<RgbTraits>(image, x, y);
if (x < spec.width()) {
c = ((const uint32_t*)scanline)[x];
ch = rgba_getr(c);
}
else if (x<image->width()*2) {
c = get_pixel_fast<RgbTraits>(image, x-image->width(), y);
else if (x<spec.width()*2) {
c = ((const uint32_t*)scanline)[x-spec.width()];
ch = rgba_getg(c);
}
else {
c = get_pixel_fast<RgbTraits>(image, x-image->width()*2, y);
c = ((const uint32_t*)scanline)[x-spec.width()*2];
ch = rgba_getb(c);
}
}
@ -285,7 +291,7 @@ bool PcxFormat::onSave(FileOp* fop)
fputc(runchar, f);
fop->setProgress((float)(y+1) / (float)(image->height()));
fop->setProgress((float)(y+1) / (float)(spec.height()));
}
if (depth == 8) { /* 256 color palette */

View File

@ -54,7 +54,8 @@ class PngFormat : public FileFormat {
FILE_SUPPORT_GRAYA |
FILE_SUPPORT_INDEXED |
FILE_SUPPORT_SEQUENCES |
FILE_SUPPORT_PALETTE_WITH_ALPHA;
FILE_SUPPORT_PALETTE_WITH_ALPHA |
FILE_ENCODE_ABSTRACT_IMAGE;
}
bool onLoad(FileOp* fop) override;
@ -275,7 +276,7 @@ bool PngFormat::onLoad(FileOp* fop)
int imageWidth = png_get_image_width(png, info);
int imageHeight = png_get_image_height(png, info);
Image* image = fop->sequenceImage(pixelFormat, imageWidth, imageHeight);
ImageRef image = fop->sequenceImage(pixelFormat, imageWidth, imageHeight);
if (!image) {
fop->setError("file_sequence_image %dx%d\n", imageWidth, imageHeight);
return false;
@ -550,23 +551,25 @@ bool PngFormat::onSave(FileOp* fop)
png_init_io(png, fp);
const Image* image = fop->sequenceImage();
switch (image->pixelFormat()) {
case IMAGE_RGB:
const FileAbstractImage* img = fop->abstractImage();
const ImageSpec spec = img->spec();
switch (spec.colorMode()) {
case ColorMode::RGB:
color_type =
(fop->document()->sprite()->needAlpha() ||
(img->needAlpha() ||
fix_one_alpha_pixel ?
PNG_COLOR_TYPE_RGB_ALPHA:
PNG_COLOR_TYPE_RGB);
break;
case IMAGE_GRAYSCALE:
case ColorMode::GRAYSCALE:
color_type =
(fop->document()->sprite()->needAlpha() ||
(img->needAlpha() ||
fix_one_alpha_pixel ?
PNG_COLOR_TYPE_GRAY_ALPHA:
PNG_COLOR_TYPE_GRAY);
break;
case IMAGE_INDEXED:
case ColorMode::INDEXED:
if (fix_one_alpha_pixel)
color_type = PNG_COLOR_TYPE_RGB_ALPHA;
else
@ -574,8 +577,8 @@ bool PngFormat::onSave(FileOp* fop)
break;
}
const png_uint_32 width = image->width();
const png_uint_32 height = image->height();
const png_uint_32 width = spec.width();
const png_uint_32 height = spec.height();
png_set_IHDR(png, info, width, height, 8, color_type,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
@ -606,9 +609,9 @@ bool PngFormat::onSave(FileOp* fop)
png_set_unknown_chunks(png, info, &unknowns[0], num_unknowns);
}
if (fop->preserveColorProfile() &&
fop->document()->sprite()->colorSpace())
saveColorSpace(png, info, fop->document()->sprite()->colorSpace().get());
if (fop->preserveColorProfile() && spec.colorSpace()) {
saveColorSpace(png, info, spec.colorSpace().get());
}
if (color_type == PNG_COLOR_TYPE_PALETTE) {
int c, r, g, b;
@ -633,7 +636,7 @@ bool PngFormat::onSave(FileOp* fop)
// If the sprite does not have a (visible) background layer, we
// put alpha=0 to the transparent color.
int mask_entry = -1;
if (fop->document()->sprite()->backgroundLayer() == NULL ||
if (fop->document()->sprite()->backgroundLayer() == nullptr ||
!fop->document()->sprite()->backgroundLayer()->isVisible()) {
mask_entry = fop->document()->sprite()->transparentColor();
}
@ -668,8 +671,8 @@ bool PngFormat::onSave(FileOp* fop)
unsigned int x, c, a;
bool opaque = true;
if (image->pixelFormat() == IMAGE_RGB) {
uint32_t* src_address = (uint32_t*)image->getPixelAddress(0, y);
if (spec.colorMode() == ColorMode::RGB) {
auto src_address = (const uint32_t*)img->getScanline(y);
for (x=0; x<width; ++x) {
c = *(src_address++);
@ -690,8 +693,8 @@ bool PngFormat::onSave(FileOp* fop)
}
// In case that we are converting an indexed image to RGB just
// to convert one pixel with alpha=254.
else if (image->pixelFormat() == IMAGE_INDEXED) {
uint8_t* src_address = (uint8_t*)image->getPixelAddress(0, y);
else if (spec.colorMode() == ColorMode::INDEXED) {
auto src_address = (const uint8_t*)img->getScanline(y);
unsigned int x, c;
int r, g, b, a;
bool opaque = true;
@ -716,7 +719,7 @@ bool PngFormat::onSave(FileOp* fop)
}
}
else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_RGB) {
uint32_t* src_address = (uint32_t*)image->getPixelAddress(0, y);
auto src_address = (const uint32_t*)img->getScanline(y);
unsigned int x, c;
for (x=0; x<width; ++x) {
@ -727,7 +730,7 @@ bool PngFormat::onSave(FileOp* fop)
}
}
else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_GRAY_ALPHA) {
uint16_t* src_address = (uint16_t*)image->getPixelAddress(0, y);
auto src_address = (const uint16_t*)img->getScanline(y);
unsigned int x, c, a;
bool opaque = true;
@ -747,7 +750,7 @@ bool PngFormat::onSave(FileOp* fop)
}
}
else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_GRAY) {
uint16_t* src_address = (uint16_t*)image->getPixelAddress(0, y);
auto src_address = (const uint16_t*)img->getScanline(y);
unsigned int x, c;
for (x=0; x<width; ++x) {
@ -756,7 +759,7 @@ bool PngFormat::onSave(FileOp* fop)
}
}
else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_PALETTE) {
uint8_t* src_address = (uint8_t*)image->getPixelAddress(0, y);
auto src_address = (const uint8_t*)img->getScanline(y);
unsigned int x;
for (x=0; x<width; ++x)
@ -771,7 +774,7 @@ bool PngFormat::onSave(FileOp* fop)
png_free(png, row_pointer);
png_write_end(png, info);
if (image->pixelFormat() == IMAGE_INDEXED) {
if (spec.colorMode() == ColorMode::INDEXED) {
png_free(png, palette);
palette = nullptr;
}

View File

@ -81,7 +81,7 @@ bool SvgFormat::onLoad(FileOp* fop)
bool SvgFormat::onSave(FileOp* fop)
{
const Image* image = fop->sequenceImage();
const ImageRef image = fop->sequenceImage();
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);
@ -103,7 +103,7 @@ bool SvgFormat::onSave(FileOp* fop)
case IMAGE_RGB: {
for (y=0; y<image->height(); y++) {
for (x=0; x<image->width(); x++) {
c = get_pixel_fast<RgbTraits>(image, x, y);
c = get_pixel_fast<RgbTraits>(image.get(), x, y);
alpha = rgba_geta(c);
if (alpha != 0x00)
printcol(x, y, rgba_getr(c), rgba_getg(c), rgba_getb(c), alpha, pixelScaleValue);
@ -115,7 +115,7 @@ bool SvgFormat::onSave(FileOp* fop)
case IMAGE_GRAYSCALE: {
for (y=0; y<image->height(); y++) {
for (x=0; x<image->width(); x++) {
c = get_pixel_fast<GrayscaleTraits>(image, x, y);
c = get_pixel_fast<GrayscaleTraits>(image.get(), x, y);
auto v = graya_getv(c);
alpha = graya_geta(c);
if (alpha != 0x00)
@ -142,7 +142,7 @@ bool SvgFormat::onSave(FileOp* fop)
}
for (y=0; y<image->height(); y++) {
for (x=0; x<image->width(); x++) {
c = get_pixel_fast<IndexedTraits>(image, x, y);
c = get_pixel_fast<IndexedTraits>(image.get(), x, y);
if (c != mask_color)
printcol(x, y, image_palette[c][0] & 0xff,
image_palette[c][1] & 0xff,

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -53,7 +53,8 @@ class TgaFormat : public FileFormat {
FILE_SUPPORT_INDEXED |
FILE_SUPPORT_SEQUENCES |
FILE_SUPPORT_GET_FORMAT_OPTIONS |
FILE_SUPPORT_PALETTE_WITH_ALPHA;
FILE_SUPPORT_PALETTE_WITH_ALPHA |
FILE_ENCODE_ABSTRACT_IMAGE;
}
bool onLoad(FileOp* fop) override;
@ -164,9 +165,9 @@ bool TgaFormat::onLoad(FileOp* fop)
if (decoder.hasAlpha())
fop->sequenceSetHasAlpha(true);
Image* image = fop->sequenceImage((doc::PixelFormat)spec.colorMode(),
spec.width(),
spec.height());
ImageRef image = fop->sequenceImage((doc::PixelFormat)spec.colorMode(),
spec.width(),
spec.height());
if (!image)
return false;
@ -188,7 +189,7 @@ bool TgaFormat::onLoad(FileOp* fop)
// Post process gray image pixels (because we use grayscale images
// with alpha).
if (header.isGray()) {
doc::LockImageBits<GrayscaleTraits> bits(image);
doc::LockImageBits<GrayscaleTraits> bits(image.get());
for (auto it=bits.begin(), end=bits.end(); it != end; ++it) {
*it = doc::graya(*it, 255);
}
@ -217,7 +218,7 @@ bool TgaFormat::onLoad(FileOp* fop)
namespace {
void prepare_header(tga::Header& header,
const doc::Image* image,
const doc::ImageSpec& spec,
const doc::Palette* palette,
const bool isOpaque,
const bool compressed,
@ -231,13 +232,13 @@ void prepare_header(tga::Header& header,
header.colormapDepth = 0;
header.xOrigin = 0;
header.yOrigin = 0;
header.width = image->width();
header.height = image->height();
header.width = spec.width();
header.height = spec.height();
header.bitsPerPixel = 0;
// TODO make this option configurable
header.imageDescriptor = 0x20; // Top-to-bottom
switch (image->colorMode()) {
switch (spec.colorMode()) {
case ColorMode::RGB:
header.imageType = (compressed ? tga::RleRgb: tga::UncompressedRgb);
header.bitsPerPixel = (bitsPerPixel > 8 ?
@ -287,7 +288,7 @@ void prepare_header(tga::Header& header,
bool TgaFormat::onSave(FileOp* fop)
{
const Image* image = fop->sequenceImage();
const FileAbstractImage* img = fop->abstractImage();
const Palette* palette = fop->sequenceGetPalette();
FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb"));
@ -297,7 +298,7 @@ bool TgaFormat::onSave(FileOp* fop)
const auto tgaOptions = std::static_pointer_cast<TgaOptions>(fop->formatOptions());
prepare_header(
header, image, palette,
header, img->spec(), palette,
// Is alpha channel required?
fop->document()->sprite()->isOpaque(),
// Compressed by default
@ -307,6 +308,7 @@ bool TgaFormat::onSave(FileOp* fop)
encoder.writeHeader(header);
doc::ImageRef image = img->getScaledImage();
tga::Image tgaImage;
tgaImage.pixels = image->getPixelAddress(0, 0);
tgaImage.rowstride = image->getRowStrideSize();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2021 Igara Studio S.A.
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -112,7 +112,10 @@ void Preferences::load()
void Preferences::save()
{
ui::assert_ui_thread();
#ifdef _DEBUG
if (ui::UISystem::instance())
ui::assert_ui_thread();
#endif
app::gen::GlobalPref::save();
for (auto& pair : m_tools)

View File

@ -10,6 +10,6 @@
// Increment this value if the scripting API is modified between two
// released Aseprite versions.
#define API_VERSION 18
#define API_VERSION 19
#endif

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2021 Igara Studio S.A.
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2015-2018 David Capello
//
// This program is distributed under the terms of
@ -105,8 +105,29 @@ int Image_new(lua_State* L)
if (auto spec2 = may_get_obj<doc::ImageSpec>(L, 1)) {
spec = *spec2;
}
else if (may_get_obj<ImageObj>(L, 1)) {
return Image_clone(L);
else if (auto imgObj = may_get_obj<ImageObj>(L, 1)) {
// Copy a region of the image
if (auto rc = may_get_obj<gfx::Rect>(L, 2)) {
doc::Image* crop = nullptr;
try {
auto docImg = imgObj->image(L);
crop = doc::crop_image(docImg, *rc, docImg->maskColor());
}
catch (const std::invalid_argument&) {
// Do nothing (will return nil)
}
if (crop) {
push_new<ImageObj>(L, crop);
return 1;
}
else {
return 0;
}
}
// Copy the whole image
else {
return Image_clone(L);
}
}
else if (auto spr = may_get_docobj<doc::Sprite>(L, 1)) {
image = doc::Image::create(spr->spec());

View File

@ -35,6 +35,7 @@ namespace app {
AppMenuItem(const std::string& text,
const std::string& commandId = std::string(),
const Params& params = Params());
AppMenuItem(const std::string& text, std::nullptr_t) = delete;
KeyPtr key() { return m_key; }
void setKey(const KeyPtr& key);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
@ -51,8 +51,7 @@ ExportFileWindow::ExportFileWindow(const Doc* doc)
}
// Default export configuration
resize()->setValue(
base::convert_to<std::string>(m_docPref.saveCopy.resizeScale()));
setResizeScale(m_docPref.saveCopy.resizeScale());
fill_layers_combobox(m_doc->sprite(), layers(), m_docPref.saveCopy.layer());
fill_frames_combobox(m_doc->sprite(), frames(), m_docPref.saveCopy.frameTag());
fill_anidir_combobox(anidir(), m_docPref.saveCopy.aniDir());
@ -113,7 +112,8 @@ std::string ExportFileWindow::outputFilenameValue() const
double ExportFileWindow::resizeValue() const
{
return base::convert_to<double>(resize()->getValue());
double value = resize()->getEntryWidget()->textDouble() / 100.0;
return std::clamp(value, 0.001, 100000000.0);
}
std::string ExportFileWindow::layersValue() const
@ -141,6 +141,16 @@ bool ExportFileWindow::isForTwitter() const
return forTwitter()->isSelected();
}
void ExportFileWindow::setResizeScale(double scale)
{
resize()->setValue(fmt::format("{:.2f}", 100.0 * scale));
}
void ExportFileWindow::setAniDir(const doc::AniDir aniDir)
{
anidir()->setSelectedItemIndex(int(aniDir));
}
void ExportFileWindow::setOutputFilename(const std::string& pathAndFilename)
{
m_outputPath = base::get_file_path(pathAndFilename);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2020-2022 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
@ -34,10 +34,13 @@ namespace app {
bool applyPixelRatio() const;
bool isForTwitter() const;
void setOutputFilename(const std::string& pathAndFilename);
void setResizeScale(const double scale);
void setAniDir(const doc::AniDir aniDir);
obs::signal<std::string()> SelectOutputFile;
private:
void setOutputFilename(const std::string& pathAndFilename);
void updateOutputFilenameEntry();
void onOutputFilenameEntryChange();
void updateAniDir();

View File

@ -267,6 +267,11 @@ void ExpandCelCanvas::commit()
// And add the cel again in the layer.
m_cmds->executeAndAdd(new cmd::AddCel(m_layer, m_cel));
}
else {
// Delete unused cel
delete m_cel;
m_cel = nullptr;
}
}
// We are selecting...
else {

View File

@ -231,6 +231,10 @@ Widget* WidgetLoader::convertXmlElementToWidget(const TiXmlElement* elem, Widget
bool editable = bool_attr(elem, "editable", false);
if (editable)
((ComboBox*)widget)->setEditable(true);
const char* suffix = elem->Attribute("suffix");
if (suffix)
((ComboBox*)widget)->getEntryWidget()->setSuffix(suffix);
}
else if (elem_name == "entry" ||
elem_name == "expr") {

View File

@ -328,7 +328,8 @@ void ComboBox::setValue(const std::string& value)
{
if (isEditable()) {
m_entry->setText(value);
m_entry->selectAllText();
if (hasFocus())
m_entry->selectAllText();
}
else {
int index = findItemIndexByValue(value);
@ -560,6 +561,14 @@ bool ComboBoxEntry::onProcessMessage(Message* msg)
return result;
}
case kFocusLeaveMessage:
if (m_comboBox->isEditable() &&
m_comboBox->m_window &&
!View::getView(m_comboBox->m_listbox)->hasMouse()) {
m_comboBox->closeListBox();
}
break;
}
return Entry::onProcessMessage(msg);

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2018-2021 Igara Studio S.A.
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -103,7 +103,7 @@ void Overlay::captureOverlappedArea()
m_overlap->width(), m_overlap->height());
m_overlap->setImmutable();
m_captured = displaySurface;
m_captured = base::AddRef(displaySurface);
}
void Overlay::restoreOverlappedArea(const gfx::Rect& restoreBounds)
@ -118,7 +118,7 @@ void Overlay::restoreOverlappedArea(const gfx::Rect& restoreBounds)
return;
os::SurfaceLock lock(m_overlap.get());
m_overlap->blitTo(m_captured, 0, 0, m_pos.x, m_pos.y,
m_overlap->blitTo(m_captured.get(), 0, 0, m_pos.x, m_pos.y,
m_overlap->width(), m_overlap->height());
m_display->dirtyRect(bounds());

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2018-2021 Igara Studio S.A.
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -60,7 +60,7 @@ namespace ui {
// Surface where we captured the overlapped (m_overlap)
// region. It's nullptr if the overlay wasn't drawn yet.
os::Surface* m_captured;
os::SurfaceRef m_captured;
gfx::Point m_pos;
ZOrder m_zorder;

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2018-2021 Igara Studio S.A.
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.