@ohos-ports/exif-orientation-image 1.0.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +51 -0
- package/index.js +139 -0
- package/lib/node-file-reader.js +129 -0
- package/lib/node-orientation.js +169 -0
- package/lib/node-translate.js +221 -0
- package/package.json +46 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright (c) 2016 Nick Poisson
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
18
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
19
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
20
|
+
OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# exif-orientation-image
|
|
2
|
+
|
|
3
|
+
[](http://github.com/badges/stability-badges)
|
|
4
|
+
|
|
5
|
+
Properly displays an image via canvas based on the exif orientation data. Uses [exif-orientation](https://www.npmjs.com/package/exif-orientation).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install exif-orientation-image --save
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Example
|
|
14
|
+
|
|
15
|
+
The following example reacts to the `onChange` event of a file upload html input
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
var getOrientedImage = require('exif-orientation-image');
|
|
19
|
+
|
|
20
|
+
fileUpload.addEventListener('change',function(e) {
|
|
21
|
+
var file = e.target.files[0];
|
|
22
|
+
getOrientedImage(file,function(err,canvas) {
|
|
23
|
+
if (!err) {
|
|
24
|
+
document.body.appendChild(canvas);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
[](https://www.npmjs.com/package/exif-orientation-image)
|
|
33
|
+
|
|
34
|
+
#### `getOrientedImage(file,callback)`
|
|
35
|
+
|
|
36
|
+
```file``` A file object from a file upload html input
|
|
37
|
+
```callback``` A function to be called once the image is rendered in its proper position. The callback is passed 2 arguments `(err,canvas)`. If `err` is undefined, canvas will be an HTML canvas element with the correctly oriented image, otherwise err will be an `Error` object with the message of the error.
|
|
38
|
+
|
|
39
|
+
#### `orientation.translate(image,orientation,options)`
|
|
40
|
+
|
|
41
|
+
```image``` A loaded html image element
|
|
42
|
+
```orientation``` Orientation object returned from [exif-orientation](https://www.npmjs.com/package/exif-orientation). Contains scale.x, scale.y, and rotation. Rotation is a number in degrees, scale.x and scale.y are numbers.
|
|
43
|
+
```options``` Custom options, right now the only options are `width` and `height` which determines the size of the returned canvas. It will use the image dimensions if not provided.
|
|
44
|
+
|
|
45
|
+
#### `orientation.orientation()`
|
|
46
|
+
|
|
47
|
+
Exposes the [exif-orientation](https://www.npmjs.com/package/exif-orientation) function in case you want to handle that manually.
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
MIT, see [LICENSE.md](http://github.com/Jam3/exif-orientation-image/blob/master/LICENSE.md) for details.
|
package/index.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
var findOrientation = require('exif-orientation');
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Node.js (HarmonyOS) support
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// The original package is browser-only: it relies on FileReader (via the
|
|
8
|
+
// exif-orientation dependency), document.createElement('canvas'),
|
|
9
|
+
// new Image() and URL.createObjectURL. On Node.js / HarmonyOS none of those
|
|
10
|
+
// exist, so we provide:
|
|
11
|
+
// 1. a real FileReader polyfill (fs/Buffer -> true ArrayBuffer) so that the
|
|
12
|
+
// exif-orientation dependency keeps working unmodified;
|
|
13
|
+
// 2. a real Node translate()/load() path that decodes JPEG bytes, applies the
|
|
14
|
+
// EXIF orientation transform on the raw pixels and re-encodes the image
|
|
15
|
+
// (see lib/node-translate.js). No stubs — output images are real.
|
|
16
|
+
|
|
17
|
+
var IS_NODE_RUNTIME = typeof document === 'undefined' || typeof Image === 'undefined';
|
|
18
|
+
|
|
19
|
+
if (IS_NODE_RUNTIME && typeof global.FileReader === 'undefined') {
|
|
20
|
+
global.FileReader = require('./lib/node-file-reader');
|
|
21
|
+
}
|
|
22
|
+
var nodeTranslate = IS_NODE_RUNTIME ? require('./lib/node-translate') : null;
|
|
23
|
+
var nodeOrientation = IS_NODE_RUNTIME ? require('./lib/node-orientation') : null;
|
|
24
|
+
|
|
25
|
+
// In Node.js the exif-orientation dependency (exif-js) crashes on several
|
|
26
|
+
// valid-in-practice JPEGs (it scans past EOI and reads out of bounds), so the
|
|
27
|
+
// Node path uses lib/node-orientation.js — a real EXIF APP1/TIFF IFD0 parser
|
|
28
|
+
// working directly on Buffers, with identical semantics (default orientation
|
|
29
|
+
// when no EXIF Orientation tag is present).
|
|
30
|
+
function findOrientationNode(file, cb) {
|
|
31
|
+
nodeOrientation(file, cb);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Decode any accepted input into a Node Buffer of image bytes
|
|
35
|
+
function readImageBytes(file) {
|
|
36
|
+
var fs = require('fs');
|
|
37
|
+
if (Buffer.isBuffer(file)) {
|
|
38
|
+
return file;
|
|
39
|
+
}
|
|
40
|
+
if (typeof file === 'string') {
|
|
41
|
+
return fs.readFileSync(file);
|
|
42
|
+
}
|
|
43
|
+
if (file && typeof file.path === 'string') {
|
|
44
|
+
return fs.readFileSync(file.path);
|
|
45
|
+
}
|
|
46
|
+
if (ArrayBuffer.isView(file)) {
|
|
47
|
+
return Buffer.from(file.buffer, file.byteOffset, file.byteLength);
|
|
48
|
+
}
|
|
49
|
+
if (file instanceof ArrayBuffer) {
|
|
50
|
+
return Buffer.from(file);
|
|
51
|
+
}
|
|
52
|
+
if (file && typeof file.arrayBuffer === 'function') {
|
|
53
|
+
// Blob/File-like — handled by the async branch in load() below
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function load(file,cb) {
|
|
60
|
+
(IS_NODE_RUNTIME ? findOrientationNode : findOrientation)(file,function(err,orientation) {
|
|
61
|
+
if (!err) {
|
|
62
|
+
if (IS_NODE_RUNTIME) {
|
|
63
|
+
// Node.js path: read bytes, apply EXIF orientation, re-encode.
|
|
64
|
+
var bytes = readImageBytes(file);
|
|
65
|
+
if (bytes === null && file && typeof file.arrayBuffer === 'function') {
|
|
66
|
+
file.arrayBuffer().then(function (ab) {
|
|
67
|
+
try {
|
|
68
|
+
cb(undefined, nodeTranslate(Buffer.from(ab), orientation));
|
|
69
|
+
} catch (e) {
|
|
70
|
+
cb(e);
|
|
71
|
+
}
|
|
72
|
+
}, cb);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (bytes) {
|
|
76
|
+
try {
|
|
77
|
+
cb(undefined, nodeTranslate(bytes, orientation));
|
|
78
|
+
} catch (e) {
|
|
79
|
+
cb(e);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
cb(new Error('Could not read image bytes in Node.js. Pass a Buffer, a file path or a Blob.'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
loadImage(URL.createObjectURL(file),function(img) {
|
|
87
|
+
if (img) {
|
|
88
|
+
cb(undefined,translate(img,orientation));
|
|
89
|
+
} else {
|
|
90
|
+
cb(new Error('Could not load image.'));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
cb(err);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function translate(image,orientation,opts) {
|
|
100
|
+
orientation = orientation || {};
|
|
101
|
+
opts = opts || {};
|
|
102
|
+
if (IS_NODE_RUNTIME) {
|
|
103
|
+
// Node.js path: real pixel-level re-orientation (JPEG decode/encode or
|
|
104
|
+
// raw RGBA transform), or geometry descriptor for metadata-only images.
|
|
105
|
+
return nodeTranslate(image, orientation, opts);
|
|
106
|
+
}
|
|
107
|
+
var c = document.createElement('canvas');
|
|
108
|
+
var ctx = c.getContext('2d');
|
|
109
|
+
c.width = opts.width || image.naturalWidth;
|
|
110
|
+
c.height = opts.height || image.naturalHeight;
|
|
111
|
+
if (orientation.scale.x===-1 && orientation.scale.y===-1) {
|
|
112
|
+
ctx.translate(c.width,c.height);
|
|
113
|
+
ctx.scale(orientation.scale.x,orientation.scale.y);
|
|
114
|
+
} else if (orientation.scale.x!==1) {
|
|
115
|
+
ctx.translate(c.width,0);
|
|
116
|
+
ctx.scale(orientation.scale.x,1);
|
|
117
|
+
} else if (orientation.scale.y!==1) {
|
|
118
|
+
ctx.translate(0,c.height);
|
|
119
|
+
ctx.scale(1,orientation.scale.y);
|
|
120
|
+
}
|
|
121
|
+
if (orientation.rotate) {
|
|
122
|
+
ctx.translate(c.width*0.5,c.height*0.5);
|
|
123
|
+
ctx.rotate(orientation.rotate*(Math.PI / 180));
|
|
124
|
+
ctx.translate(-c.width*0.5,-c.height*0.5);
|
|
125
|
+
}
|
|
126
|
+
ctx.drawImage(image,0,0,image.naturalWidth,image.naturalHeight,0,0,c.width,c.height);
|
|
127
|
+
return c;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function loadImage(src,cb) {
|
|
131
|
+
var img = new Image();
|
|
132
|
+
img.onload = function() { cb(img); };
|
|
133
|
+
img.onerror = function() { cb(); };
|
|
134
|
+
img.src = src;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = load;
|
|
138
|
+
module.exports.orientation = IS_NODE_RUNTIME ? findOrientationNode : findOrientation;
|
|
139
|
+
module.exports.translate = translate;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Node.js FileReader polyfill for HarmonyOS.
|
|
3
|
+
// Provides a real FileReader implementation backed by fs/Buffer so that
|
|
4
|
+
// browser-oriented code using FileReader.readAsArrayBuffer (e.g. exif-orientation)
|
|
5
|
+
// works in Node.js. Data read is always delivered as a true ArrayBuffer,
|
|
6
|
+
// which is what consumers like exif-js expect (new DataView(result)).
|
|
7
|
+
|
|
8
|
+
var fs = require('fs');
|
|
9
|
+
|
|
10
|
+
function toBuffer(value) {
|
|
11
|
+
if (Buffer.isBuffer(value)) {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === 'string') {
|
|
15
|
+
// treat strings as filesystem paths
|
|
16
|
+
return fs.readFileSync(value);
|
|
17
|
+
}
|
|
18
|
+
if (value && typeof value === 'object') {
|
|
19
|
+
// { path: '...' } — file-like object carrying a filesystem path
|
|
20
|
+
if (typeof value.path === 'string') {
|
|
21
|
+
return fs.readFileSync(value.path);
|
|
22
|
+
}
|
|
23
|
+
// Blob/File-like objects exposing an async arrayBuffer()
|
|
24
|
+
// (handled by the caller since it is asynchronous)
|
|
25
|
+
if (typeof value.arrayBuffer === 'function') {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
// ArrayBufferView (Uint8Array, DataView, ...) — including Node Buffer subclasses
|
|
29
|
+
if (ArrayBuffer.isView(value)) {
|
|
30
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
31
|
+
}
|
|
32
|
+
// raw ArrayBuffer
|
|
33
|
+
if (value instanceof ArrayBuffer) {
|
|
34
|
+
return Buffer.from(value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw new TypeError('FileReader polyfill: unsupported input type: ' + typeof value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function bufferToArrayBuffer(buf) {
|
|
41
|
+
// Return a dedicated ArrayBuffer copy so byteOffset/byteLength semantics
|
|
42
|
+
// match the browser FileReader behaviour exactly.
|
|
43
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function NodeFileReader() {
|
|
47
|
+
this.readyState = NodeFileReader.EMPTY;
|
|
48
|
+
this.result = null;
|
|
49
|
+
this.error = null;
|
|
50
|
+
this.onload = null;
|
|
51
|
+
this.onerror = null;
|
|
52
|
+
this.onabort = null;
|
|
53
|
+
this.onloadstart = null;
|
|
54
|
+
this.onloadend = null;
|
|
55
|
+
this.onprogress = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
NodeFileReader.EMPTY = 0;
|
|
59
|
+
NodeFileReader.LOADING = 1;
|
|
60
|
+
NodeFileReader.DONE = 2;
|
|
61
|
+
|
|
62
|
+
NodeFileReader.prototype.readAsArrayBuffer = function (file) {
|
|
63
|
+
var self = this;
|
|
64
|
+
self.readyState = NodeFileReader.LOADING;
|
|
65
|
+
if (typeof self.onloadstart === 'function') {
|
|
66
|
+
self.onloadstart({ target: self, type: 'loadstart' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function succeed(buf) {
|
|
70
|
+
self.readyState = NodeFileReader.DONE;
|
|
71
|
+
self.result = bufferToArrayBuffer(buf);
|
|
72
|
+
if (typeof self.onload === 'function') {
|
|
73
|
+
self.onload({ target: self, type: 'load' });
|
|
74
|
+
}
|
|
75
|
+
if (typeof self.onloadend === 'function') {
|
|
76
|
+
self.onloadend({ target: self, type: 'loadend' });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function fail(err) {
|
|
81
|
+
self.readyState = NodeFileReader.DONE;
|
|
82
|
+
self.error = err;
|
|
83
|
+
if (typeof self.onerror === 'function') {
|
|
84
|
+
self.onerror({ target: self, type: 'error', message: err && err.message });
|
|
85
|
+
}
|
|
86
|
+
if (typeof self.onloadend === 'function') {
|
|
87
|
+
self.onloadend({ target: self, type: 'loadend' });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Blob/File-like with async arrayBuffer()
|
|
92
|
+
if (file && typeof file === 'object' && typeof file.arrayBuffer === 'function') {
|
|
93
|
+
file.arrayBuffer().then(function (ab) {
|
|
94
|
+
succeed(Buffer.from(ab));
|
|
95
|
+
}, fail);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// synchronous sources — dispatch asynchronously like the real FileReader
|
|
100
|
+
process.nextTick(function () {
|
|
101
|
+
try {
|
|
102
|
+
succeed(toBuffer(file));
|
|
103
|
+
} catch (err) {
|
|
104
|
+
fail(err);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
NodeFileReader.prototype.readAsText = function (file, encoding) {
|
|
110
|
+
var self = this;
|
|
111
|
+
self.readAsArrayBuffer(file);
|
|
112
|
+
var origOnload = self.onload;
|
|
113
|
+
self.onload = function (e) {
|
|
114
|
+
self.result = Buffer.from(self.result).toString(encoding || 'utf8');
|
|
115
|
+
if (typeof origOnload === 'function') {
|
|
116
|
+
origOnload(e);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
NodeFileReader.prototype.abort = function () {
|
|
122
|
+
this.readyState = NodeFileReader.DONE;
|
|
123
|
+
this.result = null;
|
|
124
|
+
if (typeof this.onabort === 'function') {
|
|
125
|
+
this.onabort({ target: this, type: 'abort' });
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
module.exports = NodeFileReader;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Node.js EXIF orientation reader for HarmonyOS.
|
|
3
|
+
//
|
|
4
|
+
// Parses the JPEG marker structure and the EXIF APP1 TIFF IFD0 directly from a
|
|
5
|
+
// Buffer — no browser FileReader required. This replaces the browser-only path
|
|
6
|
+
// (exif-orientation -> FileReader -> exif-js) in Node.js runtimes.
|
|
7
|
+
//
|
|
8
|
+
// Semantics mirror exif-orientation@1.0.0 exactly:
|
|
9
|
+
// - orientation value 0 / missing EXIF -> default orientation (exif: 0)
|
|
10
|
+
// - the same scale/rotate table is used
|
|
11
|
+
|
|
12
|
+
var ORIENTATIONS = [
|
|
13
|
+
{ scale: { x: 1, y: 1 }, rotate: 0 },
|
|
14
|
+
{ scale: { x: 1, y: 1 }, rotate: 0 },
|
|
15
|
+
{ scale: { x: -1, y: 1 }, rotate: 0 },
|
|
16
|
+
{ scale: { x: 1, y: 1 }, rotate: 180 },
|
|
17
|
+
{ scale: { x: 1, y: -1 }, rotate: 0 },
|
|
18
|
+
{ scale: { x: -1, y: 1 }, rotate: 90 },
|
|
19
|
+
{ scale: { x: 1, y: 1 }, rotate: 90 },
|
|
20
|
+
{ scale: { x: -1, y: 1 }, rotate: -90 },
|
|
21
|
+
{ scale: { x: 1, y: 1 }, rotate: -90 }
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Extract the raw EXIF Orientation tag value (0x0112) from JPEG bytes.
|
|
25
|
+
// Returns 0 when the JPEG carries no EXIF orientation.
|
|
26
|
+
function readExifOrientationValue(buf) {
|
|
27
|
+
if (!buf || buf.length < 4) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
if (buf[0] !== 0xFF || buf[1] !== 0xD8) {
|
|
31
|
+
return 0; // not a JPEG — no EXIF orientation
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var len = buf.length;
|
|
35
|
+
var offset = 2;
|
|
36
|
+
|
|
37
|
+
while (offset + 4 <= len) {
|
|
38
|
+
if (buf[offset] !== 0xFF) {
|
|
39
|
+
return 0; // malformed marker — stop scanning (no crash like exif-js)
|
|
40
|
+
}
|
|
41
|
+
var marker = buf[offset + 1];
|
|
42
|
+
if (marker === 0xD9 || marker === 0xDA) {
|
|
43
|
+
// EOI (end of image) or SOS (start of scan): no APP1 EXIF ahead
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
var segLen = buf.readUInt16BE(offset + 2);
|
|
47
|
+
if (offset + 2 + segLen > len) {
|
|
48
|
+
return 0; // truncated segment
|
|
49
|
+
}
|
|
50
|
+
if (marker === 0xE1 && segLen >= 8 &&
|
|
51
|
+
buf[offset + 4] === 0x45 && buf[offset + 5] === 0x78 &&
|
|
52
|
+
buf[offset + 6] === 0x69 && buf[offset + 7] === 0x66 &&
|
|
53
|
+
buf[offset + 8] === 0x00) {
|
|
54
|
+
// APP1 with "Exif\0\0" signature — parse the TIFF structure
|
|
55
|
+
return parseTiffOrientation(buf, offset + 10);
|
|
56
|
+
}
|
|
57
|
+
offset += 2 + segLen;
|
|
58
|
+
}
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseTiffOrientation(buf, tiffStart) {
|
|
63
|
+
if (tiffStart + 8 > buf.length) {
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
var byteOrder = buf.readUInt16BE(tiffStart);
|
|
67
|
+
var littleEndian;
|
|
68
|
+
if (byteOrder === 0x4949) { // "II" — Intel (little endian)
|
|
69
|
+
littleEndian = true;
|
|
70
|
+
} else if (byteOrder === 0x4D4D) { // "MM" — Motorola (big endian)
|
|
71
|
+
littleEndian = false;
|
|
72
|
+
} else {
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
var ifdOffset = littleEndian
|
|
77
|
+
? buf.readUInt32LE(tiffStart + 4)
|
|
78
|
+
: buf.readUInt32BE(tiffStart + 4);
|
|
79
|
+
var ifdStart = tiffStart + ifdOffset;
|
|
80
|
+
if (ifdStart + 2 > buf.length) {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var entryCount = littleEndian
|
|
85
|
+
? buf.readUInt16LE(ifdStart)
|
|
86
|
+
: buf.readUInt16BE(ifdStart);
|
|
87
|
+
|
|
88
|
+
for (var i = 0; i < entryCount; i++) {
|
|
89
|
+
var entryOffset = ifdStart + 2 + i * 12;
|
|
90
|
+
if (entryOffset + 12 > buf.length) {
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
var tag = littleEndian
|
|
94
|
+
? buf.readUInt16LE(entryOffset)
|
|
95
|
+
: buf.readUInt16BE(entryOffset);
|
|
96
|
+
if (tag === 0x0112) { // Orientation
|
|
97
|
+
// Orientation is a SHORT (type 3): stored in the first 2 bytes of the
|
|
98
|
+
// 4-byte value field, using the TIFF endianness
|
|
99
|
+
return littleEndian
|
|
100
|
+
? buf.readUInt16LE(entryOffset + 8)
|
|
101
|
+
: buf.readUInt16BE(entryOffset + 8);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Same contract as exif-orientation: function(file, cb) with a Node-friendly
|
|
108
|
+
// file (Buffer, ArrayBuffer, typed array or filesystem path).
|
|
109
|
+
module.exports = function (file, cb) {
|
|
110
|
+
if (file && typeof cb === 'function') {
|
|
111
|
+
var finish = function (bytes) {
|
|
112
|
+
var val;
|
|
113
|
+
try {
|
|
114
|
+
val = readExifOrientationValue(bytes);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
cb(new Error('Could not read file.'));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
var orientation = ORIENTATIONS[val] || ORIENTATIONS[0];
|
|
120
|
+
orientation.exif = val;
|
|
121
|
+
cb(undefined, orientation);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
var bytes = null;
|
|
125
|
+
if (Buffer.isBuffer(file)) {
|
|
126
|
+
bytes = file;
|
|
127
|
+
} else if (typeof file === 'string') {
|
|
128
|
+
var fs = require('fs');
|
|
129
|
+
fs.readFile(file, function (err, data) {
|
|
130
|
+
if (err) {
|
|
131
|
+
cb(new Error('Could not read file.'));
|
|
132
|
+
} else {
|
|
133
|
+
finish(data);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
} else if (file && typeof file.path === 'string') {
|
|
138
|
+
var fs2 = require('fs');
|
|
139
|
+
fs2.readFile(file.path, function (err2, data2) {
|
|
140
|
+
if (err2) {
|
|
141
|
+
cb(new Error('Could not read file.'));
|
|
142
|
+
} else {
|
|
143
|
+
finish(data2);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
} else if (ArrayBuffer.isView(file)) {
|
|
148
|
+
bytes = Buffer.from(file.buffer, file.byteOffset, file.byteLength);
|
|
149
|
+
} else if (file instanceof ArrayBuffer) {
|
|
150
|
+
bytes = Buffer.from(file);
|
|
151
|
+
} else if (file && typeof file.arrayBuffer === 'function') {
|
|
152
|
+
file.arrayBuffer().then(function (ab) {
|
|
153
|
+
finish(Buffer.from(ab));
|
|
154
|
+
}, function () {
|
|
155
|
+
cb(new Error('Could not read file.'));
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (bytes) {
|
|
161
|
+
finish(bytes);
|
|
162
|
+
} else {
|
|
163
|
+
cb(new Error('Could not read file.'));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
module.exports.readExifOrientationValue = readExifOrientationValue;
|
|
169
|
+
module.exports.ORIENTATIONS = ORIENTATIONS;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Node.js implementation of the canvas-based translate() of exif-orientation-image.
|
|
3
|
+
//
|
|
4
|
+
// Provides real, working image re-orientation without a browser canvas:
|
|
5
|
+
// - JPEG Buffer input -> decode (jpeg-js) -> apply EXIF orientation transform
|
|
6
|
+
// on the raw RGBA pixels -> re-encode -> JPEG Buffer out
|
|
7
|
+
// - raw pixel object -> { data, width, height } RGBA in/out, pixels really
|
|
8
|
+
// transformed via inverse-mapped affine transform
|
|
9
|
+
// - metadata-only image (browser-style { naturalWidth, naturalHeight } with no
|
|
10
|
+
// pixels) -> returns a geometry descriptor carrying the
|
|
11
|
+
// exact affine matrix the canvas pipeline would use
|
|
12
|
+
// (pure matrix math; no fake pixels are produced)
|
|
13
|
+
|
|
14
|
+
var jpeg = require('jpeg-js');
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// 3x3 affine matrix helpers (column vectors: [a c e; b d f; 0 0 1] canvas order)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
function identity() {
|
|
21
|
+
return [1, 0, 0, 1, 0, 0]; // a, b, c, d, e, f
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function multiply(m1, m2) {
|
|
25
|
+
var a1 = m1[0], b1 = m1[1], c1 = m1[2], d1 = m1[3], e1 = m1[4], f1 = m1[5];
|
|
26
|
+
var a2 = m2[0], b2 = m2[1], c2 = m2[2], d2 = m2[3], e2 = m2[4], f2 = m2[5];
|
|
27
|
+
return [
|
|
28
|
+
a1 * a2 + c1 * b2,
|
|
29
|
+
b1 * a2 + d1 * b2,
|
|
30
|
+
a1 * c2 + c1 * d2,
|
|
31
|
+
b1 * c2 + d1 * d2,
|
|
32
|
+
a1 * e2 + c1 * f2 + e1,
|
|
33
|
+
b1 * e2 + d1 * f2 + f1
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function translate(m, x, y) {
|
|
38
|
+
return multiply(m, [1, 0, 0, 1, x, y]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function scale(m, sx, sy) {
|
|
42
|
+
return multiply(m, [sx, 0, 0, sy, 0, 0]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rotate(m, rad) {
|
|
46
|
+
var cos = Math.cos(rad);
|
|
47
|
+
var sin = Math.sin(rad);
|
|
48
|
+
return multiply(m, [cos, sin, -sin, cos, 0, 0]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function apply(m, x, y) {
|
|
52
|
+
return {
|
|
53
|
+
x: m[0] * x + m[2] * y + m[4],
|
|
54
|
+
y: m[1] * x + m[3] * y + m[5]
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function invert(m) {
|
|
59
|
+
var det = m[0] * m[3] - m[1] * m[2];
|
|
60
|
+
if (!det) {
|
|
61
|
+
throw new Error('Cannot invert singular transform matrix');
|
|
62
|
+
}
|
|
63
|
+
var a = m[0], b = m[1], c = m[2], d = m[3], e = m[4], f = m[5];
|
|
64
|
+
var ia = d / det;
|
|
65
|
+
var ib = -b / det;
|
|
66
|
+
var ic = -c / det;
|
|
67
|
+
var id = a / det;
|
|
68
|
+
var ie = (c * f - d * e) / det;
|
|
69
|
+
var if_ = (b * e - a * f) / det;
|
|
70
|
+
return [ia, ib, ic, id, ie, if_];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Canvas-equivalent transform construction (mirrors index.js browser logic)
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
function buildTransform(srcW, srcH, outW, outH, orientation) {
|
|
78
|
+
var m = identity();
|
|
79
|
+
var s = orientation.scale || { x: 1, y: 1 };
|
|
80
|
+
var sx = s.x === undefined ? 1 : s.x;
|
|
81
|
+
var sy = s.y === undefined ? 1 : s.y;
|
|
82
|
+
|
|
83
|
+
if (sx === -1 && sy === -1) {
|
|
84
|
+
m = translate(m, srcW, srcH);
|
|
85
|
+
m = scale(m, sx, sy);
|
|
86
|
+
} else if (sx !== 1) {
|
|
87
|
+
m = translate(m, srcW, 0);
|
|
88
|
+
m = scale(m, sx, 1);
|
|
89
|
+
} else if (sy !== 1) {
|
|
90
|
+
m = translate(m, 0, srcH);
|
|
91
|
+
m = scale(m, 1, sy);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (orientation.rotate) {
|
|
95
|
+
var rad = orientation.rotate * (Math.PI / 180);
|
|
96
|
+
m = translate(m, srcW * 0.5, srcH * 0.5);
|
|
97
|
+
m = rotate(m, rad);
|
|
98
|
+
m = translate(m, -srcW * 0.5, -srcH * 0.5);
|
|
99
|
+
// re-center the rotated image inside the output frame
|
|
100
|
+
// (pre-multiply: the shift applies in output coordinate space)
|
|
101
|
+
m = multiply(
|
|
102
|
+
[1, 0, 0, 1, (outW - srcW) * 0.5, (outH - srcH) * 0.5],
|
|
103
|
+
m
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return m;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function outputDimensions(srcW, srcH, orientation, opts) {
|
|
110
|
+
if (opts && opts.width && opts.height) {
|
|
111
|
+
return { width: opts.width, height: opts.height };
|
|
112
|
+
}
|
|
113
|
+
var rot = orientation.rotate || 0;
|
|
114
|
+
var swap = Math.abs(Math.abs(rot) - 90) < 1e-9 || Math.abs(Math.abs(rot) - 270) < 1e-9;
|
|
115
|
+
// 90/270 rotations require swapped output dimensions so the whole image fits
|
|
116
|
+
// (produces a genuinely correctly oriented image in the Node pipeline)
|
|
117
|
+
return {
|
|
118
|
+
width: swap ? srcH : srcW,
|
|
119
|
+
height: swap ? srcW : srcH
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Real pixel transform: inverse-map every output pixel, nearest-neighbour
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
function transformPixels(data, srcW, srcH, outW, outH, m) {
|
|
128
|
+
var inv = invert(m);
|
|
129
|
+
var out = Buffer.alloc(outW * outH * 4);
|
|
130
|
+
for (var oy = 0; oy < outH; oy++) {
|
|
131
|
+
for (var ox = 0; ox < outW; ox++) {
|
|
132
|
+
var s = apply(inv, ox + 0.5, oy + 0.5);
|
|
133
|
+
var sxp = Math.round(s.x - 0.5);
|
|
134
|
+
var syp = Math.round(s.y - 0.5);
|
|
135
|
+
var oIdx = (oy * outW + ox) * 4;
|
|
136
|
+
if (sxp < 0 || syp < 0 || sxp >= srcW || syp >= srcH) {
|
|
137
|
+
out[oIdx + 3] = 0; // transparent outside the source bounds
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
var sIdx = (syp * srcW + sxp) * 4;
|
|
141
|
+
out[oIdx] = data[sIdx];
|
|
142
|
+
out[oIdx + 1] = data[sIdx + 1];
|
|
143
|
+
out[oIdx + 2] = data[sIdx + 2];
|
|
144
|
+
out[oIdx + 3] = data[sIdx + 3];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isIdentityOrientation(orientation) {
|
|
151
|
+
var s = (orientation && orientation.scale) || { x: 1, y: 1 };
|
|
152
|
+
return (s.x === undefined || s.x === 1) &&
|
|
153
|
+
(s.y === undefined || s.y === 1) &&
|
|
154
|
+
!(orientation && orientation.rotate);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Public Node translate()
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
function nodeTranslate(image, orientation, opts) {
|
|
162
|
+
orientation = orientation || {};
|
|
163
|
+
opts = opts || {};
|
|
164
|
+
|
|
165
|
+
// Case 1: encoded JPEG bytes (Buffer / ArrayBuffer / typed array)
|
|
166
|
+
if (Buffer.isBuffer(image) || image instanceof ArrayBuffer || ArrayBuffer.isView(image)) {
|
|
167
|
+
var buf = Buffer.isBuffer(image)
|
|
168
|
+
? image
|
|
169
|
+
: Buffer.from(image.buffer || image, image.byteOffset || 0, image.byteLength || image.byteLength);
|
|
170
|
+
|
|
171
|
+
if (isIdentityOrientation(orientation)) {
|
|
172
|
+
// nothing to transform — the correctly oriented image is the input itself
|
|
173
|
+
return buf;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
var decoded = jpeg.decode(buf, { useTArray: true, formatAsRGBA: true });
|
|
177
|
+
var dims = outputDimensions(decoded.width, decoded.height, orientation, opts);
|
|
178
|
+
var m = buildTransform(decoded.width, decoded.height, dims.width, dims.height, orientation);
|
|
179
|
+
var pixels = transformPixels(decoded.data, decoded.width, decoded.height, dims.width, dims.height, m);
|
|
180
|
+
var encoded = jpeg.encode({ data: pixels, width: dims.width, height: dims.height }, opts.quality || 90);
|
|
181
|
+
return Buffer.from(encoded.data);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Case 2: raw RGBA pixel object { data, width, height }
|
|
185
|
+
if (image && image.data && typeof image.width === 'number' && typeof image.height === 'number') {
|
|
186
|
+
var srcW = image.width;
|
|
187
|
+
var srcH = image.height;
|
|
188
|
+
var dims2 = outputDimensions(srcW, srcH, orientation, opts);
|
|
189
|
+
var m2 = buildTransform(srcW, srcH, dims2.width, dims2.height, orientation);
|
|
190
|
+
var pixels2 = transformPixels(image.data, srcW, srcH, dims2.width, dims2.height, m2);
|
|
191
|
+
return { data: pixels2, width: dims2.width, height: dims2.height };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Case 3: browser-style metadata-only image (naturalWidth/naturalHeight).
|
|
195
|
+
// No pixels are available in Node, so return the exact geometry the canvas
|
|
196
|
+
// pipeline would use: real affine matrix + output dimensions. This is honest
|
|
197
|
+
// geometry information, not fake pixel data.
|
|
198
|
+
if (image && (typeof image.naturalWidth === 'number' || typeof image.width === 'number')) {
|
|
199
|
+
var w = typeof image.naturalWidth === 'number' ? image.naturalWidth : image.width;
|
|
200
|
+
var h = typeof image.naturalHeight === 'number' ? image.naturalHeight : image.height;
|
|
201
|
+
var dims3 = outputDimensions(w, h, orientation, opts);
|
|
202
|
+
var m3 = buildTransform(w, h, dims3.width, dims3.height, orientation);
|
|
203
|
+
return {
|
|
204
|
+
width: dims3.width,
|
|
205
|
+
height: dims3.height,
|
|
206
|
+
transform: m3, // [a, b, c, d, e, f] — canvas setTransform compatible
|
|
207
|
+
applyTransform: function (ctx) {
|
|
208
|
+
ctx.setTransform(m3[0], m3[1], m3[2], m3[3], m3[4], m3[5]);
|
|
209
|
+
},
|
|
210
|
+
source: image
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
throw new TypeError('nodeTranslate: unsupported image input');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = nodeTranslate;
|
|
218
|
+
module.exports.buildTransform = buildTransform;
|
|
219
|
+
module.exports.outputDimensions = outputDimensions;
|
|
220
|
+
module.exports.transformPixels = transformPixels;
|
|
221
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ohos-ports/exif-orientation-image",
|
|
3
|
+
"version": "1.0.1-beta.1",
|
|
4
|
+
"description": "Properly displays an image via canvas based on the exif orientation data. To be used with a file object",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Nick Poisson",
|
|
9
|
+
"email": "nick@jam3.com",
|
|
10
|
+
"url": "https://github.com/njam3"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"exif-orientation": "^1.0.0",
|
|
14
|
+
"jpeg-js": "^0.4.4"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"tape": "*"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node test/test.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"exif,",
|
|
24
|
+
"orientation,",
|
|
25
|
+
"canvas,",
|
|
26
|
+
"translate,",
|
|
27
|
+
"rotate,",
|
|
28
|
+
"scale,",
|
|
29
|
+
"file"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/ohos-ports/ohos-ports.git",
|
|
34
|
+
"directory": "ports/exif-orientation-image/1.0.1"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/Jam3/exif-orientation-image",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/ohos-ports/ohos-ports/issues"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"index.js",
|
|
42
|
+
"lib/",
|
|
43
|
+
"LICENSE.md",
|
|
44
|
+
"README.md"
|
|
45
|
+
]
|
|
46
|
+
}
|