Files
system-taskbar/ding/app/thumbnails.js

495 lines
15 KiB
JavaScript

/* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
};