diff --git a/Userland/Applications/FileManager/CMakeLists.txt b/Userland/Applications/FileManager/CMakeLists.txt index baf6f811fc..de1807bba8 100644 --- a/Userland/Applications/FileManager/CMakeLists.txt +++ b/Userland/Applications/FileManager/CMakeLists.txt @@ -9,6 +9,7 @@ compile_gml(FileManagerWindow.gml FileManagerWindowGML.h file_manager_window_gml compile_gml(FileOperationProgress.gml FileOperationProgressGML.h file_operation_progress_gml) compile_gml(PropertiesWindowAudioTab.gml PropertiesWindowAudioTabGML.h properties_window_audio_tab_gml) compile_gml(PropertiesWindowGeneralTab.gml PropertiesWindowGeneralTabGML.h properties_window_general_tab_gml) +compile_gml(PropertiesWindowImageTab.gml PropertiesWindowImageTabGML.h properties_window_image_tab_gml) set(SOURCES DesktopWidget.cpp @@ -24,6 +25,7 @@ set(GENERATED_SOURCES FileOperationProgressGML.h PropertiesWindowAudioTabGML.h PropertiesWindowGeneralTabGML.h + PropertiesWindowImageTabGML.h ) serenity_app(FileManager ICON app-file-manager) diff --git a/Userland/Applications/FileManager/PropertiesWindow.cpp b/Userland/Applications/FileManager/PropertiesWindow.cpp index 2c5bd97956..3b42801f1b 100644 --- a/Userland/Applications/FileManager/PropertiesWindow.cpp +++ b/Userland/Applications/FileManager/PropertiesWindow.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,8 @@ #include #include #include +#include +#include #include #include #include @@ -215,6 +218,9 @@ ErrorOr PropertiesWindow::create_file_type_specific_tabs(GUI::TabWidget& t if (mime_type.starts_with("audio/"sv)) return create_audio_tab(tab_widget, move(mapped_file)); + if (mime_type.starts_with("image/"sv)) + return create_image_tab(tab_widget, move(mapped_file), mime_type); + return {}; } @@ -256,6 +262,64 @@ ErrorOr PropertiesWindow::create_audio_tab(GUI::TabWidget& tab_widget, Non return {}; } +ErrorOr PropertiesWindow::create_image_tab(GUI::TabWidget& tab_widget, NonnullRefPtr mapped_file, StringView mime_type) +{ + auto image_decoder = Gfx::ImageDecoder::try_create_for_raw_bytes(mapped_file->bytes(), mime_type); + if (!image_decoder) + return {}; + + auto tab = TRY(tab_widget.try_add_tab("Image"_short_string)); + TRY(tab->load_from_gml(properties_window_image_tab_gml)); + + tab->find_descendant_of_type_named("image_type")->set_text(TRY(String::from_utf8(mime_type))); + tab->find_descendant_of_type_named("image_size")->set_text(TRY(String::formatted("{} x {}", image_decoder->width(), image_decoder->height()))); + + String animation_text; + if (image_decoder->is_animated()) { + auto loops = image_decoder->loop_count(); + auto frames = image_decoder->frame_count(); + StringBuilder builder; + if (loops == 0) { + TRY(builder.try_append("Loop indefinitely"sv)); + } else if (loops == 1) { + TRY(builder.try_append("Once"sv)); + } else { + TRY(builder.try_appendff("Loop {} times"sv, loops)); + } + TRY(builder.try_appendff(" ({} frames)"sv, frames)); + + animation_text = TRY(builder.to_string()); + } else { + animation_text = "None"_short_string; + } + tab->find_descendant_of_type_named("image_animation")->set_text(move(animation_text)); + + auto hide_icc_group = [&tab](String profile_text) { + tab->find_descendant_of_type_named("image_has_icc_profile")->set_text(profile_text); + tab->find_descendant_of_type_named("image_icc_group")->set_visible(false); + }; + + if (auto embedded_icc_bytes = TRY(image_decoder->icc_data()); embedded_icc_bytes.has_value()) { + auto icc_profile_or_error = Gfx::ICC::Profile::try_load_from_externally_owned_memory(embedded_icc_bytes.value()); + if (icc_profile_or_error.is_error()) { + hide_icc_group(TRY("Present but invalid"_string)); + } else { + auto icc_profile = icc_profile_or_error.release_value(); + + tab->find_descendant_of_type_named("image_has_icc_profile")->set_text(TRY("See below"_string)); + tab->find_descendant_of_type_named("image_icc_profile")->set_text(icc_profile->tag_string_data(Gfx::ICC::profileDescriptionTag).value_or({})); + tab->find_descendant_of_type_named("image_icc_copyright")->set_text(icc_profile->tag_string_data(Gfx::ICC::copyrightTag).value_or({})); + tab->find_descendant_of_type_named("image_icc_color_space")->set_text(TRY(String::from_utf8(data_color_space_name(icc_profile->data_color_space())))); + tab->find_descendant_of_type_named("image_icc_device_class")->set_text(TRY(String::from_utf8((device_class_name(icc_profile->device_class()))))); + } + + } else { + hide_icc_group("None"_short_string); + } + + return {}; +} + void PropertiesWindow::update() { m_icon->set_bitmap(GUI::FileIconProvider::icon_for_path(make_full_path(m_name), m_mode).bitmap_for_size(32)); diff --git a/Userland/Applications/FileManager/PropertiesWindow.h b/Userland/Applications/FileManager/PropertiesWindow.h index 92aad1b460..768236359a 100644 --- a/Userland/Applications/FileManager/PropertiesWindow.h +++ b/Userland/Applications/FileManager/PropertiesWindow.h @@ -31,6 +31,7 @@ private: ErrorOr create_general_tab(GUI::TabWidget&, bool disable_rename); ErrorOr create_file_type_specific_tabs(GUI::TabWidget&); ErrorOr create_audio_tab(GUI::TabWidget&, NonnullRefPtr); + ErrorOr create_image_tab(GUI::TabWidget&, NonnullRefPtr, StringView mime_type); struct PermissionMasks { mode_t read; diff --git a/Userland/Applications/FileManager/PropertiesWindowImageTab.gml b/Userland/Applications/FileManager/PropertiesWindowImageTab.gml new file mode 100644 index 0000000000..25ccb5cf7c --- /dev/null +++ b/Userland/Applications/FileManager/PropertiesWindowImageTab.gml @@ -0,0 +1,171 @@ +@GUI::Widget { + layout: @GUI::VerticalBoxLayout { + margins: [8] + spacing: 12 + } + + @GUI::GroupBox { + title: "Image" + preferred_height: "shrink" + layout: @GUI::VerticalBoxLayout { + margins: [12, 8, 0] + spacing: 2 + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 12 + } + + @GUI::Label { + text: "Type:" + text_alignment: "TopLeft" + fixed_width: 80 + } + + @GUI::Label { + name: "image_type" + text: "JPEG" + text_alignment: "TopLeft" + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 12 + } + + @GUI::Label { + text: "Size:" + text_alignment: "TopLeft" + fixed_width: 80 + } + + @GUI::Label { + name: "image_size" + text: "1920 x 1080" + text_alignment: "TopLeft" + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 12 + } + + @GUI::Label { + text: "Animation:" + text_alignment: "TopLeft" + fixed_width: 80 + } + + @GUI::Label { + name: "image_animation" + text: "Loop indefinitely (12 frames)" + text_alignment: "TopLeft" + text_wrapping: "DontWrap" + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 12 + } + + @GUI::Label { + text: "ICC profile:" + text_alignment: "TopLeft" + fixed_width: 80 + } + + @GUI::Label { + name: "image_has_icc_profile" + text: "See below" + text_alignment: "TopLeft" + } + } + } + + @GUI::GroupBox { + name: "image_icc_group" + title: "ICC Profile" + preferred_height: "shrink" + layout: @GUI::VerticalBoxLayout { + margins: [12, 8, 0] + spacing: 2 + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 12 + } + + @GUI::Label { + text: "Profile:" + text_alignment: "TopLeft" + fixed_width: 80 + } + + @GUI::Label { + name: "image_icc_profile" + text: "e-sRGB" + text_alignment: "TopLeft" + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 12 + } + + @GUI::Label { + text: "Copyright:" + text_alignment: "TopLeft" + fixed_width: 80 + } + + @GUI::Label { + name: "image_icc_copyright" + text: "(c) 1999 Adobe Systems Inc." + text_alignment: "TopLeft" + text_wrapping: "DontWrap" + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 12 + } + + @GUI::Label { + text: "Color space:" + text_alignment: "TopLeft" + fixed_width: 80 + } + + @GUI::Label { + name: "image_icc_color_space" + text: "RGB" + text_alignment: "TopLeft" + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 12 + } + + @GUI::Label { + text: "Device class:" + text_alignment: "TopLeft" + fixed_width: 80 + } + + @GUI::Label { + name: "image_icc_device_class" + text: "DisplayDevice" + text_alignment: "TopLeft" + } + } + } +}