Skip to content
Closed
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
6 changes: 3 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ declare module '@react-pdf/renderer' {

type SourceObject =
| string
| { data: Buffer; format: 'png' | 'jpg' }
| { data: Buffer; format: 'png' | 'jpg' | 'gif' }
| { uri: string; method: HTTPMethod; body: any; headers: any };

interface BaseImageProps extends NodeProps {
Expand All @@ -114,8 +114,8 @@ declare module '@react-pdf/renderer' {
type ImageProps = ImageWithSrcProp | ImageWithSourceProp;

/**
* A React component for displaying network or local (Node only) JPG or
* PNG images, as well as base64 encoded image strings.
* A React component for displaying network or local (Node only) JPG,
* PNG or GIF images, as well as base64 encoded image strings.
*/
class Image extends React.Component<ImageProps> {}

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@
"@textkit/pdf-renderer": "^0.1.13",
"@textkit/text-decoration-engine": "^0.1.10",
"blob-stream": "^0.1.3",
"blob-to-buffer": "^1.2.8",
"cross-fetch": "^3.0.1",
"emoji-regex": "^7.0.3",
"hyphen": "^1.1.1",
"is-url": "^1.2.4",
"jpegasus": "^1.9.54",
"lodash.groupby": "^4.6.0",
"lodash.isfunction": "^3.0.9",
"lodash.merge": "^4.6.1",
Expand All @@ -46,6 +48,8 @@
"page-wrapping": "^1.1.0",
"react": "^16.8.3",
"react-reconciler": "^0.20.1",
"sharp": "^0.22.0",
"to-arraybuffer": "^1.0.1",
"yoga-layout-prebuilt": "^1.9.3"
},
"devDependencies": {
Expand Down
69 changes: 69 additions & 0 deletions src/utils/gif.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* global File */

import jpegasus from 'jpegasus';
import toArrayBuffer from 'to-arraybuffer';
import toBufferCb from 'blob-to-buffer';
import sharp from 'sharp';
import JPEG from './jpeg';
import PNG from './png';

const toBuffer = blob =>
new Promise((resolve, reject) =>
toBufferCb(blob, (err, buffer) => (err ? reject(err) : resolve(buffer))),
);

const renderGIF = async data => {
if (!GIF.isValid(data)) {
throw new TypeError(
'Image passed to GIF decoder appears not to be in GIF format',
);
}

if (!BROWSER) {
const pngBuffer = await sharp(data).toBuffer();
return new PNG(pngBuffer);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an issue with the way Rollup bundles this — because it doesn’t do dead-code elimination exactly, the browser builds still import the server-side sharp module, so — even though the BROWSER global is set to true — when it’s consuming the build Webpack processes the import sharp from 'sharp' statement and gives this error:

WARNING: Critical dependency: the request of a dependency is an expression (expand for details)  
WARNING in ./node_modules/sharp/lib/libvips.js 51:22-69
Critical dependency: the request of a dependency is an expression
@ ./node_modules/sharp/lib/constructor.js
@ ./node_modules/sharp/lib/index.js
@ ./node_modules/@react-pdf/renderer/dist/react-pdf.browser.es.js
@ ./src/client/components/xocs/jsx-components.pdf.js
@ ./src/client/clientRender.pdf.js
@ ./src/client/index.pdf.js
@ multi (webpack)-dev-server/client?http://localhost:8082 (webpack)/hot/dev-server.js ./src/client/index.pdf ./src/client/style.scss

WARNING in ./node_modules/sharp/lib/libvips.js 52:23-70
Critical dependency: the request of a dependency is an expression
@ ./node_modules/sharp/lib/constructor.js
@ ./node_modules/sharp/lib/index.js
@ ./node_modules/@react-pdf/renderer/dist/react-pdf.browser.es.js
@ ./src/client/components/xocs/jsx-components.pdf.js
@ ./src/client/clientRender.pdf.js
@ ./src/client/index.pdf.js
@ multi (webpack)-dev-server/client?http://localhost:8082 (webpack)/hot/dev-server.js ./src/client/index.pdf ./src/client/style.scss
ERROR: Module not found: Error: Can't resolve 'child_process' (expand for details)  
ERROR in ./node_modules/detect-libc/lib/detect-libc.js
Module not found: Error: Can't resolve 'child_process' in './node_modules/detect-libc/lib'
@ ./node_modules/detect-libc/lib/detect-libc.js 4:16-40
@ ./node_modules/sharp/lib/platform.js
@ ./node_modules/sharp/lib/libvips.js
@ ./node_modules/sharp/lib/constructor.js
@ ./node_modules/sharp/lib/index.js
@ ./node_modules/@react-pdf/renderer/dist/react-pdf.browser.es.js
@ ./src/client/components/xocs/jsx-components.pdf.js
@ ./src/client/clientRender.pdf.js
@ ./src/client/index.pdf.js
@ multi (webpack)-dev-server/client?http://localhost:8082 (webpack)/hot/dev-server.js ./src/client/index.pdf ./src/client/style.scss

ERROR in ./node_modules/sharp/lib/libvips.js
Module not found: Error: Can't resolve 'child_process' in './node_modules/sharp/lib'
@ ./node_modules/sharp/lib/libvips.js 6:18-42
@ ./node_modules/sharp/lib/constructor.js
@ ./node_modules/sharp/lib/index.js
@ ./node_modules/@react-pdf/renderer/dist/react-pdf.browser.es.js
@ ./src/client/components/xocs/jsx-components.pdf.js
@ ./src/client/clientRender.pdf.js
@ ./src/client/index.pdf.js
@ multi (webpack)-dev-server/client?http://localhost:8082 (webpack)/hot/dev-server.js ./src/client/index.pdf ./src/client/style.scss

ERROR in ./node_modules/bindings/bindings.js
Module not found: Error: Can't resolve 'fs' in './node_modules/bindings'
@ ./node_modules/bindings/bindings.js 5:9-22
@ ./node_modules/sharp/lib/constructor.js
@ ./node_modules/sharp/lib/index.js
@ ./node_modules/@react-pdf/renderer/dist/react-pdf.browser.es.js
@ ./src/client/components/xocs/jsx-components.pdf.js
@ ./src/client/clientRender.pdf.js
@ ./src/client/index.pdf.js
@ multi (webpack)-dev-server/client?http://localhost:8082 (webpack)/hot/dev-server.js ./src/client/index.pdf ./src/client/style.scss

ERROR in ./node_modules/detect-libc/lib/detect-libc.js
Module not found: Error: Can't resolve 'fs' in './node_modules/detect-libc/lib'
@ ./node_modules/detect-libc/lib/detect-libc.js 5:18-31
@ ./node_modules/sharp/lib/platform.js
@ ./node_modules/sharp/lib/libvips.js
@ ./node_modules/sharp/lib/constructor.js
@ ./node_modules/sharp/lib/index.js
@ ./node_modules/@react-pdf/renderer/dist/react-pdf.browser.es.js
@ ./src/client/components/xocs/jsx-components.pdf.js
@ ./src/client/clientRender.pdf.js
@ ./src/client/index.pdf.js
@ multi (webpack)-dev-server/client?http://localhost:8082 (webpack)/hot/dev-server.js ./src/client/index.pdf ./src/client/style.scss

ERROR in ./node_modules/sharp/lib/libvips.js
Module not found: Error: Can't resolve 'fs' in './node_modules/sharp/lib'
@ ./node_modules/sharp/lib/libvips.js 3:11-24
@ ./node_modules/sharp/lib/constructor.js
@ ./node_modules/sharp/lib/index.js
@ ./node_modules/@react-pdf/renderer/dist/react-pdf.browser.es.js
@ ./src/client/components/xocs/jsx-components.pdf.js
@ ./src/client/clientRender.pdf.js
@ ./src/client/index.pdf.js
@ multi (webpack)-dev-server/client?http://localhost:8082 (webpack)/hot/dev-server.js ./src/client/index.pdf ./src/client/style.scss

ERROR in ./node_modules/sharp/build/Release/sharp.node 1:0
Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type.
(Source code omitted for this binary file)
@ ./node_modules/sharp/lib/input.js 5:14-52
@ ./node_modules/sharp/lib/index.js
@ ./node_modules/@react-pdf/renderer/dist/react-pdf.browser.es.js
@ ./src/client/components/xocs/jsx-components.pdf.js
@ ./src/client/clientRender.pdf.js
@ ./src/client/index.pdf.js
@ multi (webpack)-dev-server/client?http://localhost:8082 (webpack)/hot/dev-server.js ./src/client/index.pdf ./src/client/style.scss
ℹ 「wdm」: Failed to compile.

Do you know of a way to shake the non-BROWSER code completely out of Rollup’s browser builds? :)


const jpegBlob = await jpegasus.compress(
new File([toArrayBuffer(data)], 'image.gif', {
type: 'image/gif',
}),
{
quality: 0.8,
},
);
const jpegBuffer = await toBuffer(jpegBlob);
return new JPEG(jpegBuffer);
};

// Instantiate a “thenable” object — an object that behaves like a Promise
// https://samdanielson.com/blog/2016/09/15/subclassing-promises-not.html
// by implementing the Promise prototype’s public methods, and applying them to a real Promise
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#Promise_prototype
class GIF {
constructor(data) {
this.imagePromise = renderGIF(data);
}

then(...args) {
return this.imagePromise.then.apply(this.imagePromise, args);
}

catch(...args) {
return this.imagePromise.catch.apply(this.imagePromise, args);
}

finally(...args) {
return this.imagePromise.finally.apply(this.imagePromise, args);
}
}

GIF.isValid = function(data) {
try {
return data[0] === 71 && data[1] === 73 && data[2] === 70;
} catch (e) {
return false;
}
};

export default GIF;
34 changes: 21 additions & 13 deletions src/utils/image.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 GIF from './gif';
import createCache from './cache';

export const IMAGE_CACHE = createCache({ limit: 30 });
Expand Down Expand Up @@ -74,7 +75,9 @@ 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 === 'gif'
);
};

const guessFormat = buffer => {
Expand All @@ -84,13 +87,15 @@ const guessFormat = buffer => {
format = 'jpg';
} else if (PNG.isValid(buffer)) {
format = 'png';
} else if (GIF.isValid(buffer)) {
format = 'gif';
}

return format;
};

const isCompatibleBase64 = ({ uri }) =>
/^data:image\/[a-zA-Z]*;base64,[^"]*/g.test(uri);
/^data:image\/[a-zA-Z]*;base64,[^"]*/.test(uri);

function getImage(body, extension) {
switch (extension.toLowerCase()) {
Expand All @@ -99,38 +104,36 @@ function getImage(body, extension) {
return new JPEG(body);
case 'png':
return new PNG(body);
case 'gif':
return new GIF(body);
default:
return null;
}
}

const resolveBase64Image = ({ uri }) => {
const match = /^data:image\/([a-zA-Z]*);base64,([^"]*)/g.exec(uri);
const format = match[1];
const data = match[2];
const resolveBase64Image = async ({ uri }) => {
const [, format, data] = /^data:image\/([a-zA-Z]*);base64,([^"]*)/.exec(uri);

if (!isValidFormat(format)) {
throw new Error(`Base64 image invalid format: ${format}`);
}

return new Promise(resolve => {
return resolve(getImage(Buffer.from(data, 'base64'), format));
});
return getImage(Buffer.from(data, 'base64'), format);
};

const resolveImageFromData = src => {
const resolveImageFromData = async src => {
if (src.data && src.format) {
return new Promise(resolve => resolve(getImage(src.data, src.format)));
return getImage(src.data, src.format);
}

throw new Error(`Invalid data given for local file: ${JSON.stringify(src)}`);
};

const resolveBufferImage = buffer => {
const resolveBufferImage = async buffer => {
const format = guessFormat(buffer);

if (format) {
return new Promise(resolve => resolve(getImage(buffer, format)));
return getImage(buffer, format);
}
};

Expand All @@ -147,11 +150,16 @@ const getImageFormat = body => {

const isJpg = body[0] === 255 && body[1] === 216 && body[2] === 255;

// based on https://github.com/sindresorhus/file-type/blob/master/index.js#L65
const isGif = body[0] === 71 && body[1] === 73 && body[2] === 70;

let extension = '';
if (isPng) {
extension = 'png';
} else if (isJpg) {
extension = 'jpg';
} else if (isGif) {
extension = 'gif';
} else {
throw new Error('Not valid image extension');
}
Expand Down
Binary file added tests/assets/test.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions tests/gif.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import fs from 'fs';
import path from 'path';
import GIF from '../src/utils/gif';

const localGIFImage = fs.readFileSync(path.join(__dirname, 'assets/test.gif'));
const localPNGImage = fs.readFileSync(path.join(__dirname, 'assets/test.png'));

describe('gif', () => {
test('should return true for valid gif images', () => {
expect(GIF.isValid(localGIFImage)).toBeTruthy();
});

test('should return false for non gif images', () => {
expect(GIF.isValid(localPNGImage)).toBeFalsy();
});

test('should return false for empty argument', () => {
expect(GIF.isValid()).toBeFalsy();
});

test('should return false for non buffer argument', () => {
expect(GIF.isValid('some data')).toBeFalsy();
});
});
26 changes: 26 additions & 0 deletions tests/image.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ let dummyRoot;

const jpgImageUrl = 'https://react-pdf.org/static/images/quijote1.jpg';
const pngImageUrl = 'https://react-pdf.org/static/images/quijote2.png';
const gifImageUrl = 'https://react-pdf.org/static/images/quijote3.gif';
const localJPGImage = fs.readFileSync(path.join(__dirname, 'assets/test.jpg'));
const localPNGImage = fs.readFileSync(path.join(__dirname, 'assets/test.png'));
const localGIFImage = fs.readFileSync(path.join(__dirname, 'assets/test.gif'));

jest.mock('../src/utils/warning');

Expand Down Expand Up @@ -141,6 +143,19 @@ describe('Image', () => {
expect(dummyRoot.instance.image.mock.calls[0][0]).toBe(image.image.data);
});

test('Should render a gif image over http', async () => {
fetch.once(localGIFImage);

const image = new Image(dummyRoot, { src: gifImageUrl });

await image.fetch();
await image.render();

expect(image.image.data).toBeTruthy();
expect(dummyRoot.instance.image.mock.calls).toHaveLength(1);
expect(dummyRoot.instance.image.mock.calls[0][0]).toBe(image.image.data);
});

test('Should render a remote image from src object', async () => {
fetch.once(localJPGImage);

Expand Down Expand Up @@ -254,6 +269,17 @@ describe('Image', () => {
expect(dummyRoot.instance.image.mock.calls[0][0]).toBe(image.image.data);
});

/* test('Should render a buffer gif image', async () => {

@DuncanMacWeb DuncanMacWeb Mar 14, 2019

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test currently makes the test runner hang, which is why I’ve commented it out. Haven’t worked out why it’s hanging yet 🤔

const image = new Image(dummyRoot, { src: localGIFImage });

await image.fetch();
await image.render();

expect(image.image.data).toBeTruthy();
expect(dummyRoot.instance.image.mock.calls).toHaveLength(1);
expect(dummyRoot.instance.image.mock.calls[0][0]).toBe(image.image.data);
}); */

test('Should correctly resolve objectFit styles given a flat `style` prop', async () => {
const resolveObjectFitSpy = jest.spyOn(objectFit, 'resolveObjectFit');
resolveObjectFitSpy.mockClear();
Expand Down
Loading