/* DING: Desktop Icons New Generation for GNOME Shell * * Copyright (C) 2022, 2024 Sundeep Mediratta (smedius@gmail.com) port to work with * gnome desktop 4 * * Code cherry picked from Marco Trevisan for async methods to generate icons. * * Copyright (C) 2021 Sergio Costas (rastersoft@gmail.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import {Cairo, Gdk, GdkPixbuf, GLib, Gio, GnomeDesktop, Poppler} from '../dependencies/gi.js'; export {ThumbnailLoader}; Gio._promisify(GnomeDesktop.DesktopThumbnailFactory.prototype, 'generate_thumbnail_async', 'generate_thumbnail_finish'); Gio._promisify(GnomeDesktop.DesktopThumbnailFactory.prototype, 'create_failed_thumbnail_async', 'create_failed_thumbnail_finish'); Gio._promisify(GnomeDesktop.DesktopThumbnailFactory.prototype, 'save_thumbnail_async', 'save_thumbnail_finish'); const PIXBUF_CONTENT_TYPES = new Set(); GdkPixbuf.Pixbuf .get_formats().forEach(f => PIXBUF_CONTENT_TYPES.add(...f.get_mime_types())); // Max file size for which to attempt thumbnail generation with local code const MAX_FILE_SIZE = 5242880; // Width and height of icons generated by local code const WIDTH = 130; const HEIGHT = 130; const ThumbnailLoader = class { constructor(FileUtils) { this.FileUtils = FileUtils; this._timeoutValue = 5000; this._thumbnailFactory = GnomeDesktop.DesktopThumbnailFactory .new(GnomeDesktop.DesktopThumbnailSize.LARGE); this.standardThumbnailsFolder = GLib.build_filenamev([GLib.get_home_dir(), '.cache/thumbnails']); this.standardThumbnailSubFolders = ['large', 'normal']; this.gimpSnapThumbnailsFolder = GLib.build_filenamev( [ GLib.get_home_dir(), 'snap/common/gimp', '.cache/thumbnails', ] ); this.gimpFlatPackThumbnailsFolder = GLib.build_filenamev( [ GLib.get_home_dir(), '.var/app/org.gimp.GIMP', 'cache/thumbnails', ] ); this.md5Hasher = GLib.Checksum.new(GLib.ChecksumType.MD5); this.textCoder = new TextEncoder(); } async _generateThumbnail(file, cancellable) { if (!await this.FileUtils.queryExists(file.file)) return null; if (this._thumbnailFactory .has_valid_failed_thumbnail(file.uri, file.modifiedTime) ) return null; if (!await this._createThumbnailAsync(file, cancellable)) { await this._createFailedThumbnailAsync(file, cancellable); return null; } if (cancellable.is_cancelled()) return null; return this._thumbnailFactory.lookup(file.uri, file.modifiedTime); } async _createThumbnailAsync(file, cancellable) { let gotTimeout = false; let timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._timeoutValue, () => { console.log( `Timeout while generating thumbnail for ${file.displayName}` ); timeoutId = 0; gotTimeout = true; cancellable.cancel(); return GLib.SOURCE_REMOVE; }); try { const thumbnailPixbuf = await this._thumbnailFactory .generate_thumbnail_async( file.uri, file.attributeContentType, cancellable ); await this._thumbnailFactory .save_thumbnail_async( thumbnailPixbuf, file.uri, file.modifiedTime, cancellable ) .catch(e => { if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) console.error(`Error saving thumbnail ${e}`); }); return true; } catch (e) { if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { console.error(e, 'Error creating thumbnail with thumbnailFactory: ' + `${e.message}` ); } return await this._createFallBackThumbnailAsync( file, gotTimeout && cancellable.is_cancelled() ? null : cancellable ); } finally { if (timeoutId) GLib.source_remove(timeoutId); } } async _createFallBackThumbnailAsync(file, cancellable) { const thumbnailPixbuf = this._createThumbnailLocally(file, cancellable); if (thumbnailPixbuf !== null) { await this._thumbnailFactory .save_thumbnail_async( thumbnailPixbuf, file.uri, file.modifiedTime, cancellable ) .catch(e => { if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) console.error(`Error saving thumbnail ${e}`); }); return true; } return false; } async _createFailedThumbnailAsync(file, cancellable) { try { await this._thumbnailFactory.create_failed_thumbnail_async( file.uri, file.modifiedTime, cancellable ); } catch (e) { if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { console.error(e, `Error while creating failed thumbnail: ${e.message}` ); } } } _createThumbnailLocally(file, cancellable) { let thumbnailPixbuf = null; if (file.fileSize < MAX_FILE_SIZE) { const contentType = file.attributeContentType; try { if (PIXBUF_CONTENT_TYPES.has(contentType)) thumbnailPixbuf = this._loadImageAsIcon(file, cancellable); else if ( contentType === 'application/pdf' || contentType === 'x-pdf' ) thumbnailPixbuf = this._loadPdfAsIcon(file, cancellable); } catch (e) { if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) throw e; console.error(e, `Error while generating icon image: ${e.message}` ); } } return thumbnailPixbuf; } _loadPdfAsIcon(file, cancellable) { let thumbnailPixbuf = null; try { // Assume no password const password = null; const popplerDocument = Poppler.Document.new_from_gfile( file.file, password, cancellable ); if (!popplerDocument) return thumbnailPixbuf; const firstPage = popplerDocument.get_page(0); if (!firstPage) return thumbnailPixbuf; const [pagewidth, pageheight] = firstPage.get_size(); let width = WIDTH; let height = HEIGHT; const aspectRatio = pagewidth / pageheight; if ((width / height) > aspectRatio) width = height * aspectRatio; else height = width / aspectRatio; const hScale = width / pagewidth; const vScale = height / pageheight; const imageSurface = new Cairo.ImageSurface( Cairo.Format.ARGB32, pagewidth, pageheight ); const ctx = new Cairo.Context(imageSurface); this._drawPdfOn(ctx, firstPage); const scaledSurface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height); const scaledCtx = new Cairo.Context(scaledSurface); scaledCtx.scale(hScale, vScale); scaledCtx.setSourceSurface(imageSurface, 0, 0); scaledCtx.paint(); thumbnailPixbuf = Gdk.pixbuf_get_from_surface(scaledSurface, 0, 0, width, height); ctx.$dispose(); scaledCtx.$dispose(); } catch (e) { if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) throw e; if (!e.matches(Poppler.Error, Poppler.Error.ENCRYPTED)) { console.error(e, `Error creating pdf thumbnail pixbuf ${file.uri}` ); } } return thumbnailPixbuf; } _drawPdfOn(ctx, firstPage) { ctx.setSourceRGBA(1, 1, 1, 1); ctx.save(); ctx.paint(); ctx.restore(); firstPage.render(ctx); ctx.save(); } _loadImageAsIcon(file) { let thumbnailPixbuf = null; try { const pixbuf = GdkPixbuf.Pixbuf.new_from_file(file.path); let width = WIDTH; let height = HEIGHT; const aspectRatio = pixbuf.width / pixbuf.height; if ((width / height) > aspectRatio) width = height * aspectRatio; else height = width / aspectRatio; thumbnailPixbuf = pixbuf.scale_simple( width, height, GdkPixbuf.InterpType.BILINEAR ); return thumbnailPixbuf; } catch (e) { if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) throw e; console.error(e, `Error creating image thumbnail pixbuf ${file.uri}` ); } return thumbnailPixbuf; } /* * ExtraCode to find thumbnail in the thumbnail Folder * Was Used to find GIMP thumbnails, however ThumbnailFactoryNormal * can now find it. * * However to do that you have to start two ThumbnailFactories, * this is simpler and lighter, can be used to search arbitrary folders * for thumbnails in Futre if necessary, not just Subfolders */ _findThumbnail(file, basePath, subFolders = null, cancellable) { if (!basePath) return null; let md5FileUriHash = this._getMD5Hash(file.uri); if (!md5FileUriHash) return null; let thumbnailMD5Name = `${md5FileUriHash}.png`; let thumbnailFilePath = null; let thumbnailFileSearchPath = null; if (subFolders) { for (const subfolder of subFolders) { thumbnailFileSearchPath = GLib .build_filenamev([basePath, subfolder, thumbnailMD5Name]); if ( Gio.File.new_for_path(thumbnailFileSearchPath) .query_exists(cancellable) ) { thumbnailFilePath = thumbnailFileSearchPath; break; } } return thumbnailFilePath; } thumbnailFileSearchPath = GLib.build_filenamev([basePath, thumbnailMD5Name]); if ( Gio.File.new_for_path(thumbnailFileSearchPath) .query_exists(cancellable) ) thumbnailFilePath = thumbnailFileSearchPath; return thumbnailFilePath; } _getMD5Hash(string) { let hashString = null; this.md5Hasher.update(this.textCoder.encode(string)); hashString = this.md5Hasher.get_string(); this.md5Hasher.reset(); return hashString; } canThumbnail(file) { return this._thumbnailFactory .can_thumbnail( file.uri, file.attributeContentType, file.modifiedTime ); } _lookupThumbnail(file, cancellable) { let thumbnail = null; // do searches for only special cases to conserve resources // if (file.attributeContentType === 'image/x-xcf') { // lets do a local search in thumbnails dir, look only in normal // subfolder as we already searched large thumbnail = this._findThumbnail( file, this.standardThumbnailsFolder, ['normal'], cancellable ); if (thumbnail) return thumbnail; // we can now search far and wide in snaps and flatpacks if we want. thumbnail = this._findThumbnail( file, this.gimpSnapThumbnailsFolder, this.standardThumbnailSubFolders, cancellable ); if (!thumbnail) { thumbnail = this._findThumbnail( file, this.gimpFlatPackThumbnailsFolder, this.standardThumbnailSubFolders, cancellable ); } return thumbnail; } return thumbnail; } hasThumbnail(file, cancellable) { let thumbnail = null; thumbnail = this._thumbnailFactory.lookup(file.uri, file.modifiedTime); if (thumbnail) return thumbnail; thumbnail = this._lookupThumbnail(file, cancellable); return thumbnail; } async getThumbnail(file, cancellable) { try { let thumbnail = this.hasThumbnail(file, cancellable); if (!thumbnail && this.canThumbnail(file)) thumbnail = await this._generateThumbnail(file, cancellable); if ( !thumbnail && await this._createFallBackThumbnailAsync(file, cancellable) ) { thumbnail = this._thumbnailFactory.lookup(file.uri, file.modifiedTime); } return thumbnail; } catch (error) { console.log( `Error when asking for a thumbnail for ${file.displayName}:` + ` ${error.message}\n${error.stack}`); } return null; } };