// ***************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * // * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * // ***************************************************************************** #include "icon_loader.h" #include //includes #include #include #include #include #include using namespace zen; using namespace fff; namespace { ImageHolder copyToImageHolder(const GdkPixbuf& pixBuf, int maxSize) //throw SysError { //see: https://developer.gnome.org/gdk-pixbuf/stable/gdk-pixbuf-The-GdkPixbuf-Structure.html if (const GdkColorspace cs = ::gdk_pixbuf_get_colorspace(&pixBuf); cs != GDK_COLORSPACE_RGB) throw SysError(formatSystemError("gdk_pixbuf_get_colorspace", L"", L"Unexpected color space: " + numberTo(static_cast(cs)))); if (const int bitCount = ::gdk_pixbuf_get_bits_per_sample(&pixBuf); bitCount != 8) throw SysError(formatSystemError("gdk_pixbuf_get_bits_per_sample", L"", L"Unexpected bits per sample: " + numberTo(bitCount))); const int channels = ::gdk_pixbuf_get_n_channels(&pixBuf); if (channels != 3 && channels != 4) throw SysError(formatSystemError("gdk_pixbuf_get_n_channels", L"", L"Unexpected number of channels: " + numberTo(channels))); assert(::gdk_pixbuf_get_has_alpha(&pixBuf) == (channels == 4)); const unsigned char* srcBytes = ::gdk_pixbuf_read_pixels(&pixBuf); const int srcWidth = ::gdk_pixbuf_get_width (&pixBuf); const int srcHeight = ::gdk_pixbuf_get_height(&pixBuf); const int srcStride = ::gdk_pixbuf_get_rowstride(&pixBuf); //don't stretch small images, shrink large ones only! int targetWidth = srcWidth; int targetHeight = srcHeight; const int maxExtent = std::max(targetWidth, targetHeight); if (maxSize < maxExtent) { targetWidth = numeric::intDivRound(targetWidth * maxSize, maxExtent); targetHeight = numeric::intDivRound(targetHeight * maxSize, maxExtent); } ImageHolder imgOut(targetWidth, targetHeight, true /*withAlpha*/); unsigned char* rgbOut = imgOut.getRgb(); unsigned char* alphaOut = imgOut.getAlpha(); if (srcWidth != targetWidth || srcHeight != targetHeight) { const auto pixRead = [srcBytes, srcStride, channels](int x, int y) { const unsigned char* const ptr = srcBytes + y * srcStride + channels * x; //RGB(A) byte order const int a = channels == 4 ? ptr[3] : 255; return [a, ptr](int channel) { if (channel == 3) return a; return ptr[channel] * a; //Limitation: alpha should be applied in gamma-decoded linear RGB space: https://ssp.impulsetrain.com/gamma-premult.html }; }; const auto pixWrite = [rgbOut, alphaOut](const auto& interpolate) mutable { const double a = interpolate(3); if (a <= 0.0) { *alphaOut++ = 0; rgbOut += 3; //don't care about color } else { *alphaOut++ = xbrz::byteRound(a); *rgbOut++ = xbrz::byteRound(interpolate(0) / a); //r *rgbOut++ = xbrz::byteRound(interpolate(1) / a); //g *rgbOut++ = xbrz::byteRound(interpolate(2) / a); //b } }; xbrz::bilinearScale(pixRead, //PixReader pixRead srcWidth, //int srcWidth srcHeight, //int srcHeight pixWrite, //PixWriter pixWrite targetWidth, //int trgWidth targetHeight, //int trgHeight 0, //int yFirst targetHeight); //int yLast #if 0 //alternative: but does it support alpha-channel? GdkPixbuf* pixBufShrinked = ::gdk_pixbuf_scale_simple(pixBuf, //const GdkPixbuf* src targetWidth, //int dest_width targetHeight, //int dest_height GDK_INTERP_BILINEAR); //GdkInterpType interp_type if (!pixBufShrinked) throw SysError(formatSystemError("gdk_pixbuf_scale_simple", L"", L"Not enough memory.")); ZEN_ON_SCOPE_EXIT(::g_object_unref(pixBufShrinked)); #endif } else //perf: going overboard? for (int y = 0; y < srcHeight; ++y) for (int x = 0; x < srcWidth; ++x) { const unsigned char* const ptr = srcBytes + y * srcStride + channels * x; //RGB(A) byte order *alphaOut++ = channels == 4 ? ptr[3] : 255; *rgbOut++ = ptr[0]; *rgbOut++ = ptr[1]; *rgbOut++ = ptr[2]; } return imgOut; } ImageHolder imageHolderFromGicon(GIcon& gicon, int maxSize) //throw SysError { assert(runningOnMainThread()); //GTK is NOT thread safe!!! assert(!G_IS_FILE_ICON(&gicon) && !G_IS_LOADABLE_ICON(&gicon)); //see comment in image_holder.h => icon loading must not block main thread GtkIconTheme* const defaultTheme = ::gtk_icon_theme_get_default(); //not owned! ASSERT_SYSERROR(defaultTheme); //no more error details GtkIconInfo* const iconInfo = ::gtk_icon_theme_lookup_by_gicon(defaultTheme, //GtkIconTheme* icon_theme &gicon, //GIcon* icon maxSize, //gint size GTK_ICON_LOOKUP_USE_BUILTIN); //GtkIconLookupFlags flags if (!iconInfo) throw SysError(formatSystemError("gtk_icon_theme_lookup_by_gicon", L"", L"Icon not available.")); #if GTK_MAJOR_VERSION == 2 ZEN_ON_SCOPE_EXIT(::gtk_icon_info_free(iconInfo)); #elif GTK_MAJOR_VERSION == 3 ZEN_ON_SCOPE_EXIT(::g_object_unref(iconInfo)); #else #error unknown GTK version! #endif GError* error = nullptr; ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error)); GdkPixbuf* const pixBuf = ::gtk_icon_info_load_icon(iconInfo, &error); if (!pixBuf) throw SysError(formatGlibError("gtk_icon_info_load_icon", error)); ZEN_ON_SCOPE_EXIT(::g_object_unref(pixBuf)); //we may have to shrink (e.g. GTK3, openSUSE): "an icon theme may have icons that differ slightly from their nominal sizes" return copyToImageHolder(*pixBuf, maxSize); //throw SysError } } FileIconHolder fff::getIconByTemplatePath(const Zstring& templatePath, int maxSize) //throw SysError { //uses full file name, e.g. "AUTHORS" has own mime type on Linux: gchar* const contentType = ::g_content_type_guess(templatePath.c_str(), //const gchar* filename nullptr, //const guchar* data 0, //gsize data_size nullptr); //gboolean* result_uncertain if (!contentType) throw SysError(formatSystemError("g_content_type_guess(" + copyStringTo(templatePath) + ')', L"", L"Unknown content type.")); ZEN_ON_SCOPE_EXIT(::g_free(contentType)); GIcon* const fileIcon = ::g_content_type_get_icon(contentType); if (!fileIcon) throw SysError(formatSystemError("g_content_type_get_icon(" + std::string(contentType) + ')', L"", L"Icon not available.")); return FileIconHolder(fileIcon /*pass ownership*/, maxSize); } FileIconHolder fff::genericFileIcon(int maxSize) //throw SysError { //we're called by getDisplayIcon()! -> avoid endless recursion! GIcon* const fileIcon = ::g_content_type_get_icon("text/plain"); if (!fileIcon) throw SysError(formatSystemError("g_content_type_get_icon(text/plain)", L"", L"Icon not available.")); return FileIconHolder(fileIcon /*pass ownership*/, maxSize); } FileIconHolder fff::genericDirIcon(int maxSize) //throw SysError { GIcon* const dirIcon = ::g_content_type_get_icon("inode/directory"); //should contain fallback to GTK_STOCK_DIRECTORY ("gtk-directory") if (!dirIcon) throw SysError(formatSystemError("g_content_type_get_icon(inode/directory)", L"", L"Icon not available.")); return FileIconHolder(dirIcon /*pass ownership*/, maxSize); } FileIconHolder fff::getTrashIcon(int maxSize) //throw SysError { GIcon* const trashIcon = ::g_themed_icon_new("user-trash-full"); //empty: "user-trash" if (!trashIcon) throw SysError(formatSystemError("g_themed_icon_new(user-trash-full)", L"", L"Icon not available.")); return FileIconHolder(trashIcon /*pass ownership*/, maxSize); } FileIconHolder fff::getFileManagerIcon(int maxSize) //throw SysError { GIcon* const trashIcon = ::g_themed_icon_new("system-file-manager"); if (!trashIcon) throw SysError(formatSystemError("g_themed_icon_new(system-file-manager)", L"", L"Icon not available.")); return FileIconHolder(trashIcon /*pass ownership*/, maxSize); } FileIconHolder fff::getFileIcon(const Zstring& filePath, int maxSize) //throw SysError { GFile* file = ::g_file_new_for_path(filePath.c_str()); //documented to "never fail" ZEN_ON_SCOPE_EXIT(::g_object_unref(file)); GError* error = nullptr; ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error)); GFileInfo* const fileInfo = ::g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_ICON, G_FILE_QUERY_INFO_NONE, nullptr /*cancellable*/, &error); if (!fileInfo) throw SysError(formatGlibError("g_file_query_info", error)); ZEN_ON_SCOPE_EXIT(::g_object_unref(fileInfo)); GIcon* const gicon = ::g_file_info_get_icon(fileInfo); //no ownership transfer! if (!gicon) throw SysError(formatSystemError("g_file_info_get_icon", L"", L"Icon not available.")); //https://github.com/GNOME/gtk/blob/master/gtk/gtkicontheme.c#L4082 if (G_IS_FILE_ICON(gicon) || G_IS_LOADABLE_ICON(gicon)) //see comment in image_holder.h throw SysError(L"Icon loading might block main thread."); //shouldn't be a problem for native file systems -> G_IS_THEMED_ICON(gicon) //the remaining icon types won't block! assert(GDK_IS_PIXBUF(gicon) || G_IS_THEMED_ICON(gicon) || G_IS_EMBLEMED_ICON(gicon)); g_object_ref(gicon); /*macro!*/ //pass ownership return FileIconHolder(gicon, maxSize); // } ImageHolder fff::getThumbnailImage(const Zstring& filePath, int maxSize) //throw SysError { struct stat fileInfo = {}; if (::stat(filePath.c_str(), &fileInfo) != 0) THROW_LAST_SYS_ERROR("stat"); if (!S_ISREG(fileInfo.st_mode)) //skip blocking file types, e.g. named pipes, see file_io.cpp throw SysError(_("Unsupported item type.") + L" [" + printNumber(L"0%06o", fileInfo.st_mode & S_IFMT) + L']'); GError* error = nullptr; ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error)); GdkPixbuf* const pixBuf = ::gdk_pixbuf_new_from_file(filePath.c_str(), &error); if (!pixBuf) throw SysError(formatGlibError("gdk_pixbuf_new_from_file", error)); ZEN_ON_SCOPE_EXIT(::g_object_unref(pixBuf)); return copyToImageHolder(*pixBuf, maxSize); //throw SysError } wxImage fff::extractWxImage(ImageHolder&& ih) { assert(runningOnMainThread()); if (!ih.getRgb()) return wxNullImage; wxImage img(ih.getWidth(), ih.getHeight(), ih.releaseRgb(), false /*static_data*/); //pass ownership if (ih.getAlpha()) img.SetAlpha(ih.releaseAlpha(), false /*static_data*/); else { assert(false); img.SetAlpha(); ::memset(img.GetAlpha(), wxIMAGE_ALPHA_OPAQUE, ih.getWidth() * ih.getHeight()); } return img; } wxImage fff::extractWxImage(zen::FileIconHolder&& fih) { assert(runningOnMainThread()); wxImage img; if (GIcon* gicon = fih.gicon.get()) try { img = extractWxImage(imageHolderFromGicon(*gicon, fih.maxSize)); //throw SysError } catch (SysError&) {} //might fail if icon theme is missing a MIME type! fih.gicon.reset(); return img; }