Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for webp #2463

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion packages/image/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>",
"homepage": "https://github.com/diegomura/react-pdf#readme",
"main": "./lib/index.cjs.js",
Expand Down
19 changes: 18 additions & 1 deletion packages/image/src/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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 => {
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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');
}
Expand Down
60 changes: 60 additions & 0 deletions packages/image/src/webp.js
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 6 additions & 0 deletions packages/pdfkit/src/image.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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.');
}
}
Expand Down
73 changes: 73 additions & 0 deletions packages/pdfkit/src/image/webp.js
Original file line number Diff line number Diff line change
@@ -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;