Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions app/back-end/helpers/specs/webp.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { Jimp } = require('jimp');
const Image = require('../../image.js');
const WebpHelper = require('../webp.js');

describe('WebP Helper', function() {
this.timeout(10000);

it('should encode and decode a Jimp bitmap', async function() {
let fixturePath = path.join(__dirname, 'mock-data', 'avatar.png');
let image = await Jimp.read(fixturePath);
let encoded = await WebpHelper.encode(image.bitmap, { quality: 60 });
let decoded = await WebpHelper.decode(encoded);

assert.strictEqual('RIFF', encoded.subarray(0, 4).toString());
assert.strictEqual('WEBP', encoded.subarray(8, 12).toString());
assert.strictEqual(image.bitmap.width, decoded.width);
assert.strictEqual(image.bitmap.height, decoded.height);
assert.strictEqual(decoded.data.length, decoded.width * decoded.height * 4);
});

it('should preserve the WebP destination when Jimp is used', async function() {
let tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'publii-webp-'));
let sourcePath = path.join(__dirname, 'mock-data', 'avatar.png');
let destinationPath = path.join(tempDir, 'avatar-small.webp');
let fallbackDestinationPath = path.join(tempDir, 'avatar-small.png');
let image = new Image({
db: null,
appDir: path.join(__dirname, '..', '..', '..'),
sitesDir: tempDir,
appConfig: { resizeEngine: 'jimp' }
}, {
site: 'test',
id: '1',
path: sourcePath
});

try {
let result = await image.processWithJimp({
originalPath: sourcePath,
destinationPath,
fallbackDestinationPath,
sourceExtension: '.png',
format: 'webp',
width: 100,
height: 100,
crop: false,
forceWebp: true,
imagesQuality: 60,
alphaQuality: 100,
webpLossless: false
});

assert.strictEqual(destinationPath, result);
assert.strictEqual(true, fs.existsSync(destinationPath));
assert.strictEqual(false, fs.existsSync(fallbackDestinationPath));
assert.strictEqual('WEBP', fs.readFileSync(destinationPath).subarray(8, 12).toString());
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
});
79 changes: 79 additions & 0 deletions app/back-end/helpers/webp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const fs = require('fs');
const path = require('path');

let encoderPromise = null;
let decoderPromise = null;

function getWebpPackageDir() {
return path.dirname(require.resolve('@jsquash/webp/package.json'));
}

function loadWasm(relativePath) {
let wasmPath = path.join(getWebpPackageDir(), relativePath);
let wasmBuffer = fs.readFileSync(wasmPath);

return WebAssembly.compile(wasmBuffer);
}

async function getEncoder() {
if (!encoderPromise) {
encoderPromise = (async () => {
let encoder = await import('@jsquash/webp/encode.js');
let encoderDir = path.join('codec', 'enc');
let simdWasmPath = path.join(encoderDir, 'webp_enc_simd.wasm');
let simdWasm = fs.readFileSync(path.join(getWebpPackageDir(), simdWasmPath));
let wasmPath = WebAssembly.validate(simdWasm)
? simdWasmPath
: path.join(encoderDir, 'webp_enc.wasm');

await encoder.init(await loadWasm(wasmPath));

return encoder.default;
})();
}

return encoderPromise;
}

async function getDecoder() {
if (!decoderPromise) {
decoderPromise = (async () => {
let decoder = await import('@jsquash/webp/decode.js');
let wasmPath = path.join('codec', 'dec', 'webp_dec.wasm');

await decoder.init(await loadWasm(wasmPath));

return decoder.default;
})();
}

return decoderPromise;
}

async function encode(bitmap, options) {
let encoder = await getEncoder();
let data = {
...bitmap,
data: new Uint8ClampedArray(bitmap.data)
};
let result = await encoder(data, options);

return Buffer.from(result);
}

async function decode(buffer) {
let decoder = await getDecoder();
let data = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
let result = await decoder(data);

return {
data: Buffer.from(result.data),
width: result.width,
height: result.height
};
}

module.exports = {
encode,
decode
};
40 changes: 29 additions & 11 deletions app/back-end/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const Utils = require('./helpers/utils.js');
const slug = require('./helpers/slug');
const { Jimp } = require('jimp');
const sharpQueue = require('./helpers/sharp-queue.js');
const WebpHelper = require('./helpers/webp.js');
// Default config
const defaultAstCurrentSiteConfig = require('./../config/AST.currentSite.config');

Expand Down Expand Up @@ -269,7 +270,7 @@ class Image extends Model {
webpLossless = !!siteConfig.advanced.webpLossless;
}

if (siteConfig?.advanced?.forceWebp && !this.shouldUseJimp()) {
if (siteConfig?.advanced?.forceWebp) {
forceWebp = !!siteConfig.advanced.forceWebp;
}

Expand Down Expand Up @@ -341,7 +342,7 @@ class Image extends Model {
let destinationPath = fallbackDestinationPath;
let shouldBeChangedToWebp = false;

if (!this.shouldUseJimp() && ['.png', '.jpg', '.jpeg'].indexOf(extLower) > -1) {
if (['.png', '.jpg', '.jpeg'].indexOf(extLower) > -1) {
shouldBeChangedToWebp = true;
}

Expand Down Expand Up @@ -428,20 +429,29 @@ class Image extends Model {
async processWithJimp(job) {
const {
originalPath,
destinationPath,
fallbackDestinationPath,
sourceExtension,
format,
width,
height,
crop,
imagesQuality
forceWebp,
imagesQuality,
alphaQuality,
webpLossless
} = job;

let image;

if (sourceExtension === '.webp') {
console.log('jimp cannot process webp source, skipping:', originalPath);
return;
}
let source = await fs.readFile(originalPath);
let bitmap = await WebpHelper.decode(source);

let image = await Jimp.read(originalPath);
image = Jimp.fromBitmap(bitmap);
} else {
image = await Jimp.read(originalPath);
}

if (crop) {
if (width === null || height === null) {
Expand Down Expand Up @@ -469,6 +479,18 @@ class Image extends Model {
}
}

if (format === 'webp' || forceWebp) {
let buffer = await WebpHelper.encode(image.bitmap, {
quality: imagesQuality,
alpha_quality: alphaQuality,
lossless: webpLossless ? 1 : 0
});

await fs.writeFile(destinationPath, buffer);

return destinationPath;
}

await image.write(fallbackDestinationPath, { quality: imagesQuality });

return fallbackDestinationPath;
Expand Down Expand Up @@ -511,10 +533,6 @@ class Image extends Model {
allowedImageExtension(extension) {
let allowedExtensions = ['.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG', '.webp', '.WEBP'];

if (this.shouldUseJimp()) {
allowedExtensions = ['.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG'];
}

return allowedExtensions.indexOf(extension) > -1;
}

Expand Down
6 changes: 2 additions & 4 deletions app/default-files/default-languages/en-gb/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -719,8 +719,7 @@
"continueSync": "Continue and sync",
"continueSyncNoRemoteFiles": "Publii is unable to find remote files list on your server. If you will continue the sync process, ALL generated website files will be uploaded to your server. You can cancel the sync process to check the reason of your issue and then try again.",
"convertToWebp": "Convert to WebP format",
"convertToWebpInfo": "Enable this option if you want to convert all JPG, JPEG and PNG images to WebP format. Once enabled, your image thumbnails will need to be regenerated. Note, this feature works only if you’re using the Sharp library for generating thumbnails.",
"convertToWebpJimpWarning": "This option does not work with the currently used image resizing engine, Jimp. Instead, switch to Sharp in the app's settings.",
"convertToWebpInfo": "Enable this option if you want to convert all JPG, JPEG and PNG images to WebP format. Once enabled, your image thumbnails will need to be regenerated.",
"cookieGroups": "Cookie Groups",
"cookieBanner": "Cookie Banner",
"cookieBasic": "Basic",
Expand Down Expand Up @@ -855,9 +854,8 @@
"howToPrepareYourThemeForGDPRInfo": "Enabling this option will display a cookie banner on your website. Please read the <a href=\"https://getpublii.com/docs/gdpr-cookie-banner-configuration.html\" target=\"_blank\" rel=\"noopener noreferrer\">GDPR Cookie Banner Configuration</a> article for more information on how to configure the banner correctly. ",
"howYtNoCookiesWorks": "When enabled, the YouTube domain for the embed URL e.g. 'www.youtube.com' will be automatically changed to 'www.youtube-nocookie.com', to prevent the embedded player from personalising the YouTube browsing experience of the visitors, and also ensures that no information is used to customise advertising shown to the visitor outside of your site. Learn more about Enhanced Privacy Mode by visiting <a href=\"https://support.google.com/youtube/answer/171780?hl=en#zippy=%2Cturn-on-privacy-enhanced-mode\">YouTube help page</a>.",
"howVimeoNoTrackWorks": "When enabled, the additional dnt=1 parameter will be added to the Vimeo embed URL (player.vimeo.com/video) to prevent the embedded player from tracking any session data, including all cookies and analytics. Learn more about Vimeo player parameters by visiting <a href=\"https://vimeo.zendesk.com/hc/en-us/articles/360001494447-Player-parameters-overview\">Vimeo help page</a>.",
"imageResizeEngineInfo": "The Sharp resize engine is much faster than Jimp, but can cause issues with some images. If you are encountering problems when creating or regenerating thumbnails, please try switching to the Jimp resize engine. Should you wish to use WebP images, then you’ll need to use the Sharp resize engine.",
"imageResizeEngineInfo": "The Sharp resize engine is much faster than Jimp, but can cause issues with some images. If you are encountering problems when creating or regenerating thumbnails, please try switching to the Jimp resize engine. Both engines support WebP images.",
"imagesResizeEngine": "Images resize engine:",
"imageResizeEngineWarning": "Jimp resize engine and the option to convert images to WebP format won't work properly. Make sure that all of your websites has the “Website Speed / Convert to WebP format” disabled, and make sure to regenerate thumbnails for consistency.",
"indentSize": "Indent size (spaces)",
"internalPage": "Internal page",
"leaveBlankForDefaultBackupsDir": "Leave blank to use the default backups directory",
Expand Down
6 changes: 2 additions & 4 deletions app/default-files/default-languages/pl/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -718,8 +718,7 @@
"continueSync": "Kontynuuj i synchronizuj",
"continueSyncNoRemoteFiles": "Publii nie może znaleźć listy plików na twoim serwerze. Jeśli będziesz kontynuować proces synchronizacji, WSZYSTKIE wygenerowane pliki strony internetowej zostaną przesłane na twój serwer. Możesz anulować proces synchronizacji, aby sprawdzić przyczynę problemu, a następnie spróbować ponownie.",
"convertToWebp": "Konwertuj do WebP",
"convertToWebpInfo": "Włącz tę opcję, jeśli chcesz konwertować wszystkie grafiki w formatach JPG, JPEG oraz PNG do formatu WebP. Po włączeniu tej opcji konieczna jest ponowna regeneracja miniaturek. Opcja ta zadziała tylko gdy używasz biblioteki Sharp do tworzenia miniaturek.",
"convertToWebpJimpWarning": "Ta opcja nie działa z obecnie używanym silnikiem tworzenia miniaturek - Jimp. Proszę użyć silnika Sharp aby móc używać tej opcji.",
"convertToWebpInfo": "Włącz tę opcję, jeśli chcesz konwertować wszystkie grafiki w formatach JPG, JPEG oraz PNG do formatu WebP. Po włączeniu tej opcji konieczna jest ponowna regeneracja miniaturek.",
"cookieGroups": "Grupy plików cookies",
"cookieBanner": "Banner cookies",
"cookieBasic": "Podstawowy",
Expand Down Expand Up @@ -854,9 +853,8 @@
"howToPrepareYourThemeForGDPRInfo": "Włączenie tej opcji spowoduje wyświetlenie banera plików cookie w Twojej witrynie. Aby uzyskać więcej informacji na temat prawidłowej konfiguracji banera zapoznaj się z następującym artykułem <a href=\"https://getpublii.com/docs/gdpr-cookie-banner-configuration.html\" target=\"_blank\" rel=\"noopener noreferrer\">GDPR Cookie Banner Configuration</a> ",
"howYtNoCookiesWorks": "Po włączeniu tej opcji, domena adresu URL umieszczonego filmu 'www.youtube.com' zostanie automatycznie zmieniona na 'www.youtube-nocookie.com', aby uniemożliwić personalizowanie sposobu przeglądania YouTube ani w tym odtwarzaczu, ani w samym serwisie, a także zapewni, że żadne dane nie są wykorzystywane do dostosowywania reklam wyświetlanych widzom poza Twoją witryną. Dowiedz się więcej o rozszerzonym trybie prywatności odwiedzając <a href=\"https://support.google.com/youtube/answer/171780?hl=en#zippy=%2Cturn-on-privacy-enhanced-mode\">stronę pomocy YouTube.</a>.",
"howVimeoNoTrackWorks": "Po włączeniu tej opcji, dodatkowy parametr dnt=1 zostanie dodany do adresu URL umieszczonego odtwarzacza Vimeo (player.vimeo.com/video), aby uniemożliwić śledzenie jakichkolwiek danych sesji, w tym wszystkich plików cookie i danych analitycznych. Dowiedz się więcej o parametrach odtwarzacza Vimeo odwiedzając <a href=\"https://vimeo.zendesk.com/hc/en-us/articles/360001494447-Player-parameters-overview\">stronę pomocy Vimeo</a>.",
"imageResizeEngineInfo": "Silnik zmiany rozmiaru obrazów The Sharp jest znacznie szybszy niż Jimp, ale może powodować problemy z niektórymi obrazami. Jeśli napotkasz problemy podczas tworzenia lub ponownego generowania miniatur, spróbuj przełączyć się na mechanizm zmiany rozmiaru Jimp. Jeśli chcesz używać obrazów WebP, musisz użyć mechanizmu zmiany rozmiaru Sharp.",
"imageResizeEngineInfo": "Silnik zmiany rozmiaru obrazów Sharp jest znacznie szybszy niż Jimp, ale może powodować problemy z niektórymi obrazami. Jeśli napotkasz problemy podczas tworzenia lub ponownego generowania miniatur, spróbuj przełączyć się na mechanizm zmiany rozmiaru Jimp. Oba silniki obsługują obrazy WebP.",
"imagesResizeEngine": "Silnik zmiany rozmiaru obrazów:",
"imageResizeEngineWarning": "Jeśli używany jest silnik Jimp, opcja wykorzystania grafik WebP nie będzie działać poprawnie. Upewnij się, że wszystkie Twoje strony mają opcję 'Szybkość strony / Konwertuj do WebP' wyłączoną i wygeneruj miniatury ponownie aby zapewnić poprawne wyświetlanie grafik.",
"indentSize": "Rozmiar wcięć (spacje)",
"internalPage": "Strona wewnętrzna",
"leaveBlankForDefaultBackupsDir": "Pozostaw puste, aby użyć domyślnego katalogu kopii zapasowych",
Expand Down
16 changes: 16 additions & 0 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@aws-sdk/client-s3": "3.1054.0",
"@gitbeaker/rest": "43.8.0",
"@google-cloud/storage": "7.19.0",
"@jsquash/webp": "1.5.0",
"@octokit/rest": "22.0.0",
"adm-zip": "0.5.10",
"archiver": "5.3.1",
Expand Down
10 changes: 0 additions & 10 deletions app/src/components/AppSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,6 @@
id="images-resize-engine"
:items="imageResizeEngines"
v-model="imageResizeEnginesSelected"></dropdown>
<p
v-if="showWebpWarning"
slot="note"
class="msg msg-icon msg-alert">
<icon name="warning" customWidth="28" customHeight="28" />
<span>{{ $t('settings.imageResizeEngineWarning') }}</span>
</p>
<small
slot="note"
class="note">
Expand Down Expand Up @@ -571,9 +564,6 @@ export default {
'var(--font-serif)': this.$t('settings.editorFontFamilySerif')
};
},
showWebpWarning () {
return this.imageResizeEnginesSelected === 'jimp';
},
isSitesLocationExists () {
return this.sitesLocationExists;
},
Expand Down
8 changes: 0 additions & 8 deletions app/src/components/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1825,14 +1825,6 @@
v-model="advanced.forceWebp"
slot="field" />

<p
v-if="advanced.forceWebp && $store.state.app.config.resizeEngine === 'jimp'"
slot="note"
class="msg msg-icon msg-alert">
<icon name="warning" customWidth="28" customHeight="28" />
<span>{{ $t('settings.convertToWebpJimpWarning') }}</span>
</p>

<small
slot="note"
class="note">
Expand Down