From 1c2f2797575a5758c672ab1d20ca783a815499c1 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sun, 17 Dec 2023 03:54:56 -0800 Subject: [PATCH 1/3] feat: add webp support feature --- packages/image/src/resolve.js | 19 ++++++++++- packages/image/src/webp.js | 60 +++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 packages/image/src/webp.js diff --git a/packages/image/src/resolve.js b/packages/image/src/resolve.js index 6bee8270b..6de2f2fa0 100644 --- a/packages/image/src/resolve.js +++ b/packages/image/src/resolve.js @@ -5,6 +5,7 @@ import fetch from 'cross-fetch'; import PNG from './png'; import JPEG from './jpeg'; +import WEBP from './webp'; import createCache from './cache'; export const IMAGE_CACHE = createCache({ limit: 30 }); @@ -56,7 +57,7 @@ const fetchRemoteFile = async (uri, options) => { const isValidFormat = format => { const lower = format.toLowerCase(); - return lower === 'jpg' || lower === 'jpeg' || lower === 'png'; + return lower === 'jpg' || lower === 'jpeg' || lower === 'png' || lower === 'webp'; }; const guessFormat = buffer => { @@ -66,6 +67,8 @@ const guessFormat = buffer => { format = 'jpg'; } else if (PNG.isValid(buffer)) { format = 'png'; + } else if (WEBP.isValid(buffer)){ + format = 'webp'; } return format; @@ -81,6 +84,8 @@ function getImage(body, extension) { return new JPEG(body); case 'png': return new PNG(body); + case 'webp': + return new WEBP(body); default: return null; } @@ -131,11 +136,23 @@ const getImageFormat = body => { const isJpg = body[0] === 255 && body[1] === 216 && body[2] === 255; + const isWebp = + body[0] === 82 && + body[1] === 73 && + body[2] === 70 && + body[3] === 70 && + body[8] === 87 && + body[9] === 69 && + body[10] === 66 && + body[11] === 80; + let extension = ''; if (isPng) { extension = 'png'; } else if (isJpg) { extension = 'jpg'; + } else if (isWebp) { + extension = 'webp'; } else { throw new Error('Not valid image extension'); } diff --git a/packages/image/src/webp.js b/packages/image/src/webp.js new file mode 100644 index 000000000..44b638db5 --- /dev/null +++ b/packages/image/src/webp.js @@ -0,0 +1,60 @@ +class WEBP { + data = null; + width = null; + height = null; + + constructor(data) { + this.data = data; + + // WebP signature 'RIFF' at the start and 'WEBP' at offset 8 + if (!this.isValid()) { + throw new Error('Invalid WebP format'); + } + + // Extract width and height from the VP8/VP8L/VP8X chunk + this.extractDimensions(); + } + + isValid() { + const riffHeader = this.data.toString('ascii', 0, 4); + const webpHeader = this.data.toString('ascii', 8, 12); + return riffHeader === 'RIFF' && webpHeader === 'WEBP'; + } + + extractDimensions() { + // This is a simplified example. Actual dimension extraction will depend on + // the VP8/VP8L/VP8X chunk structure + // For VP8 (lossy), dimensions are in bytes 26-29 + // For VP8L (lossless), dimensions are in bytes 21-24 + // For VP8X (extended), dimensions are in bytes 24-29 + + const chunkHeader = this.data.toString('ascii', 12, 16); + let pos; + switch (chunkHeader) { + case 'VP8 ': + pos = 26; + break; + case 'VP8L': + pos = 21; + break; + case 'VP8X': + pos = 24; + break; + default: + throw new Error('Unknown WebP format'); + } + + this.width = this.data.readUInt16LE(pos); + this.height = this.data.readUInt16LE(pos + 2); + } +} + +WEBP.isValid = function(data) { + if (!data || !Buffer.isBuffer(data)) { + return false; + } + const webp = new WebP(data); + return webp.isValid(); +}; + +export default WEBP; From 13c3f72ba9c597df1f5e4b3c4809c440733ca234 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sun, 17 Dec 2023 04:23:26 -0800 Subject: [PATCH 2/3] feat: update description & hotfix --- packages/image/package.json | 2 +- packages/image/src/webp.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/image/package.json b/packages/image/package.json index b9d8ddad6..da207cf22 100644 --- a/packages/image/package.json +++ b/packages/image/package.json @@ -2,7 +2,7 @@ "name": "@react-pdf/image", "version": "2.2.2", "license": "MIT", - "description": "Parses the images in png or jpeg format for react-pdf document", + "description": "Parses the images in png, webp or jpeg format for react-pdf document", "author": "Diego Muracciole ", "homepage": "https://github.com/diegomura/react-pdf#readme", "main": "./lib/index.cjs.js", diff --git a/packages/image/src/webp.js b/packages/image/src/webp.js index 44b638db5..e3787d2eb 100644 --- a/packages/image/src/webp.js +++ b/packages/image/src/webp.js @@ -53,7 +53,7 @@ WEBP.isValid = function(data) { if (!data || !Buffer.isBuffer(data)) { return false; } - const webp = new WebP(data); + const webp = new WEBP(data); return webp.isValid(); }; From 190defdb2f31d69af2a576b7818983110977c2f1 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sun, 17 Dec 2023 14:27:22 -0800 Subject: [PATCH 3/3] feat: update pdfkit to support webp --- packages/pdfkit/src/image.js | 6 +++ packages/pdfkit/src/image/webp.js | 73 +++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 packages/pdfkit/src/image/webp.js diff --git a/packages/pdfkit/src/image.js b/packages/pdfkit/src/image.js index b79d49d35..b5f0a580e 100644 --- a/packages/pdfkit/src/image.js +++ b/packages/pdfkit/src/image.js @@ -1,6 +1,7 @@ import fs from 'fs'; import JPEG from './image/jpeg'; import PNG from './image/png'; +import WEBP from './image/webp'; class PDFImage { static open(src, label) { @@ -27,6 +28,11 @@ class PDFImage { return new PNG(data, label); } + if (data[0] === 0x52 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x46 && + data[8] === 0x57 && data[9] === 0x45 && data[10] === 0x42 && data[11] === 0x50) { + return new WEBP(data, label); + } + throw new Error('Unknown image format.'); } } diff --git a/packages/pdfkit/src/image/webp.js b/packages/pdfkit/src/image/webp.js new file mode 100644 index 000000000..2274017c2 --- /dev/null +++ b/packages/pdfkit/src/image/webp.js @@ -0,0 +1,73 @@ +class WEBP { + data = null; + width = null; + height = null; + + constructor(data, label) { + this.data = data; + this.label = label; + + // WebP signature 'RIFF' at the start and 'WEBP' at offset 8 + if (!this.isValid()) { + throw new Error('Invalid WebP format'); + } + + // Extract width and height from the VP8/VP8L/VP8X chunk + this.extractDimensions(); + } + + isValid() { + const riffHeader = this.data.toString('ascii', 0, 4); + const webpHeader = this.data.toString('ascii', 8, 12); + return riffHeader === 'RIFF' && webpHeader === 'WEBP'; + } + + extractDimensions() { + // This is a simplified example. Actual dimension extraction will depend on + // the VP8/VP8L/VP8X chunk structure + // For VP8 (lossy), dimensions are in bytes 26-29 + // For VP8L (lossless), dimensions are in bytes 21-24 + // For VP8X (extended), dimensions are in bytes 24-29 + + const chunkHeader = this.data.toString('ascii', 12, 16); + let pos; + switch (chunkHeader) { + case 'VP8 ': + pos = 26; + break; + case 'VP8L': + pos = 21; + break; + case 'VP8X': + pos = 24; + break; + default: + throw new Error('Unknown WebP format'); + } + + this.width = this.data.readUInt16LE(pos); + this.height = this.data.readUInt16LE(pos + 2); + } + + embed(document) { + if (this.obj) { + return; + } + + this.obj = document.ref({ + Type: 'XObject', + Subtype: 'Image', + Width: this.width, + Height: this.height, + ColorSpace: 'DeviceRGB', + Filter: 'DCTDecode' + }); + + this.obj.end(this.data); + + // free memory + this.data = null; + } +} + +export default WEBP;