
Previously, only the color from the selected layer would be picked. Now, we allow the user to select if they want to sample the color from all layers.
550 lines
16 KiB
C++
550 lines
16 KiB
C++
/*
|
|
* Copyright (c) 2020-2021, Andreas Kling <kling@serenityos.org>
|
|
* Copyright (c) 2021, Mustafa Quraish <mustafa@cs.toronto.edu>
|
|
* Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include "Image.h"
|
|
#include "Layer.h"
|
|
#include "Selection.h"
|
|
#include <AK/Base64.h>
|
|
#include <AK/JsonObject.h>
|
|
#include <AK/JsonObjectSerializer.h>
|
|
#include <AK/JsonValue.h>
|
|
#include <AK/LexicalPath.h>
|
|
#include <AK/MappedFile.h>
|
|
#include <AK/StringBuilder.h>
|
|
#include <LibGUI/Painter.h>
|
|
#include <LibGfx/BMPWriter.h>
|
|
#include <LibGfx/Bitmap.h>
|
|
#include <LibGfx/PNGWriter.h>
|
|
#include <LibImageDecoderClient/Client.h>
|
|
#include <stdio.h>
|
|
|
|
namespace PixelPaint {
|
|
|
|
RefPtr<Image> Image::try_create_with_size(Gfx::IntSize const& size)
|
|
{
|
|
if (size.is_empty())
|
|
return nullptr;
|
|
|
|
if (size.width() > 16384 || size.height() > 16384)
|
|
return nullptr;
|
|
|
|
return adopt_ref(*new Image(size));
|
|
}
|
|
|
|
Image::Image(Gfx::IntSize const& size)
|
|
: m_title("Untitled")
|
|
, m_size(size)
|
|
{
|
|
}
|
|
|
|
void Image::paint_into(GUI::Painter& painter, Gfx::IntRect const& dest_rect) const
|
|
{
|
|
float scale = (float)dest_rect.width() / (float)rect().width();
|
|
Gfx::PainterStateSaver saver(painter);
|
|
painter.add_clip_rect(dest_rect);
|
|
for (auto& layer : m_layers) {
|
|
if (!layer.is_visible())
|
|
continue;
|
|
auto target = dest_rect.translated(layer.location().x() * scale, layer.location().y() * scale);
|
|
target.set_size(layer.size().width() * scale, layer.size().height() * scale);
|
|
painter.draw_scaled_bitmap(target, layer.bitmap(), layer.rect(), (float)layer.opacity_percent() / 100.0f);
|
|
}
|
|
}
|
|
|
|
RefPtr<Gfx::Bitmap> Image::try_decode_bitmap(ReadonlyBytes const& bitmap_data)
|
|
{
|
|
// Spawn a new ImageDecoder service process and connect to it.
|
|
auto client = ImageDecoderClient::Client::construct();
|
|
|
|
// FIXME: Find a way to avoid the memory copying here.
|
|
auto decoded_image_or_error = client->decode_image(bitmap_data);
|
|
if (!decoded_image_or_error.has_value())
|
|
return nullptr;
|
|
|
|
// FIXME: Support multi-frame images?
|
|
auto decoded_image = decoded_image_or_error.release_value();
|
|
if (decoded_image.frames.is_empty())
|
|
return nullptr;
|
|
return move(decoded_image.frames[0].bitmap);
|
|
}
|
|
|
|
RefPtr<Image> Image::try_create_from_bitmap(NonnullRefPtr<Gfx::Bitmap> bitmap)
|
|
{
|
|
auto image = try_create_with_size({ bitmap->width(), bitmap->height() });
|
|
if (!image)
|
|
return nullptr;
|
|
|
|
auto layer = Layer::try_create_with_bitmap(*image, *bitmap, "Background");
|
|
if (!layer)
|
|
return nullptr;
|
|
|
|
image->add_layer(layer.release_nonnull());
|
|
return image;
|
|
}
|
|
|
|
Result<NonnullRefPtr<Image>, String> Image::try_create_from_pixel_paint_json(JsonObject const& json)
|
|
{
|
|
auto image = try_create_with_size({ json.get("width").to_i32(), json.get("height").to_i32() });
|
|
if (!image)
|
|
return String { "Image memory allocation failed" };
|
|
|
|
auto layers_value = json.get("layers");
|
|
for (auto& layer_value : layers_value.as_array().values()) {
|
|
auto& layer_object = layer_value.as_object();
|
|
auto name = layer_object.get("name").as_string();
|
|
|
|
auto bitmap_base64_encoded = layer_object.get("bitmap").as_string();
|
|
auto bitmap_data = decode_base64(bitmap_base64_encoded);
|
|
|
|
auto bitmap = try_decode_bitmap(bitmap_data);
|
|
if (!bitmap)
|
|
return String { "Layer bitmap decode failed"sv };
|
|
|
|
auto layer = Layer::try_create_with_bitmap(*image, bitmap.release_nonnull(), name);
|
|
if (!layer)
|
|
return String { "Layer allocation failed"sv };
|
|
|
|
auto width = layer_object.get("width").to_i32();
|
|
auto height = layer_object.get("height").to_i32();
|
|
|
|
if (width != layer->size().width() || height != layer->size().height())
|
|
return String { "Decoded layer bitmap has wrong size"sv };
|
|
|
|
image->add_layer(*layer);
|
|
|
|
layer->set_location({ layer_object.get("locationx").to_i32(), layer_object.get("locationy").to_i32() });
|
|
layer->set_opacity_percent(layer_object.get("opacity_percent").to_i32());
|
|
layer->set_visible(layer_object.get("visible").as_bool());
|
|
layer->set_selected(layer_object.get("selected").as_bool());
|
|
}
|
|
|
|
return image.release_nonnull();
|
|
}
|
|
|
|
void Image::serialize_as_json(JsonObjectSerializer<StringBuilder>& json) const
|
|
{
|
|
json.add("width", m_size.width());
|
|
json.add("height", m_size.height());
|
|
{
|
|
auto json_layers = json.add_array("layers");
|
|
for (const auto& layer : m_layers) {
|
|
Gfx::BMPWriter bmp_dumber;
|
|
auto json_layer = json_layers.add_object();
|
|
json_layer.add("width", layer.size().width());
|
|
json_layer.add("height", layer.size().height());
|
|
json_layer.add("name", layer.name());
|
|
json_layer.add("locationx", layer.location().x());
|
|
json_layer.add("locationy", layer.location().y());
|
|
json_layer.add("opacity_percent", layer.opacity_percent());
|
|
json_layer.add("visible", layer.is_visible());
|
|
json_layer.add("selected", layer.is_selected());
|
|
json_layer.add("bitmap", encode_base64(bmp_dumber.dump(layer.bitmap())));
|
|
}
|
|
}
|
|
}
|
|
|
|
Result<void, String> Image::write_to_file(const String& file_path) const
|
|
{
|
|
StringBuilder builder;
|
|
JsonObjectSerializer json(builder);
|
|
serialize_as_json(json);
|
|
json.finish();
|
|
|
|
auto file_or_error = Core::File::open(file_path, (Core::OpenMode)(Core::OpenMode::WriteOnly | Core::OpenMode::Truncate));
|
|
if (file_or_error.is_error())
|
|
return String { file_or_error.error().string() };
|
|
|
|
if (!file_or_error.value()->write(builder.string_view()))
|
|
return String { file_or_error.value()->error_string() };
|
|
return {};
|
|
}
|
|
|
|
RefPtr<Gfx::Bitmap> Image::try_compose_bitmap(Gfx::BitmapFormat format) const
|
|
{
|
|
auto bitmap = Gfx::Bitmap::try_create(format, m_size);
|
|
if (!bitmap)
|
|
return nullptr;
|
|
GUI::Painter painter(*bitmap);
|
|
paint_into(painter, { 0, 0, m_size.width(), m_size.height() });
|
|
return bitmap;
|
|
}
|
|
|
|
RefPtr<Gfx::Bitmap> Image::try_copy_bitmap(Selection const& selection) const
|
|
{
|
|
if (selection.is_empty())
|
|
return {};
|
|
auto selection_rect = selection.bounding_rect();
|
|
|
|
// FIXME: Add a way to only compose a certain part of the image
|
|
auto full_bitmap = try_compose_bitmap(Gfx::BitmapFormat::BGRA8888);
|
|
if (!full_bitmap)
|
|
return {};
|
|
|
|
return full_bitmap->cropped(selection_rect);
|
|
}
|
|
|
|
Result<void, String> Image::export_bmp_to_fd_and_close(int fd, bool preserve_alpha_channel)
|
|
{
|
|
auto file = Core::File::construct();
|
|
file->open(fd, Core::OpenMode::WriteOnly | Core::OpenMode::Truncate, Core::File::ShouldCloseFileDescriptor::Yes);
|
|
if (file->has_error())
|
|
return String { file->error_string() };
|
|
|
|
auto bitmap_format = preserve_alpha_channel ? Gfx::BitmapFormat::BGRA8888 : Gfx::BitmapFormat::BGRx8888;
|
|
auto bitmap = try_compose_bitmap(bitmap_format);
|
|
if (!bitmap)
|
|
return String { "Failed to allocate bitmap for encoding"sv };
|
|
|
|
Gfx::BMPWriter dumper;
|
|
auto encoded_data = dumper.dump(bitmap);
|
|
|
|
if (!file->write(encoded_data.data(), encoded_data.size()))
|
|
return String { "Failed to write encoded BMP data to file"sv };
|
|
|
|
return {};
|
|
}
|
|
|
|
Result<void, String> Image::export_png_to_fd_and_close(int fd, bool preserve_alpha_channel)
|
|
{
|
|
auto file = Core::File::construct();
|
|
file->open(fd, Core::OpenMode::WriteOnly | Core::OpenMode::Truncate, Core::File::ShouldCloseFileDescriptor::Yes);
|
|
if (file->has_error())
|
|
return String { file->error_string() };
|
|
|
|
auto bitmap_format = preserve_alpha_channel ? Gfx::BitmapFormat::BGRA8888 : Gfx::BitmapFormat::BGRx8888;
|
|
auto bitmap = try_compose_bitmap(bitmap_format);
|
|
if (!bitmap)
|
|
return String { "Failed to allocate bitmap for encoding"sv };
|
|
|
|
auto encoded_data = Gfx::PNGWriter::encode(*bitmap);
|
|
if (!file->write(encoded_data.data(), encoded_data.size()))
|
|
return String { "Failed to write encoded PNG data to file"sv };
|
|
|
|
return {};
|
|
}
|
|
|
|
void Image::add_layer(NonnullRefPtr<Layer> layer)
|
|
{
|
|
for (auto& existing_layer : m_layers) {
|
|
VERIFY(&existing_layer != layer.ptr());
|
|
}
|
|
m_layers.append(move(layer));
|
|
|
|
for (auto* client : m_clients)
|
|
client->image_did_add_layer(m_layers.size() - 1);
|
|
|
|
did_modify_layer_stack();
|
|
}
|
|
|
|
RefPtr<Image> Image::take_snapshot() const
|
|
{
|
|
auto snapshot = try_create_with_size(m_size);
|
|
if (!snapshot)
|
|
return nullptr;
|
|
for (const auto& layer : m_layers) {
|
|
auto layer_snapshot = Layer::try_create_snapshot(*snapshot, layer);
|
|
if (!layer_snapshot)
|
|
return nullptr;
|
|
snapshot->add_layer(layer_snapshot.release_nonnull());
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
void Image::restore_snapshot(Image const& snapshot)
|
|
{
|
|
m_layers.clear();
|
|
select_layer(nullptr);
|
|
for (const auto& snapshot_layer : snapshot.m_layers) {
|
|
auto layer = Layer::try_create_snapshot(*this, snapshot_layer);
|
|
VERIFY(layer);
|
|
if (layer->is_selected())
|
|
select_layer(layer.ptr());
|
|
add_layer(*layer);
|
|
}
|
|
|
|
did_modify_layer_stack();
|
|
}
|
|
|
|
size_t Image::index_of(Layer const& layer) const
|
|
{
|
|
for (size_t i = 0; i < m_layers.size(); ++i) {
|
|
if (&m_layers.at(i) == &layer)
|
|
return i;
|
|
}
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
void Image::move_layer_to_back(Layer& layer)
|
|
{
|
|
NonnullRefPtr<Layer> protector(layer);
|
|
auto index = index_of(layer);
|
|
m_layers.remove(index);
|
|
m_layers.prepend(layer);
|
|
|
|
did_modify_layer_stack();
|
|
}
|
|
|
|
void Image::move_layer_to_front(Layer& layer)
|
|
{
|
|
NonnullRefPtr<Layer> protector(layer);
|
|
auto index = index_of(layer);
|
|
m_layers.remove(index);
|
|
m_layers.append(layer);
|
|
|
|
did_modify_layer_stack();
|
|
}
|
|
|
|
void Image::move_layer_down(Layer& layer)
|
|
{
|
|
NonnullRefPtr<Layer> protector(layer);
|
|
auto index = index_of(layer);
|
|
if (!index)
|
|
return;
|
|
m_layers.remove(index);
|
|
m_layers.insert(index - 1, layer);
|
|
|
|
did_modify_layer_stack();
|
|
}
|
|
|
|
void Image::move_layer_up(Layer& layer)
|
|
{
|
|
NonnullRefPtr<Layer> protector(layer);
|
|
auto index = index_of(layer);
|
|
if (index == m_layers.size() - 1)
|
|
return;
|
|
m_layers.remove(index);
|
|
m_layers.insert(index + 1, layer);
|
|
|
|
did_modify_layer_stack();
|
|
}
|
|
|
|
void Image::change_layer_index(size_t old_index, size_t new_index)
|
|
{
|
|
VERIFY(old_index < m_layers.size());
|
|
VERIFY(new_index < m_layers.size());
|
|
auto layer = m_layers.take(old_index);
|
|
m_layers.insert(new_index, move(layer));
|
|
did_modify_layer_stack();
|
|
}
|
|
|
|
void Image::did_modify_layer_stack()
|
|
{
|
|
for (auto* client : m_clients)
|
|
client->image_did_modify_layer_stack();
|
|
|
|
did_change();
|
|
}
|
|
|
|
void Image::remove_layer(Layer& layer)
|
|
{
|
|
NonnullRefPtr<Layer> protector(layer);
|
|
auto index = index_of(layer);
|
|
m_layers.remove(index);
|
|
|
|
for (auto* client : m_clients)
|
|
client->image_did_remove_layer(index);
|
|
|
|
did_modify_layer_stack();
|
|
}
|
|
|
|
void Image::flatten_all_layers()
|
|
{
|
|
if (m_layers.size() < 2)
|
|
return;
|
|
|
|
auto& bottom_layer = m_layers.at(0);
|
|
|
|
GUI::Painter painter(bottom_layer.bitmap());
|
|
paint_into(painter, { 0, 0, m_size.width(), m_size.height() });
|
|
|
|
for (size_t index = m_layers.size() - 1; index > 0; index--) {
|
|
auto& layer = m_layers.at(index);
|
|
remove_layer(layer);
|
|
}
|
|
bottom_layer.set_name("Background");
|
|
select_layer(&bottom_layer);
|
|
}
|
|
|
|
void Image::merge_visible_layers()
|
|
{
|
|
if (m_layers.size() < 2)
|
|
return;
|
|
|
|
size_t index = 0;
|
|
|
|
while (index < m_layers.size()) {
|
|
if (m_layers.at(index).is_visible()) {
|
|
auto& bottom_layer = m_layers.at(index);
|
|
GUI::Painter painter(bottom_layer.bitmap());
|
|
paint_into(painter, { 0, 0, m_size.width(), m_size.height() });
|
|
select_layer(&bottom_layer);
|
|
index++;
|
|
break;
|
|
}
|
|
index++;
|
|
}
|
|
while (index < m_layers.size()) {
|
|
if (m_layers.at(index).is_visible()) {
|
|
auto& layer = m_layers.at(index);
|
|
remove_layer(layer);
|
|
} else {
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Image::merge_active_layer_down(Layer& layer)
|
|
{
|
|
if (m_layers.size() < 2)
|
|
return;
|
|
int layer_index = this->index_of(layer);
|
|
if (layer_index == 0) {
|
|
dbgln("Cannot merge layer down: layer is already at the bottom");
|
|
return; // FIXME: Notify user of error properly.
|
|
}
|
|
|
|
auto& layer_below = m_layers.at(layer_index - 1);
|
|
GUI::Painter painter(layer_below.bitmap());
|
|
painter.draw_scaled_bitmap(rect(), layer.bitmap(), layer.rect(), (float)layer.opacity_percent() / 100.0f);
|
|
remove_layer(layer);
|
|
select_layer(&layer_below);
|
|
}
|
|
|
|
void Image::select_layer(Layer* layer)
|
|
{
|
|
for (auto* client : m_clients)
|
|
client->image_select_layer(layer);
|
|
}
|
|
|
|
void Image::add_client(ImageClient& client)
|
|
{
|
|
VERIFY(!m_clients.contains(&client));
|
|
m_clients.set(&client);
|
|
}
|
|
|
|
void Image::remove_client(ImageClient& client)
|
|
{
|
|
VERIFY(m_clients.contains(&client));
|
|
m_clients.remove(&client);
|
|
}
|
|
|
|
void Image::layer_did_modify_bitmap(Badge<Layer>, Layer const& layer, Gfx::IntRect const& modified_layer_rect)
|
|
{
|
|
auto layer_index = index_of(layer);
|
|
for (auto* client : m_clients)
|
|
client->image_did_modify_layer_bitmap(layer_index);
|
|
|
|
did_change(modified_layer_rect.translated(layer.location()));
|
|
}
|
|
|
|
void Image::layer_did_modify_properties(Badge<Layer>, Layer const& layer)
|
|
{
|
|
auto layer_index = index_of(layer);
|
|
for (auto* client : m_clients)
|
|
client->image_did_modify_layer_properties(layer_index);
|
|
|
|
did_change();
|
|
}
|
|
|
|
void Image::did_change(Gfx::IntRect const& a_modified_rect)
|
|
{
|
|
auto modified_rect = a_modified_rect.is_empty() ? this->rect() : a_modified_rect;
|
|
for (auto* client : m_clients)
|
|
client->image_did_change(modified_rect);
|
|
}
|
|
|
|
void Image::did_change_rect(Gfx::IntRect const& a_modified_rect)
|
|
{
|
|
auto modified_rect = a_modified_rect.is_empty() ? this->rect() : a_modified_rect;
|
|
for (auto* client : m_clients)
|
|
client->image_did_change_rect(modified_rect);
|
|
}
|
|
|
|
ImageUndoCommand::ImageUndoCommand(Image& image)
|
|
: m_snapshot(image.take_snapshot())
|
|
, m_image(image)
|
|
{
|
|
}
|
|
|
|
void ImageUndoCommand::undo()
|
|
{
|
|
m_image.restore_snapshot(*m_snapshot);
|
|
}
|
|
|
|
void ImageUndoCommand::redo()
|
|
{
|
|
undo();
|
|
}
|
|
|
|
void Image::set_title(String title)
|
|
{
|
|
m_title = move(title);
|
|
for (auto* client : m_clients)
|
|
client->image_did_change_title(m_title);
|
|
}
|
|
|
|
void Image::set_path(String path)
|
|
{
|
|
m_path = move(path);
|
|
set_title(LexicalPath::basename(m_path));
|
|
}
|
|
|
|
void Image::flip(Gfx::Orientation orientation)
|
|
{
|
|
for (auto& layer : m_layers) {
|
|
auto flipped = layer.bitmap().flipped(orientation);
|
|
VERIFY(flipped);
|
|
layer.set_bitmap(*flipped);
|
|
layer.did_modify_bitmap(rect());
|
|
}
|
|
|
|
did_change();
|
|
}
|
|
|
|
void Image::rotate(Gfx::RotationDirection direction)
|
|
{
|
|
for (auto& layer : m_layers) {
|
|
auto rotated = layer.bitmap().rotated(direction);
|
|
VERIFY(rotated);
|
|
layer.set_bitmap(*rotated);
|
|
layer.did_modify_bitmap(rect());
|
|
}
|
|
|
|
m_size = { m_size.height(), m_size.width() };
|
|
did_change_rect();
|
|
}
|
|
|
|
void Image::crop(Gfx::IntRect const& cropped_rect)
|
|
{
|
|
for (auto& layer : m_layers) {
|
|
auto cropped = layer.bitmap().cropped(cropped_rect);
|
|
VERIFY(cropped);
|
|
layer.set_bitmap(*cropped);
|
|
layer.did_modify_bitmap(rect());
|
|
}
|
|
|
|
m_size = { cropped_rect.width(), cropped_rect.height() };
|
|
did_change_rect(cropped_rect);
|
|
}
|
|
|
|
Color Image::color_at(Gfx::IntPoint const& point) const
|
|
{
|
|
Color color;
|
|
for (auto& layer : m_layers) {
|
|
if (!layer.is_visible() || !layer.rect().contains(point))
|
|
continue;
|
|
|
|
auto layer_color = layer.bitmap().get_pixel(point);
|
|
float layer_opacity = layer.opacity_percent() / 100.0f;
|
|
layer_color.set_alpha((u8)(layer_color.alpha() * layer_opacity));
|
|
color = color.blend(layer_color);
|
|
}
|
|
return color;
|
|
}
|
|
|
|
}
|