diff --git a/examples/image_decoders/fish.jpg b/examples/image_decoders/fish.jpg new file mode 100644 index 000000000..aa5fbd0d9 Binary files /dev/null and b/examples/image_decoders/fish.jpg differ diff --git a/examples/image_decoders/jpeg_viewer.html b/examples/image_decoders/jpeg_viewer.html new file mode 100644 index 000000000..47af71b0d --- /dev/null +++ b/examples/image_decoders/jpeg_viewer.html @@ -0,0 +1,40 @@ + + + + + + + + PDF.js standalone JpegImage parser + + + + + + + + + + + + diff --git a/examples/image_decoders/jpeg_viewer.js b/examples/image_decoders/jpeg_viewer.js new file mode 100644 index 000000000..5bc81ecf3 --- /dev/null +++ b/examples/image_decoders/jpeg_viewer.js @@ -0,0 +1,75 @@ +/* Copyright 2018 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +if (!pdfjsImageDecoders.JpegImage) { + alert('Please build the pdfjs-dist library using `gulp dist-install`'); +} + +var JPEG_IMAGE = 'fish.jpg'; + +var jpegCanvas = document.getElementById('jpegCanvas'); +var jpegCtx = jpegCanvas.getContext('2d'); + +// Load the image data, and convert it to a Uint8Array. +// +var nonBinaryRequest = false; +var request = new XMLHttpRequest(); +request.open('GET', JPEG_IMAGE, false); +try { + request.responseType = 'arraybuffer'; + nonBinaryRequest = request.responseType !== 'arraybuffer'; +} catch (e) { + nonBinaryRequest = true; +} +if (nonBinaryRequest && request.overrideMimeType) { + request.overrideMimeType('text/plain; charset=x-user-defined'); +} +request.send(null); + +var typedArrayImage; +if (nonBinaryRequest) { + var str = request.responseText, length = str.length; + var bytes = new Uint8Array(length); + for (var i = 0; i < length; ++i) { + bytes[i] = str.charCodeAt(i) & 0xFF; + } + typedArrayImage = bytes; +} else { + typedArrayImage = new Uint8Array(request.response); +} + +// Parse the image data using `JpegImage`. +// +var jpegImage = new pdfjsImageDecoders.JpegImage(); +jpegImage.parse(typedArrayImage); + +var width = jpegImage.width, height = jpegImage.height; +var jpegData = jpegImage.getData(width, height, /* forceRGB = */ true); + +// Render the JPEG image on a . +// +var imageData = jpegCtx.createImageData(width, height); +var imageBytes = imageData.data; +for (var i = 0, j = 0, ii = width * height * 4; i < ii;) { + imageBytes[i++] = jpegData[j++]; + imageBytes[i++] = jpegData[j++]; + imageBytes[i++] = jpegData[j++]; + imageBytes[i++] = 255; +} +jpegCanvas.width = width, jpegCanvas.height = height; +jpegCtx.putImageData(imageData, 0, 0); + diff --git a/gulpfile.js b/gulpfile.js index 40719ef1d..c7e66a204 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -50,6 +50,7 @@ var BASELINE_DIR = BUILD_DIR + 'baseline/'; var MOZCENTRAL_BASELINE_DIR = BUILD_DIR + 'mozcentral.baseline/'; var GENERIC_DIR = BUILD_DIR + 'generic/'; var COMPONENTS_DIR = BUILD_DIR + 'components/'; +var IMAGE_DECODERS_DIR = BUILD_DIR + 'image_decoders'; var MINIFIED_DIR = BUILD_DIR + 'minified/'; var JSDOC_BUILD_DIR = BUILD_DIR + 'jsdoc/'; var GH_PAGES_DIR = BUILD_DIR + 'gh-pages/'; @@ -70,7 +71,7 @@ var builder = require('./external/builder/builder.js'); var CONFIG_FILE = 'pdfjs.config'; var config = JSON.parse(fs.readFileSync(CONFIG_FILE).toString()); -// Default Autoprefixer config used for generic, components, minifed-pre +// Default Autoprefixer config used for generic, components, minified-pre var AUTOPREFIXER_CONFIG = { browsers: [ 'last 2 versions', @@ -96,6 +97,7 @@ var DEFINES = { COMPONENTS: false, LIB: false, SKIP_BABEL: false, + IMAGE_DECODERS: false, }; function safeSpawnSync(command, parameters, options) { @@ -306,6 +308,22 @@ function createComponentsBundle(defines) { .pipe(replaceJSRootName(componentsAMDName, 'pdfjsViewer')); } +function createImageDecodersBundle(defines) { + var imageDecodersAMDName = 'pdfjs-dist/image_decoders/pdf.image_decoders'; + var imageDecodersOutputName = 'pdf.image_decoders.js'; + + var componentsFileConfig = createWebpackConfig(defines, { + filename: imageDecodersOutputName, + library: imageDecodersAMDName, + libraryTarget: 'umd', + umdNamedDefine: true, + }); + return gulp.src('./src/pdf.image_decoders.js') + .pipe(webpack2Stream(componentsFileConfig)) + .pipe(replaceWebpackRequire()) + .pipe(replaceJSRootName(imageDecodersAMDName, 'pdfjsImageDecoders')); +} + function checkFile(path) { try { var stat = fs.lstatSync(path); @@ -631,6 +649,15 @@ gulp.task('components', ['buildnumber'], function () { ]); }); +gulp.task('image_decoders', ['buildnumber'], function() { + console.log(); + console.log('### Creating image decoders'); + var defines = builder.merge(DEFINES, { GENERIC: true, + IMAGE_DECODERS: true, }); + + return createImageDecodersBundle(defines).pipe(gulp.dest(IMAGE_DECODERS_DIR)); +}); + gulp.task('minified-pre', ['buildnumber', 'locale'], function () { console.log(); console.log('### Creating minified viewer'); @@ -641,6 +668,8 @@ gulp.task('minified-pre', ['buildnumber', 'locale'], function () { return merge([ createBundle(defines).pipe(gulp.dest(MINIFIED_DIR + 'build')), createWebBundle(defines).pipe(gulp.dest(MINIFIED_DIR + 'web')), + createImageDecodersBundle(builder.merge(defines, { IMAGE_DECODERS: true, })) + .pipe(gulp.dest(MINIFIED_DIR + 'image_decoders')), gulp.src(COMMON_WEB_FILES, { base: 'web/', }) .pipe(gulp.dest(MINIFIED_DIR + 'web')), gulp.src([ @@ -666,6 +695,8 @@ gulp.task('minified-post', ['minified-pre'], function () { var pdfFile = fs.readFileSync(MINIFIED_DIR + '/build/pdf.js').toString(); var pdfWorkerFile = fs.readFileSync(MINIFIED_DIR + '/build/pdf.worker.js').toString(); + var pdfImageDecodersFile = fs.readFileSync(MINIFIED_DIR + + '/image_decoders/pdf.image_decoders.js').toString(); var viewerFiles = { 'pdf.js': pdfFile, 'viewer.js': fs.readFileSync(MINIFIED_DIR + '/web/viewer.js').toString(), @@ -684,6 +715,8 @@ gulp.task('minified-post', ['minified-pre'], function () { UglifyES.minify(pdfFile).code); fs.writeFileSync(MINIFIED_DIR + '/build/pdf.worker.min.js', UglifyES.minify(pdfWorkerFile, optsForHugeFile).code); + fs.writeFileSync(MINIFIED_DIR + 'image_decoders/pdf.image_decoders.min.js', + UglifyES.minify(pdfImageDecodersFile).code); console.log(); console.log('### Cleaning js files'); @@ -696,6 +729,8 @@ gulp.task('minified-post', ['minified-pre'], function () { MINIFIED_DIR + '/build/pdf.js'); fs.renameSync(MINIFIED_DIR + '/build/pdf.worker.min.js', MINIFIED_DIR + '/build/pdf.worker.js'); + fs.renameSync(MINIFIED_DIR + '/image_decoders/pdf.image_decoders.min.js', + MINIFIED_DIR + '/image_decoders/pdf.image_decoders.js'); }); gulp.task('minified', ['minified-post']); @@ -1138,7 +1173,8 @@ gulp.task('gh-pages-git', ['gh-pages-prepare', 'wintersmith'], function () { gulp.task('web', ['gh-pages-prepare', 'wintersmith', 'gh-pages-git']); -gulp.task('dist-pre', ['generic', 'components', 'lib', 'minified'], function() { +gulp.task('dist-pre', + ['generic', 'components', 'image_decoders', 'lib', 'minified'], function() { var VERSION = getVersionJSON().version; console.log(); @@ -1228,8 +1264,13 @@ gulp.task('dist-pre', ['generic', 'components', 'lib', 'minified'], function() { gulp.src(MINIFIED_DIR + 'build/pdf.worker.js') .pipe(rename('pdf.worker.min.js')) .pipe(gulp.dest(DIST_DIR + 'build/')), + gulp.src(MINIFIED_DIR + 'image_decoders/pdf.image_decoders.js') + .pipe(rename('pdf.image_decoders.min.js')) + .pipe(gulp.dest(DIST_DIR + 'image_decoders/')), gulp.src(COMPONENTS_DIR + '**/*', { base: COMPONENTS_DIR, }) .pipe(gulp.dest(DIST_DIR + 'web/')), + gulp.src(IMAGE_DECODERS_DIR + '**/*', { base: IMAGE_DECODERS_DIR, }) + .pipe(gulp.dest(DIST_DIR + 'image_decoders')), gulp.src(LIB_DIR + '**/*', { base: LIB_DIR, }) .pipe(gulp.dest(DIST_DIR + 'lib/')), ]); diff --git a/src/core/jbig2.js b/src/core/jbig2.js index 9f833f979..272573cef 100644 --- a/src/core/jbig2.js +++ b/src/core/jbig2.js @@ -1192,6 +1192,47 @@ var Jbig2Image = (function Jbig2ImageClosure() { return visitor.buffer; } + function parseJbig2(data) { + let position = 0, end = data.length; + + if (data[position] !== 0x97 || data[position + 1] !== 0x4A || + data[position + 2] !== 0x42 || data[position + 3] !== 0x32 || + data[position + 4] !== 0x0D || data[position + 5] !== 0x0A || + data[position + 6] !== 0x1A || data[position + 7] !== 0x0A) { + throw new Jbig2Error('parseJbig2 - invalid header.'); + } + + let header = Object.create(null); + position += 8; + const flags = data[position++]; + header.randomAccess = !(flags & 1); + if (!(flags & 2)) { + header.numberOfPages = readUint32(data, position); + position += 4; + } + + let segments = readSegments(header, data, position, end); + let visitor = new SimpleSegmentVisitor(); + processSegments(segments, visitor); + + const { width, height, } = visitor.currentPageInfo; + const bitPacked = visitor.buffer; + let imgData = new Uint8ClampedArray(width * height); + let q = 0, k = 0; + for (let i = 0; i < height; i++) { + let mask = 0, buffer; + for (let j = 0; j < width; j++) { + if (!mask) { + mask = 128; buffer = bitPacked[k++]; + } + imgData[q++] = (buffer & mask) ? 0 : 255; + mask >>= 1; + } + } + + return { imgData, width, height, }; + } + function SimpleSegmentVisitor() {} SimpleSegmentVisitor.prototype = { @@ -2095,9 +2136,16 @@ var Jbig2Image = (function Jbig2ImageClosure() { function Jbig2Image() {} Jbig2Image.prototype = { - parseChunks: function Jbig2Image_parseChunks(chunks) { + parseChunks(chunks) { return parseJbig2Chunks(chunks); }, + + parse(data) { + const { imgData, width, height, } = parseJbig2(data); + this.width = width; + this.height = height; + return imgData; + }, }; return Jbig2Image; diff --git a/src/core/jpeg_stream.js b/src/core/jpeg_stream.js index 88a2d50cf..cc8070a67 100644 --- a/src/core/jpeg_stream.js +++ b/src/core/jpeg_stream.js @@ -63,7 +63,10 @@ let JpegStream = (function JpegStreamClosure() { if (this.eof) { return; } - let jpegImage = new JpegImage(); + let jpegOptions = { + decodeTransform: undefined, + colorTransform: undefined, + }; // Checking if values need to be transformed before conversion. let decodeArr = this.dict.getArray('Decode', 'D'); @@ -81,16 +84,17 @@ let JpegStream = (function JpegStreamClosure() { } } if (transformNeeded) { - jpegImage.decodeTransform = transform; + jpegOptions.decodeTransform = transform; } } // Fetching the 'ColorTransform' entry, if it exists. if (isDict(this.params)) { let colorTransform = this.params.get('ColorTransform'); if (Number.isInteger(colorTransform)) { - jpegImage.colorTransform = colorTransform; + jpegOptions.colorTransform = colorTransform; } } + const jpegImage = new JpegImage(jpegOptions); jpegImage.parse(this.bytes); let data = jpegImage.getData(this.drawWidth, this.drawHeight, diff --git a/src/core/jpg.js b/src/core/jpg.js index 7c29935b7..b85e1d0ed 100644 --- a/src/core/jpg.js +++ b/src/core/jpg.js @@ -94,9 +94,9 @@ var JpegImage = (function JpegImageClosure() { var dctSqrt2 = 5793; // sqrt(2) var dctSqrt1d2 = 2896; // sqrt(2) / 2 - function JpegImage() { - this.decodeTransform = null; - this.colorTransform = -1; + function JpegImage({ decodeTransform = null, colorTransform = -1, } = {}) { + this._decodeTransform = decodeTransform; + this._colorTransform = colorTransform; } function buildHuffmanTable(codeLengths, values) { @@ -1013,7 +1013,7 @@ var JpegImage = (function JpegImageClosure() { } // decodeTransform contains pairs of multiplier (-256..256) and additive - var transform = this.decodeTransform; + const transform = this._decodeTransform; if (transform) { for (i = 0; i < dataLength;) { for (j = 0, k = 0; j < numComponents; j++, i++, k += 2) { @@ -1030,7 +1030,7 @@ var JpegImage = (function JpegImageClosure() { return !!this.adobe.transformCode; } if (this.numComponents === 3) { - if (this.colorTransform === 0) { + if (this._colorTransform === 0) { // If the Adobe transform marker is not present and the image // dictionary has a 'ColorTransform' entry, explicitly set to `0`, // then the colours should *not* be transformed. @@ -1039,7 +1039,7 @@ var JpegImage = (function JpegImageClosure() { return true; } // `this.numComponents !== 3` - if (this.colorTransform === 1) { + if (this._colorTransform === 1) { // If the Adobe transform marker is not present and the image // dictionary has a 'ColorTransform' entry, explicitly set to `1`, // then the colours should be transformed. diff --git a/src/pdf.image_decoders.js b/src/pdf.image_decoders.js new file mode 100644 index 000000000..d7581e830 --- /dev/null +++ b/src/pdf.image_decoders.js @@ -0,0 +1,45 @@ +/* Copyright 2018 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable no-unused-vars */ + +import { getVerbosityLevel, setVerbosityLevel } from './shared/util'; +import { Jbig2mage } from './core/jbig2'; +import { JpegImage } from './core/jpg'; +import { JpxImage } from './core/jpx'; + +// To ensure that the standalone PDF.js image decoders have the same +// browser/environment compatibility as the regular PDF.js library, +// the standard set of polyfills are thus included in this build as well. +// +// Given that the (current) image decoders don't use all of the features +// of the complete PDF.js library, e.g. they are completely synchronous, +// some of the larger polyfills are thus unnecessary. +// +// In an attempt to reduce the size of the standalone PDF.js image decoders, +// the following polyfills are currently being excluded: +// - ReadableStream +// - Promise +// - URL + +const pdfjsVersion = PDFJSDev.eval('BUNDLE_VERSION'); +const pdfjsBuild = PDFJSDev.eval('BUNDLE_BUILD'); + +export { + Jbig2mage, + JpegImage, + JpxImage, + getVerbosityLevel, + setVerbosityLevel, +}; diff --git a/src/shared/compatibility.js b/src/shared/compatibility.js index 6578c2b11..b33831e82 100644 --- a/src/shared/compatibility.js +++ b/src/shared/compatibility.js @@ -147,6 +147,11 @@ const hasDOM = typeof window === 'object' && typeof document === 'object'; // Support: IE, Safari<8, Chrome<32 (function checkPromise() { + if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('IMAGE_DECODERS')) { + // The current image decoders are synchronous, hence `Promise` shouldn't + // need to be polyfilled for the IMAGE_DECODERS build target. + return; + } if (globalScope.Promise) { return; } @@ -166,6 +171,11 @@ const hasDOM = typeof window === 'object' && typeof document === 'object'; /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ (function checkURLConstructor() { + if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('IMAGE_DECODERS')) { + // The current image decoders doesn't utilize the `URL` constructor, hence + // it shouldn't need to be polyfilled for the IMAGE_DECODERS build target. + return; + } // feature detect for URL constructor var hasWorkingUrl = false; try { diff --git a/src/shared/streams_polyfill.js b/src/shared/streams_polyfill.js index 7c8e85d87..962a26f3b 100644 --- a/src/shared/streams_polyfill.js +++ b/src/shared/streams_polyfill.js @@ -31,6 +31,17 @@ if (typeof ReadableStream !== 'undefined') { if (isReadableStreamSupported) { exports.ReadableStream = ReadableStream; } else { - exports.ReadableStream = - require('../../external/streams/streams-lib').ReadableStream; + if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('IMAGE_DECODERS')) { + class DummyReadableStream { + constructor() { + throw new Error('The current image decoders are synchronous, ' + + 'hence `ReadableStream` shouldn\'t need to be ' + + 'polyfilled for the IMAGE_DECODERS build target.'); + } + } + exports.ReadableStream = DummyReadableStream; + } else { + exports.ReadableStream = + require('../../external/streams/streams-lib').ReadableStream; + } }