diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa5b4272..79dc840ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Add support for PDF/A-1b, PDF/A-1a, PDF/A-2b, PDF/A-2a, PDF/A-3b, PDF/A-3a - Update crypto-js to v4.2.0 (properly fix security issue) +- Add support for EXIF orientation on JPEG images (#626 and #1353) + ### [v0.13.0] - 2021-10-24 - Add tiling pattern support diff --git a/docs/images.md b/docs/images.md index d95676bd9..2e1723b78 100644 --- a/docs/images.md +++ b/docs/images.md @@ -7,21 +7,22 @@ rendered at the current point in the text flow (below the last line of text). Otherwise, it is positioned absolutely at the specified point. The image will be scaled according to the following options. -* Neither `width` or `height` provided - image is rendered at full size -* `width` provided but not `height` - image is scaled proportionally to fit in the provided `width` -* `height` provided but not `width` - image is scaled proportionally to fit in the provided `height` -* Both `width` and `height` provided - image is stretched to the dimensions provided -* `scale` factor provided - image is scaled proportionally by the provided scale factor -* `fit` array provided - image is scaled proportionally to fit within the passed width and height -* `cover` array provided - image is scaled proportionally to completely cover the rectangle defined by the passed width and height -* `link` - a URL to link this image to (shortcut to create an annotation) -* `goTo` - go to anchor (shortcut to create an annotation) -* `destination` - create anchor to this image +- Neither `width` or `height` provided - image is rendered at full size +- `width` provided but not `height` - image is scaled proportionally to fit in the provided `width` +- `height` provided but not `width` - image is scaled proportionally to fit in the provided `height` +- Both `width` and `height` provided - image is stretched to the dimensions provided +- `scale` factor provided - image is scaled proportionally by the provided scale factor +- `fit` array provided - image is scaled proportionally to fit within the passed width and height +- `cover` array provided - image is scaled proportionally to completely cover the rectangle defined by the passed width and height +- `link` - a URL to link this image to (shortcut to create an annotation) +- `goTo` - go to anchor (shortcut to create an annotation) +- `destination` - create anchor to this image +- `ignoreOrientation` - (true/false) ignore JPEG EXIF orientation. By default, images with JPEG EXIF orientation are properly rotated and/or flipped. Defaults to `false`, unless `ignoreOrientation` option set to `true` when creating the `PDFDocument` object (e.g. `new PDFDocument({ignoreOrientation: true})`) When a `fit` or `cover` array is provided, PDFKit accepts these additional options: -* `align` - horizontally align the image, the possible values are `'left'`, `'center'` and `'right'` -* `valign` - vertically align the image, the possible values are `'top'`, `'center'` and `'bottom'` +- `align` - horizontally align the image, the possible values are `'left'`, `'center'` and `'right'` +- `valign` - vertically align the image, the possible values are `'top'`, `'center'` and `'bottom'` Here is an example showing some of these options. @@ -48,11 +49,11 @@ Here is an example showing some of these options. .rect(430, 15, 100, 100).stroke() .text('Centered', 430, 0); -* * * +--- This example produces the following output: -![0](images/images.png "400") +![0](images/images.png '400') That is all there is to adding images to your PDF documents with PDFKit. Now let's look at adding outlines. diff --git a/lib/image/jpeg.js b/lib/image/jpeg.js index 46e7b729b..4d556c85c 100644 --- a/lib/image/jpeg.js +++ b/lib/image/jpeg.js @@ -1,3 +1,5 @@ +import exif from 'jpeg-exif'; + const MARKERS = [ 0xffc0, 0xffc1, @@ -31,6 +33,9 @@ class JPEG { throw 'SOI not found in JPEG'; } + // Parse the EXIF orientation + this.orientation = exif.fromBuffer(this.data).Orientation || 1; + let pos = 2; while (pos < this.data.length) { marker = this.data.readUInt16BE(pos); diff --git a/lib/mixins/images.js b/lib/mixins/images.js index 37fb898b1..f2ead6731 100644 --- a/lib/mixins/images.js +++ b/lib/mixins/images.js @@ -7,12 +7,17 @@ export default { }, image(src, x, y, options = {}) { - let bh, bp, bw, image, ip, left, left1; + let bh, bp, bw, image, ip, left, left1, rotateAngle, originX, originY; if (typeof x === 'object') { options = x; x = null; } + // Ignore orientation based on document options or image options + const ignoreOrientation = + options.ignoreOrientation || + (options.ignoreOrientation !== false && this.options.ignoreOrientation); + x = (left = x != null ? x : options.x) != null ? left : this.x; y = (left1 = y != null ? y : options.y) != null ? left1 : this.y; @@ -36,24 +41,31 @@ export default { this.page.xobjects[image.label] = image.obj; } - let w = options.width || image.width; - let h = options.height || image.height; + let { width, height } = image; + + // If EXIF orientation calls for it, swap width and height + if (!ignoreOrientation && image.orientation > 4) { + [width, height] = [height, width]; + } + + let w = options.width || width; + let h = options.height || height; if (options.width && !options.height) { - const wp = w / image.width; - w = image.width * wp; - h = image.height * wp; + const wp = w / width; + w = width * wp; + h = height * wp; } else if (options.height && !options.width) { - const hp = h / image.height; - w = image.width * hp; - h = image.height * hp; + const hp = h / height; + w = width * hp; + h = height * hp; } else if (options.scale) { - w = image.width * options.scale; - h = image.height * options.scale; + w = width * options.scale; + h = height * options.scale; } else if (options.fit) { [bw, bh] = options.fit; bp = bw / bh; - ip = image.width / image.height; + ip = width / height; if (ip > bp) { w = bw; h = bw / ip; @@ -64,7 +76,7 @@ export default { } else if (options.cover) { [bw, bh] = options.cover; bp = bw / bh; - ip = image.width / image.height; + ip = width / height; if (ip > bp) { h = bh; w = bh * ip; @@ -88,6 +100,91 @@ export default { } } + if (!ignoreOrientation) { + switch (image.orientation) { + // No orientation (need to flip image, though, because of the default transform matrix on the document) + default: + case 1: + h = -h; + y -= h; + + rotateAngle = 0; + break; + // Flip Horizontal + case 2: + w = -w; + h = -h; + x -= w; + y -= h; + + rotateAngle = 0; + break; + // Rotate 180 degrees + case 3: + originX = x; + originY = y; + + h = -h; + x -= w; + + rotateAngle = 180; + break; + // Flip vertical + case 4: + // Do nothing, image will be flipped + + break; + // Flip horizontally and rotate 270 degrees CW + case 5: + originX = x; + originY = y; + + [w, h] = [h, w]; + y -= h; + + rotateAngle = 90; + break; + // Rotate 90 degrees CW + case 6: + originX = x; + originY = y; + + [w, h] = [h, w]; + h = -h; + + rotateAngle = 90; + break; + // Flip horizontally and rotate 90 degrees CW + case 7: + originX = x; + originY = y; + + [w, h] = [h, w]; + h = -h; + w = -w; + x -= w; + + rotateAngle = 90; + break; + // Rotate 270 degrees CW + case 8: + originX = x; + originY = y; + + [w, h] = [h, w]; + h = -h; + x -= w; + y -= h; + + rotateAngle = -90; + break; + } + } else { + h = -h; + y -= h; + rotateAngle = 0; + } + // create link annotations if the link option is given if (options.link != null) { this.link(x, y, w, h, options.link); @@ -105,7 +202,14 @@ export default { } this.save(); - this.transform(w, 0, 0, -h, x, y + h); + + if (rotateAngle) { + this.rotate(rotateAngle, { + origin: [originX, originY] + }); + } + + this.transform(w, 0, 0, h, x, y); this.addContent(`/${image.label} Do`); this.restore(); diff --git a/package.json b/package.json index fad9ef529..1b2ec51c5 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "dependencies": { "crypto-js": "^4.2.0", "fontkit": "^1.8.1", + "jpeg-exif": "^1.1.4", "linebreak": "^1.0.2", "png-js": "^1.0.0" }, diff --git a/rollup.config.js b/rollup.config.js index 54a194cbe..a86ad9135 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -11,7 +11,8 @@ const external = [ 'linebreak', 'png-js', 'crypto-js', - 'saslprep' + 'saslprep', + 'jpeg-exif', ]; export default [ diff --git a/tests/images/orientation-1.jpeg b/tests/images/orientation-1.jpeg new file mode 100644 index 000000000..015c5f177 Binary files /dev/null and b/tests/images/orientation-1.jpeg differ diff --git a/tests/images/orientation-2.jpeg b/tests/images/orientation-2.jpeg new file mode 100644 index 000000000..2e7fb579a Binary files /dev/null and b/tests/images/orientation-2.jpeg differ diff --git a/tests/images/orientation-3.jpeg b/tests/images/orientation-3.jpeg new file mode 100644 index 000000000..74505cfc1 Binary files /dev/null and b/tests/images/orientation-3.jpeg differ diff --git a/tests/images/orientation-4.jpeg b/tests/images/orientation-4.jpeg new file mode 100644 index 000000000..ea133fbb0 Binary files /dev/null and b/tests/images/orientation-4.jpeg differ diff --git a/tests/images/orientation-5.jpeg b/tests/images/orientation-5.jpeg new file mode 100644 index 000000000..81a8af69b Binary files /dev/null and b/tests/images/orientation-5.jpeg differ diff --git a/tests/images/orientation-6.jpeg b/tests/images/orientation-6.jpeg new file mode 100644 index 000000000..7426aba04 Binary files /dev/null and b/tests/images/orientation-6.jpeg differ diff --git a/tests/images/orientation-7.jpeg b/tests/images/orientation-7.jpeg new file mode 100644 index 000000000..a541d95e7 Binary files /dev/null and b/tests/images/orientation-7.jpeg differ diff --git a/tests/images/orientation-8.jpeg b/tests/images/orientation-8.jpeg new file mode 100644 index 000000000..3f51d2873 Binary files /dev/null and b/tests/images/orientation-8.jpeg differ diff --git a/tests/visual/__image_snapshots__/images-spec-js-images-orientation-1-snap.png b/tests/visual/__image_snapshots__/images-spec-js-images-orientation-1-snap.png new file mode 100644 index 000000000..ae00f393c Binary files /dev/null and b/tests/visual/__image_snapshots__/images-spec-js-images-orientation-1-snap.png differ diff --git a/tests/visual/__image_snapshots__/images-spec-js-images-orientation-document-option-1-snap.png b/tests/visual/__image_snapshots__/images-spec-js-images-orientation-document-option-1-snap.png new file mode 100644 index 000000000..e43a5d29a Binary files /dev/null and b/tests/visual/__image_snapshots__/images-spec-js-images-orientation-document-option-1-snap.png differ diff --git a/tests/visual/__image_snapshots__/images-spec-js-images-orientation-with-cover-and-alignment-1-snap.png b/tests/visual/__image_snapshots__/images-spec-js-images-orientation-with-cover-and-alignment-1-snap.png new file mode 100644 index 000000000..dedd2aed0 Binary files /dev/null and b/tests/visual/__image_snapshots__/images-spec-js-images-orientation-with-cover-and-alignment-1-snap.png differ diff --git a/tests/visual/__image_snapshots__/images-spec-js-images-orientation-with-fit-and-alignment-1-snap.png b/tests/visual/__image_snapshots__/images-spec-js-images-orientation-with-fit-and-alignment-1-snap.png new file mode 100644 index 000000000..422f17dfb Binary files /dev/null and b/tests/visual/__image_snapshots__/images-spec-js-images-orientation-with-fit-and-alignment-1-snap.png differ diff --git a/tests/visual/images.spec.js b/tests/visual/images.spec.js new file mode 100644 index 000000000..419687c53 --- /dev/null +++ b/tests/visual/images.spec.js @@ -0,0 +1,687 @@ +import { runDocTest } from './helpers'; + +describe('images', function() { + test('orientation', function() { + return runDocTest(function(doc) { + doc.font('tests/fonts/Roboto-Regular.ttf'); + doc.fill('black'); + doc.fillColor('black'); + doc.fillOpacity(undefined); + doc.fontSize(12); + + doc.text( + 'EXIF orientation data may be present on some JPEG images.There are 8 exif orientation values:', + 40, + 10, + { + lineBreak: false + } + ); + + doc.text('1 - No orientation needed', 40, 30, { + lineBreak: false + }); + + doc.image('tests/images/orientation-1.jpeg', 40, 44.0625, { + height: 80, + ignoreOrientation: true + }); + + doc.text('(output)', 320, 30, { + lineBreak: false + }); + + doc.image('tests/images/orientation-1.jpeg', 320, 44.0625, { + height: 80 + }); + + doc.text('2 - Flip horizonatal', 40, 124.0625, { + lineBreak: false + }); + + doc.image('tests/images/orientation-2.jpeg', 40, 138.125, { + height: 80, + ignoreOrientation: true + }); + + doc.text('(output)', 320, 124.0625, { + lineBreak: false + }); + + doc.image('tests/images/orientation-2.jpeg', 320, 138.125, { + height: 80 + }); + + doc.text('3 - Rotate 180 degrees', 40, 218.125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-3.jpeg', 40, 232.1875, { + height: 80, + ignoreOrientation: true + }); + + doc.text('(output)', 320, 218.125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-3.jpeg', 320, 232.1875, { + height: 80 + }); + + doc.text('4 - Flip vertically', 40, 312.1875, { + lineBreak: false + }); + + doc.image('tests/images/orientation-4.jpeg', 40, 326.25, { + height: 80, + ignoreOrientation: true + }); + + doc.text('(output)', 320, 312.1875, { + lineBreak: false + }); + + doc.image('tests/images/orientation-4.jpeg', 320, 326.25, { + height: 80 + }); + + doc.text('5 - Flip horizontally and rotate 270 degrees CW', 40, 406.25, { + lineBreak: false + }); + + doc.image('tests/images/orientation-5.jpeg', 40, 420.3125, { + height: 80, + ignoreOrientation: true + }); + + doc.text('(output)', 320, 406.25, { + lineBreak: false + }); + + doc.image('tests/images/orientation-5.jpeg', 320, 420.3125, { + height: 80 + }); + + doc.text('6 - Rotate 90 degrees CW', 40, 500.3125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-6.jpeg', 40, 514.375, { + height: 80, + ignoreOrientation: true + }); + + doc.text('(output)', 320, 500.3125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-6.jpeg', 320, 514.375, { + height: 80 + }); + + doc.text('7 - Flip horizontally and rotate 90 degrees CW', 40, 594.375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-7.jpeg', 40, 608.4375, { + height: 80, + ignoreOrientation: true + }); + + doc.text('(output)', 320, 594.375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-7.jpeg', 320, 608.4375, { + height: 80 + }); + + doc.text('8 - Rotate 270 degrees CW', 40, 688.4375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-8.jpeg', 40, 702.5, { + height: 80, + ignoreOrientation: true + }); + + doc.text('(output)', 320, 688.4375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-8.jpeg', 320, 702.5, { + height: 80 + }); + }); + }); + + test('orientation - with cover and alignment', function() { + return runDocTest(function(doc) { + let options = { + align: 'center', + cover: [60, 60], + valign: 'center' + }; + + doc.font('tests/fonts/Roboto-Regular.ttf'); + doc.fill('black'); + doc.fillColor('black'); + doc.fillOpacity(undefined); + doc.fontSize(12); + + doc.text( + 'Images with EXIF orientation should properly align with fit/cover options:', + 40, + 10, + { + lineBreak: false + } + ); + + // Orientation 1 + doc.text('1 - No orientation needed', 40, 30, { + lineBreak: false + }); + + doc.image('tests/images/orientation-1.jpeg', 40, 44.0625, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 44.0625, 60, 60).stroke('red'); + + doc.text('(output)', 320, 30, { + lineBreak: false + }); + + doc.image('tests/images/orientation-1.jpeg', 320, 44.0625, options); + + doc.rect(320, 44.0625, 60, 60).stroke('red'); + + // Orientation 2 + doc.text('2 - Flip horizonatal', 40, 124.0625, { + lineBreak: false + }); + + doc.image('tests/images/orientation-2.jpeg', 40, 138.125, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 138.125, 60, 60).stroke('red'); + + doc.text('(output)', 320, 124.0625, { + lineBreak: false + }); + + doc.image('tests/images/orientation-2.jpeg', 320, 138.125, options); + + doc.rect(320, 138.125, 60, 60).stroke('red'); + + // Orientation 3 + doc.text('3 - Rotate 180 degrees', 40, 218.125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-3.jpeg', 40, 232.1875, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 232.1875, 60, 60).stroke('red'); + + doc.text('(output)', 320, 218.125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-3.jpeg', 320, 232.1875, options); + + doc.rect(320, 232.1875, 60, 60).stroke('red'); + + // Orientation 4 + doc.text('4 - Flip vertically', 40, 312.1875, { + lineBreak: false + }); + + doc.image('tests/images/orientation-4.jpeg', 40, 326.25, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 326.25, 60, 60).stroke('red'); + + doc.text('(output)', 320, 312.1875, { + lineBreak: false + }); + + doc.image('tests/images/orientation-4.jpeg', 320, 326.25, options); + + doc.rect(320, 326.25, 60, 60).stroke('red'); + + // Orientation 5 + doc.text('5 - Flip horizontally and rotate 270 degrees CW', 40, 406.25, { + lineBreak: false + }); + + doc.image('tests/images/orientation-5.jpeg', 40, 420.3125, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 420.3125, 60, 60).stroke('red'); + + doc.text('(output)', 320, 406.25, { + lineBreak: false + }); + + doc.image('tests/images/orientation-5.jpeg', 320, 420.3125, options); + + doc.rect(320, 420.3125, 60, 60).stroke('red'); + + // Orientation 6 + doc.text('6 - Rotate 90 degrees CW', 40, 500.3125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-6.jpeg', 40, 514.375, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 514.375, 60, 60).stroke('red'); + + doc.text('(output)', 320, 500.3125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-6.jpeg', 320, 514.375, options); + + doc.rect(320, 514.375, 60, 60).stroke('red'); + + // Orientation 7 + doc.text('7 - Flip horizontally and rotate 90 degrees CW', 40, 594.375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-7.jpeg', 40, 608.4375, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 608.4375, 60, 60).stroke('red'); + + doc.text('(output)', 320, 594.375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-7.jpeg', 320, 608.4375, options); + + doc.rect(320, 608.4375, 60, 60).stroke('red'); + + // Orientation 8 + doc.text('8 - Rotate 270 degrees CW', 40, 688.4375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-8.jpeg', 40, 702.5, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 702.5, 60, 60).stroke('red'); + + doc.text('(output)', 320, 688.4375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-8.jpeg', 320, 702.5, options); + + doc.rect(320, 702.5, 60, 60).stroke('red'); + }); + }); + + test('orientation - with fit and alignment', function() { + return runDocTest(function(doc) { + let options = { + align: 'center', + fit: [80, 80], + valign: 'center' + }; + + doc.font('tests/fonts/Roboto-Regular.ttf'); + doc.fill('black'); + doc.fillColor('black'); + doc.fillOpacity(undefined); + doc.fontSize(12); + + doc.text( + 'Images with EXIF orientation should properly align with fit/cover options:', + 40, + 10, + { + lineBreak: false + } + ); + + // Orientation 1 + doc.text('1 - No orientation needed', 40, 30, { + lineBreak: false + }); + + doc.image('tests/images/orientation-1.jpeg', 40, 44.0625, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 44.0625, 80, 80).stroke('red'); + + doc.text('(output)', 320, 30, { + lineBreak: false + }); + + doc.image('tests/images/orientation-1.jpeg', 320, 44.0625, options); + + doc.rect(320, 44.0625, 80, 80).stroke('red'); + + // Orientation 2 + doc.text('2 - Flip horizonatal', 40, 124.0625, { + lineBreak: false + }); + + doc.image('tests/images/orientation-2.jpeg', 40, 138.125, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 138.125, 80, 80).stroke('red'); + + doc.text('(output)', 320, 124.0625, { + lineBreak: false + }); + + doc.image('tests/images/orientation-2.jpeg', 320, 138.125, options); + + doc.rect(320, 138.125, 80, 80).stroke('red'); + + // Orientation 3 + doc.text('3 - Rotate 180 degrees', 40, 218.125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-3.jpeg', 40, 232.1875, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 232.1875, 80, 80).stroke('red'); + + doc.text('(output)', 320, 218.125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-3.jpeg', 320, 232.1875, options); + + doc.rect(320, 232.1875, 80, 80).stroke('red'); + + // Orientation 4 + doc.text('4 - Flip vertically', 40, 312.1875, { + lineBreak: false + }); + + doc.image('tests/images/orientation-4.jpeg', 40, 326.25, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 326.25, 80, 80).stroke('red'); + + doc.text('(output)', 320, 312.1875, { + lineBreak: false + }); + + doc.image('tests/images/orientation-4.jpeg', 320, 326.25, options); + + doc.rect(320, 326.25, 80, 80).stroke('red'); + + // Orientation 5 + doc.text('5 - Flip horizontally and rotate 270 degrees CW', 40, 406.25, { + lineBreak: false + }); + + doc.image('tests/images/orientation-5.jpeg', 40, 420.3125, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 420.3125, 80, 80).stroke('red'); + + doc.text('(output)', 320, 406.25, { + lineBreak: false + }); + + doc.image('tests/images/orientation-5.jpeg', 320, 420.3125, options); + + doc.rect(320, 420.3125, 80, 80).stroke('red'); + + // Orientation 6 + doc.text('6 - Rotate 90 degrees CW', 40, 500.3125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-6.jpeg', 40, 514.375, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 514.375, 80, 80).stroke('red'); + + doc.text('(output)', 320, 500.3125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-6.jpeg', 320, 514.375, options); + + doc.rect(320, 514.375, 80, 80).stroke('red'); + + // Orientation 7 + doc.text('7 - Flip horizontally and rotate 90 degrees CW', 40, 594.375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-7.jpeg', 40, 608.4375, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 608.4375, 80, 80).stroke('red'); + + doc.text('(output)', 320, 594.375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-7.jpeg', 320, 608.4375, options); + + doc.rect(320, 608.4375, 80, 80).stroke('red'); + + // Orientation 8 + doc.text('8 - Rotate 270 degrees CW', 40, 688.4375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-8.jpeg', 40, 702.5, { + ...options, + ignoreOrientation: true + }); + + doc.rect(40, 702.5, 80, 80).stroke('red'); + + doc.text('(output)', 320, 688.4375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-8.jpeg', 320, 702.5, options); + + doc.rect(320, 702.5, 80, 80).stroke('red'); + }); + }); + + test('orientation - document option', function() { + let options = { + ignoreOrientation: true + }; + + return runDocTest(options, function(doc) { + doc.font('tests/fonts/Roboto-Regular.ttf'); + doc.fill('black'); + doc.fillColor('black'); + doc.fillOpacity(undefined); + doc.fontSize(12); + + doc.text( + 'EXIF orientation support can be enabled on the entire PDFDocument:', + 40, + 10, + { + lineBreak: false + } + ); + + doc.text('1 - No orientation needed', 40, 30, { + lineBreak: false + }); + + doc.image('tests/images/orientation-1.jpeg', 40, 44.0625, { + height: 80 + }); + + doc.text('(output)', 320, 30, { + lineBreak: false + }); + + doc.image('tests/images/orientation-1.jpeg', 320, 44.0625, { + height: 80, + ignoreOrientation: false + }); + + doc.text('2 - Flip horizonatal', 40, 124.0625, { + lineBreak: false + }); + + doc.image('tests/images/orientation-2.jpeg', 40, 138.125, { + height: 80 + }); + + doc.text('(output)', 320, 124.0625, { + lineBreak: false + }); + + doc.image('tests/images/orientation-2.jpeg', 320, 138.125, { + height: 80, + ignoreOrientation: false + }); + + doc.text('3 - Rotate 180 degrees', 40, 218.125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-3.jpeg', 40, 232.1875, { + height: 80 + }); + + doc.text('(output)', 320, 218.125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-3.jpeg', 320, 232.1875, { + height: 80, + ignoreOrientation: false + }); + + doc.text('4 - Flip vertically', 40, 312.1875, { + lineBreak: false + }); + + doc.image('tests/images/orientation-4.jpeg', 40, 326.25, { + height: 80 + }); + + doc.text('(output)', 320, 312.1875, { + lineBreak: false + }); + + doc.image('tests/images/orientation-4.jpeg', 320, 326.25, { + height: 80, + ignoreOrientation: false + }); + + doc.text('5 - Flip horizontally and rotate 270 degrees CW', 40, 406.25, { + lineBreak: false + }); + + doc.image('tests/images/orientation-5.jpeg', 40, 420.3125, { + height: 80 + }); + + doc.text('(output)', 320, 406.25, { + lineBreak: false + }); + + doc.image('tests/images/orientation-5.jpeg', 320, 420.3125, { + height: 80, + ignoreOrientation: false + }); + + doc.text('6 - Rotate 90 degrees CW', 40, 500.3125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-6.jpeg', 40, 514.375, { + height: 80 + }); + + doc.text('(output)', 320, 500.3125, { + lineBreak: false + }); + + doc.image('tests/images/orientation-6.jpeg', 320, 514.375, { + height: 80, + ignoreOrientation: false + }); + + doc.text('7 - Flip horizontally and rotate 90 degrees CW', 40, 594.375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-7.jpeg', 40, 608.4375, { + height: 80 + }); + + doc.text('(output)', 320, 594.375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-7.jpeg', 320, 608.4375, { + height: 80, + ignoreOrientation: false + }); + + doc.text('8 - Rotate 270 degrees CW', 40, 688.4375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-8.jpeg', 40, 702.5, { + height: 80 + }); + + doc.text('(output)', 320, 688.4375, { + lineBreak: false + }); + + doc.image('tests/images/orientation-8.jpeg', 320, 702.5, { + height: 80, + ignoreOrientation: false + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 95fb67534..6a2ab9db9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4976,6 +4976,11 @@ jest@^29.4.3: import-local "^3.0.2" jest-cli "^29.4.3" +jpeg-exif@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/jpeg-exif/-/jpeg-exif-1.1.4.tgz#781a65b6cd74f62cb1c493511020f8d3577a1c2b" + integrity sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ== + js-stringify@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db"