diff --git a/Userland/Applications/PixelPaint/CMakeLists.txt b/Userland/Applications/PixelPaint/CMakeLists.txt index 3abfd4bd72..617ed9a98a 100644 --- a/Userland/Applications/PixelPaint/CMakeLists.txt +++ b/Userland/Applications/PixelPaint/CMakeLists.txt @@ -2,7 +2,7 @@ serenity_component( PixelPaint RECOMMENDED TARGETS PixelPaint - DEPENDS ImageDecoder + DEPENDS ImageDecoder FileSystemAccessServer ) compile_gml(PixelPaintWindow.gml PixelPaintWindowGML.h pixel_paint_window_gml) @@ -42,4 +42,4 @@ set(SOURCES ) serenity_app(PixelPaint ICON app-pixel-paint) -target_link_libraries(PixelPaint LibImageDecoderClient LibGUI LibGfx) +target_link_libraries(PixelPaint LibImageDecoderClient LibGUI LibGfx LibFileSystemAccessClient) diff --git a/Userland/Applications/PixelPaint/Image.cpp b/Userland/Applications/PixelPaint/Image.cpp index f1443cb4bb..9a77c9fe51 100644 --- a/Userland/Applications/PixelPaint/Image.cpp +++ b/Userland/Applications/PixelPaint/Image.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include @@ -85,13 +84,27 @@ RefPtr Image::try_create_from_bitmap(NonnullRefPtr bitmap) return image; } -Result, String> Image::try_create_from_pixel_paint_file(String const& file_path) +Result, String> Image::try_create_from_pixel_paint_fd(int fd, String const& file_path) +{ + auto file = Core::File::construct(); + file->open(fd, Core::OpenMode::ReadOnly, Core::File::ShouldCloseFileDescriptor::No); + if (file->has_error()) + return String { file->error_string() }; + + return try_create_from_pixel_paint_file(file, file_path); +} + +Result, String> Image::try_create_from_pixel_paint_path(String const& file_path) { auto file_or_error = Core::File::open(file_path, Core::OpenMode::ReadOnly); if (file_or_error.is_error()) return file_or_error.error(); - auto& file = *file_or_error.value(); + return try_create_from_pixel_paint_file(*file_or_error.value(), file_path); +} + +Result, String> Image::try_create_from_pixel_paint_file(Core::File& file, String const& file_path) +{ auto contents = file.read_all(); auto json_or_error = JsonValue::from_string(contents); @@ -137,9 +150,33 @@ Result, String> Image::try_create_from_pixel_paint_file(Str return image.release_nonnull(); } -Result, String> Image::try_create_from_file(String const& file_path) +Result, String> Image::try_create_from_fd_and_close(int fd, String const& file_path) { - auto image_or_error = try_create_from_pixel_paint_file(file_path); + auto image_or_error = try_create_from_pixel_paint_fd(fd, file_path); + if (!image_or_error.is_error()) { + close(fd); + return image_or_error.release_value(); + } + + auto file_or_error = MappedFile::map_from_fd_and_close(fd, file_path); + if (file_or_error.is_error()) + return String::formatted("Unable to mmap file {}", file_or_error.error().string()); + + auto& mapped_file = *file_or_error.value(); + // FIXME: Find a way to avoid the memory copy here. + auto bitmap = try_decode_bitmap(ByteBuffer::copy(mapped_file.bytes())); + if (!bitmap) + return String { "Unable to decode image"sv }; + auto image = Image::try_create_from_bitmap(bitmap.release_nonnull()); + if (!image) + return String { "Unable to allocate Image"sv }; + image->set_path(file_path); + return image.release_nonnull(); +} + +Result, String> Image::try_create_from_path(String const& file_path) +{ + auto image_or_error = try_create_from_pixel_paint_path(file_path); if (!image_or_error.is_error()) return image_or_error.release_value(); @@ -159,6 +196,40 @@ Result, String> Image::try_create_from_file(String const& f return image.release_nonnull(); } +Result Image::write_to_fd_and_close(int fd) const +{ + StringBuilder builder; + JsonObjectSerializer json(builder); + 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()))); + } + } + json.finish(); + + 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() }; + + if (!file->write(builder.string_view())) + return String { file->error_string() }; + return {}; +} + Result Image::write_to_file(const String& file_path) const { StringBuilder builder; @@ -202,11 +273,12 @@ RefPtr Image::try_compose_bitmap(Gfx::BitmapFormat format) const return bitmap; } -Result Image::export_bmp_to_file(String const& file_path, bool preserve_alpha_channel) +Result Image::export_bmp_to_fd_and_close(int fd, bool preserve_alpha_channel) { - auto file_or_error = Core::File::open(file_path, (Core::OpenMode)(Core::OpenMode::WriteOnly | Core::OpenMode::Truncate)); - if (file_or_error.is_error()) - return file_or_error.error(); + 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); @@ -216,18 +288,18 @@ Result Image::export_bmp_to_file(String const& file_path, bool pre Gfx::BMPWriter dumper; auto encoded_data = dumper.dump(bitmap); - auto& file = *file_or_error.value(); - if (!file.write(encoded_data.data(), encoded_data.size())) + if (!file->write(encoded_data.data(), encoded_data.size())) return String { "Failed to write encoded BMP data to file"sv }; return {}; } -Result Image::export_png_to_file(String const& file_path, bool preserve_alpha_channel) +Result Image::export_png_to_fd_and_close(int fd, bool preserve_alpha_channel) { - auto file_or_error = Core::File::open(file_path, (Core::OpenMode)(Core::OpenMode::WriteOnly | Core::OpenMode::Truncate)); - if (file_or_error.is_error()) - return file_or_error.error(); + 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); @@ -235,8 +307,7 @@ Result Image::export_png_to_file(String const& file_path, bool pre return String { "Failed to allocate bitmap for encoding"sv }; auto encoded_data = Gfx::PNGWriter::encode(*bitmap); - auto& file = *file_or_error.value(); - if (!file.write(encoded_data.data(), encoded_data.size())) + if (!file->write(encoded_data.data(), encoded_data.size())) return String { "Failed to write encoded PNG data to file"sv }; return {}; diff --git a/Userland/Applications/PixelPaint/Image.h b/Userland/Applications/PixelPaint/Image.h index 8371feca9a..f4c693d5e3 100644 --- a/Userland/Applications/PixelPaint/Image.h +++ b/Userland/Applications/PixelPaint/Image.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -40,7 +41,8 @@ protected: class Image : public RefCounted { public: static RefPtr try_create_with_size(Gfx::IntSize const&); - static Result, String> try_create_from_file(String const& file_path); + static Result, String> try_create_from_fd_and_close(int fd, String const& file_path); + static Result, String> try_create_from_path(String const& file_path); static RefPtr try_create_from_bitmap(NonnullRefPtr); // This generates a new Bitmap with the final image (all layers composed according to their attributes.) @@ -58,9 +60,10 @@ public: void restore_snapshot(Image const&); void paint_into(GUI::Painter&, Gfx::IntRect const& dest_rect) const; + Result write_to_fd_and_close(int fd) const; Result write_to_file(String const& file_path) const; - Result export_bmp_to_file(String const& file_path, bool preserve_alpha_channel); - Result export_png_to_file(String const& file_path, bool preserve_alpha_channel); + Result export_bmp_to_fd_and_close(int fd, bool preserve_alpha_channel); + Result export_png_to_fd_and_close(int fd, bool preserve_alpha_channel); void move_layer_to_front(Layer&); void move_layer_to_back(Layer&); @@ -89,7 +92,9 @@ public: private: explicit Image(Gfx::IntSize const&); - static Result, String> try_create_from_pixel_paint_file(String const& file_path); + static Result, String> try_create_from_pixel_paint_fd(int fd, String const& file_path); + static Result, String> try_create_from_pixel_paint_path(String const& file_path); + static Result, String> try_create_from_pixel_paint_file(Core::File& file, String const& file_path); void did_change(Gfx::IntRect const& modified_rect = {}); void did_modify_layer_stack(); diff --git a/Userland/Applications/PixelPaint/PaletteWidget.cpp b/Userland/Applications/PixelPaint/PaletteWidget.cpp index 3ecfa084c4..d42ca56e75 100644 --- a/Userland/Applications/PixelPaint/PaletteWidget.cpp +++ b/Userland/Applications/PixelPaint/PaletteWidget.cpp @@ -95,7 +95,7 @@ PaletteWidget::PaletteWidget() bottom_color_container.set_layout(); bottom_color_container.layout()->set_spacing(1); - auto result = load_palette_file("/res/color-palettes/default.palette"); + auto result = load_palette_path("/res/color-palettes/default.palette"); if (result.is_error()) { GUI::MessageBox::show_error(window(), String::formatted("Loading default palette failed: {}", result.error())); display_color_list(fallback_colors()); @@ -192,14 +192,8 @@ Vector PaletteWidget::colors() return colors; } -Result, String> PaletteWidget::load_palette_file(String const& file_path) +Result, String> PaletteWidget::load_palette_file(Core::File& file) { - auto file_or_error = Core::File::open(file_path, Core::OpenMode::ReadOnly); - if (file_or_error.is_error()) - return file_or_error.error(); - - auto& file = *file_or_error.value(); - Vector palette; while (file.can_read_line()) { @@ -224,20 +218,39 @@ Result, String> PaletteWidget::load_palette_file(String const& fil return palette; } -Result PaletteWidget::save_palette_file(Vector palette, String const& file_path) +Result, String> PaletteWidget::load_palette_fd_and_close(int fd) { - auto file_or_error = Core::File::open(file_path, Core::OpenMode::WriteOnly); + auto file = Core::File::construct(); + file->open(fd, Core::OpenMode::ReadOnly, Core::File::ShouldCloseFileDescriptor::Yes); + if (file->has_error()) + return String { file->error_string() }; + + return load_palette_file(file); +} + +Result, String> PaletteWidget::load_palette_path(String const& file_path) +{ + auto file_or_error = Core::File::open(file_path, Core::OpenMode::ReadOnly); if (file_or_error.is_error()) return file_or_error.error(); auto& file = *file_or_error.value(); + return load_palette_file(file); +} + +Result PaletteWidget::save_palette_fd_and_close(Vector palette, int fd) +{ + auto file = Core::File::construct(); + file->open(fd, Core::OpenMode::WriteOnly, Core::File::ShouldCloseFileDescriptor::Yes); + if (file->has_error()) + return String { file->error_string() }; for (auto& color : palette) { - file.write(color.to_string_without_alpha()); - file.write("\n"); + file->write(color.to_string_without_alpha()); + file->write("\n"); } - file.close(); + file->close(); return {}; } diff --git a/Userland/Applications/PixelPaint/PaletteWidget.h b/Userland/Applications/PixelPaint/PaletteWidget.h index 022909e54e..b90e5d8b42 100644 --- a/Userland/Applications/PixelPaint/PaletteWidget.h +++ b/Userland/Applications/PixelPaint/PaletteWidget.h @@ -28,13 +28,16 @@ public: Vector colors(); - static Result, String> load_palette_file(String const&); - static Result save_palette_file(Vector, String const&); + static Result, String> load_palette_fd_and_close(int); + static Result, String> load_palette_path(String const&); + static Result save_palette_fd_and_close(Vector, int); static Vector fallback_colors(); void set_image_editor(ImageEditor&); private: + static Result, String> load_palette_file(Core::File&); + explicit PaletteWidget(); ImageEditor* m_editor { nullptr }; diff --git a/Userland/Applications/PixelPaint/main.cpp b/Userland/Applications/PixelPaint/main.cpp index ee2c9b60b8..f6f95526e5 100644 --- a/Userland/Applications/PixelPaint/main.cpp +++ b/Userland/Applications/PixelPaint/main.cpp @@ -21,10 +21,10 @@ #include #include #include +#include #include #include #include -#include #include #include #include @@ -50,6 +50,45 @@ int main(int argc, char** argv) args_parser.add_positional_argument(image_file, "Image file to open", "path", Core::ArgsParser::Required::No); args_parser.parse(argc, argv); + String file_to_edit_full_path; + + if (image_file) { + file_to_edit_full_path = Core::File::absolute_path(image_file); + VERIFY(!file_to_edit_full_path.is_empty()); + if (Core::File::exists(file_to_edit_full_path)) { + dbgln("unveil for: {}", file_to_edit_full_path); + if (unveil(file_to_edit_full_path.characters(), "r") < 0) { + perror("unveil"); + return 1; + } + } + } + + if (unveil("/res", "r") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/tmp/portal/clipboard", "rw") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/tmp/portal/filesystemaccess", "rw") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/tmp/portal/image", "rw") < 0) { + perror("unveil"); + return 1; + } + + if (unveil(nullptr, nullptr) < 0) { + perror("unveil"); + return 1; + } + auto app_icon = GUI::Icon::default_icon("app-pixel-paint"); auto window = GUI::Window::construct(); @@ -111,7 +150,7 @@ int main(int argc, char** argv) window); auto open_image_file = [&](auto& path) { - auto image_or_error = PixelPaint::Image::try_create_from_file(path); + auto image_or_error = PixelPaint::Image::try_create_from_path(path); if (image_or_error.is_error()) { GUI::MessageBox::show_error(window, String::formatted("Unable to open file: {}", path)); return; @@ -121,26 +160,38 @@ int main(int argc, char** argv) layer_list_widget.set_image(&image); }; - auto open_image_action = GUI::CommonActions::make_open_action([&](auto&) { - Optional open_path = GUI::FilePicker::get_open_filepath(window); - if (!open_path.has_value()) + auto open_image_fd = [&](int fd, auto& path) { + auto image_or_error = PixelPaint::Image::try_create_from_fd_and_close(fd, path); + if (image_or_error.is_error()) { + GUI::MessageBox::show_error(window, String::formatted("Unable to open file: {}, {}", path, image_or_error.error())); return; - open_image_file(open_path.value()); + } + auto& image = *image_or_error.value(); + create_new_editor(image); + layer_list_widget.set_image(&image); + }; + + auto open_image_action = GUI::CommonActions::make_open_action([&](auto&) { + auto result = FileSystemAccessClient::Client::the().open_file(window->window_id()); + if (result.error != 0) + return; + + open_image_fd(*result.fd, *result.chosen_file); }); auto save_image_as_action = GUI::CommonActions::make_save_as_action([&](auto&) { auto* editor = current_image_editor(); if (!editor) return; - auto save_path = GUI::FilePicker::get_save_filepath(window, "untitled", "pp"); - if (!save_path.has_value()) + auto save_result = FileSystemAccessClient::Client::the().save_file(window->window_id(), "untitled", "pp"); + if (save_result.error != 0) return; - auto result = editor->image().write_to_file(save_path.value()); + auto result = editor->image().write_to_fd_and_close(*save_result.fd); if (result.is_error()) { - GUI::MessageBox::show_error(window, String::formatted("Could not save {}: {}", save_path.value(), result.error())); + GUI::MessageBox::show_error(window, String::formatted("Could not save {}: {}", *save_result.chosen_file, result.error())); return; } - editor->image().set_path(save_path.value()); + editor->image().set_path(*save_result.chosen_file); }); auto& file_menu = window->add_menu("&File"); @@ -155,11 +206,11 @@ int main(int argc, char** argv) auto* editor = current_image_editor(); if (!editor) return; - auto save_path = GUI::FilePicker::get_save_filepath(window, "untitled", "bmp"); - if (!save_path.has_value()) + auto save_result = FileSystemAccessClient::Client::the().save_file(window->window_id(), "untitled", "bmp"); + if (save_result.error != 0) return; auto preserve_alpha_channel = GUI::MessageBox::show(window, "Do you wish to preserve transparency?", "Preserve transparency?", GUI::MessageBox::Type::Question, GUI::MessageBox::InputType::YesNo); - auto result = editor->image().export_bmp_to_file(save_path.value(), preserve_alpha_channel == GUI::MessageBox::ExecYes); + auto result = editor->image().export_bmp_to_fd_and_close(*save_result.fd, preserve_alpha_channel == GUI::MessageBox::ExecYes); if (result.is_error()) GUI::MessageBox::show_error(window, String::formatted("Export to BMP failed: {}", result.error())); }, @@ -168,11 +219,11 @@ int main(int argc, char** argv) GUI::Action::create( "As &PNG", [&](auto&) { auto* editor = current_image_editor(); - auto save_path = GUI::FilePicker::get_save_filepath(window, "untitled", "png"); - if (!save_path.has_value()) + auto save_result = FileSystemAccessClient::Client::the().save_file(window->window_id(), "untitled", "png"); + if (save_result.error != 0) return; auto preserve_alpha_channel = GUI::MessageBox::show(window, "Do you wish to preserve transparency?", "Preserve transparency?", GUI::MessageBox::Type::Question, GUI::MessageBox::InputType::YesNo); - auto result = editor->image().export_png_to_file(save_path.value(), preserve_alpha_channel == GUI::MessageBox::ExecYes); + auto result = editor->image().export_png_to_fd_and_close(*save_result.fd, preserve_alpha_channel == GUI::MessageBox::ExecYes); if (result.is_error()) GUI::MessageBox::show_error(window, String::formatted("Export to PNG failed: {}", result.error())); }, @@ -270,11 +321,11 @@ int main(int argc, char** argv) window)); edit_menu.add_action(GUI::Action::create( "&Load Color Palette", [&](auto&) { - auto open_path = GUI::FilePicker::get_open_filepath(window, "Load Color Palette"); - if (!open_path.has_value()) + auto open_result = FileSystemAccessClient::Client::the().open_file(window->window_id(), "Load Color Palette"); + if (open_result.error != 0) return; - auto result = PixelPaint::PaletteWidget::load_palette_file(open_path.value()); + auto result = PixelPaint::PaletteWidget::load_palette_fd_and_close(*open_result.fd); if (result.is_error()) { GUI::MessageBox::show_error(window, String::formatted("Loading color palette failed: {}", result.error())); return; @@ -285,11 +336,11 @@ int main(int argc, char** argv) window)); edit_menu.add_action(GUI::Action::create( "Sa&ve Color Palette", [&](auto&) { - auto save_path = GUI::FilePicker::get_save_filepath(window, "untitled", "palette"); - if (!save_path.has_value()) + auto save_result = FileSystemAccessClient::Client::the().save_file(window->window_id(), "untitled", "palette"); + if (save_result.error != 0) return; - auto result = PixelPaint::PaletteWidget::save_palette_file(palette_widget.colors(), save_path.value()); + auto result = PixelPaint::PaletteWidget::save_palette_fd_and_close(palette_widget.colors(), *save_result.fd); if (result.is_error()) GUI::MessageBox::show_error(window, String::formatted("Writing color palette failed: {}", result.error())); }, @@ -693,9 +744,8 @@ int main(int argc, char** argv) }); }; - auto image_file_real_path = Core::File::real_path_for(image_file); - if (Core::File::exists(image_file_real_path)) { - open_image_file(image_file_real_path); + if (Core::File::exists(file_to_edit_full_path)) { + open_image_file(file_to_edit_full_path); } else { auto image = PixelPaint::Image::try_create_with_size({ 480, 360 });