diff --git a/src/index.js b/src/index.js index 12e6579e4..c88d41ce2 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,7 @@ import "./modules/png_support.js"; import "./modules/gif_support.js"; import "./modules/bmp_support.js"; import "./modules/webp_support.js"; +import "./modules/rgba_support.js"; import "./modules/setlanguage.js"; import "./modules/split_text_to_size.js"; import "./modules/standard_fonts_metrics.js"; diff --git a/src/modules/addimage.js b/src/modules/addimage.js index 955087591..3a93f5e29 100644 --- a/src/modules/addimage.js +++ b/src/modules/addimage.js @@ -139,6 +139,16 @@ import { atob, btoa } from "../libs/AtobBtoa.js"; var compareResult; var fileType; + if ( + fallbackFormat === "RGBA" || + (imageData.data !== undefined && + imageData.data instanceof Uint8ClampedArray && + "height" in imageData && + "width" in imageData) + ) { + return "RGBA"; + } + if (isArrayBufferView(imageData)) { for (fileType in imageFileTypeHeaders) { headerSchemata = imageFileTypeHeaders[fileType]; @@ -351,6 +361,8 @@ import { atob, btoa } from "../libs/AtobBtoa.js"; var generateAliasFromImageData = function(imageData) { if (typeof imageData === "string" || isArrayBufferView(imageData)) { return sHashCode(imageData); + } else if (isArrayBufferView(imageData.data)) { + return sHashCode(imageData.data); } return null; @@ -759,13 +771,22 @@ import { atob, btoa } from "../libs/AtobBtoa.js"; return out; }); + /** + * Possible parameter for addImage, an RGBA buffer with size. + * + * @typedef {Object} RGBAData + * @property {Uint8ClampedArray} data - Single dimensional array of RGBA values. For example from canvas getImageData. + * @property {number} width - Image width as the data does not carry this information in itself. + * @property {number} height - Image height as the data does not carry this information in itself. + */ + /** * Adds an Image to the PDF. * * @name addImage * @public * @function - * @param {string|HTMLImageElement|HTMLCanvasElement|Uint8Array} imageData imageData as base64 encoded DataUrl or Image-HTMLElement or Canvas-HTMLElement + * @param {string|HTMLImageElement|HTMLCanvasElement|Uint8Array|RGBAData} imageData imageData as base64 encoded DataUrl or Image-HTMLElement or Canvas-HTMLElement or object containing RGBA array (like output from canvas.getImageData). * @param {string} format format of file if filetype-recognition fails or in case of a Canvas-Element needs to be specified (default for Canvas is JPEG), e.g. 'JPEG', 'PNG', 'WEBP' * @param {number} x x Coordinate (in units declared at inception of PDF document) against left edge of the page * @param {number} y y Coordinate (in units declared at inception of PDF document) against upper edge of the page @@ -889,7 +910,7 @@ import { atob, btoa } from "../libs/AtobBtoa.js"; if (!result) { if (supportsArrayBuffer()) { // no need to convert if imageData is already uint8array - if (!(imageData instanceof Uint8Array)) { + if (!(imageData instanceof Uint8Array) && format !== "RGBA") { dataAsBinaryString = imageData; imageData = binaryStringToUint8Array(imageData); } diff --git a/src/modules/rgba_support.js b/src/modules/rgba_support.js new file mode 100644 index 000000000..551382dae --- /dev/null +++ b/src/modules/rgba_support.js @@ -0,0 +1,81 @@ +/** + * @license + * + * Copyright (c) 2021 Antti Palola, https://github.com/Pantura + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * ==================================================================== + */ + +import { jsPDF } from "../jspdf.js"; + +/** + * jsPDF RGBA array PlugIn + * @name rgba_support + * @module + */ +(function(jsPDFAPI) { + "use strict"; + + /** + * @name processRGBA + * @function + * + * Process RGBA Array. This is a one-dimension array with pixel data [red, green, blue, alpha, red, green, ...]. + * RGBA array data can be obtained from DOM canvas getImageData. + * @ignore + */ + jsPDFAPI.processRGBA = function(imageData, index, alias) { + "use strict"; + + var imagePixels = imageData.data; + var length = imagePixels.length; + // jsPDF takes alpha data separately so extract that. + var rgbOut = new Uint8Array((length / 4) * 3); + var alphaOut = new Uint8Array(length / 4); + var outIndex = 0; + var alphaIndex = 0; + + for (var i = 0; i < length; i += 4) { + var r = imagePixels[i]; + var g = imagePixels[i + 1]; + var b = imagePixels[i + 2]; + var alpha = imagePixels[i + 3]; + rgbOut[outIndex++] = r; + rgbOut[outIndex++] = g; + rgbOut[outIndex++] = b; + alphaOut[alphaIndex++] = alpha; + } + + var rgbData = this.__addimage__.arrayBufferToBinaryString(rgbOut); + var alphaData = this.__addimage__.arrayBufferToBinaryString(alphaOut); + + return { + alpha: alphaData, + data: rgbData, + index: index, + alias: alias, + colorSpace: "DeviceRGB", + bitsPerComponent: 8, + width: imageData.width, + height: imageData.height + }; + }; +})(jsPDF.API); diff --git a/test/reference/blackpixel_rgba.pdf b/test/reference/blackpixel_rgba.pdf new file mode 100644 index 000000000..0a99749a8 Binary files /dev/null and b/test/reference/blackpixel_rgba.pdf differ diff --git a/test/reference/rgba.pdf b/test/reference/rgba.pdf new file mode 100644 index 000000000..eea5cd7fa Binary files /dev/null and b/test/reference/rgba.pdf differ diff --git a/test/reference/rgba_alpha.pdf b/test/reference/rgba_alpha.pdf new file mode 100644 index 000000000..63c42688d Binary files /dev/null and b/test/reference/rgba_alpha.pdf differ diff --git a/test/specs/rgba.spec.js b/test/specs/rgba.spec.js new file mode 100644 index 000000000..67ef6c3f6 --- /dev/null +++ b/test/specs/rgba.spec.js @@ -0,0 +1,96 @@ +/* global jsPDF */ +/** + * Standard spec tests + */ + +describe("Module: RGBASupport", () => { + beforeAll(loadGlobals); + it("black pixel", () => { + var blackpixel = new Uint8ClampedArray([0, 0, 0, 255]); + var blackpixelData = { + data: blackpixel, + width: 1, + height: 1 + }; + + const doc = new jsPDF({ + orientation: "p", + unit: "pt", + format: "a4", + floatPrecision: 2 + }); + doc.addImage(blackpixelData, "RGBA", 15, 40, 1, 1); + + comparePdf(doc.output(), "blackpixel_rgba.pdf", "addimage"); + }); + + it("without format", () => { + var blackpixel = new Uint8ClampedArray([0, 0, 0, 255]); + var blackpixelData = { + data: blackpixel, + width: 1, + height: 1 + }; + + const doc = new jsPDF({ + orientation: "p", + unit: "pt", + format: "a4", + floatPrecision: 2 + }); + doc.addImage(blackpixelData, "RGBA", 15, 40, 1, 1); + + comparePdf(doc.output(), "blackpixel_rgba.pdf", "addimage"); + }); + + if ( + (typeof isNode === "undefined" || !isNode) && + navigator.userAgent.indexOf("Chrome") >= 0 + ) { + it("from canvas", () => { + const c = document.createElement("canvas"); + const ctx = c.getContext("2d"); + ctx.fillStyle = "#FF6600"; + ctx.fillRect(0, 0, 150, 75); + const dataFromCanvas = ctx.getImageData(0, 0, 150, 75); + const doc = new jsPDF({ + orientation: "p", + unit: "pt", + format: "a4", + floatPrecision: 2 + }); + doc.addImage( + dataFromCanvas, + "RGBA", + 100, + 200, + 280, + 210, + undefined, + undefined + ); + + comparePdf(doc.output(), "rgba.pdf", "addimage"); + }); + + it("with alpha", () => { + const c = document.createElement("canvas"); + const ctx = c.getContext("2d"); + ctx.fillStyle = "#FFFFFF"; + ctx.fillRect(0, 0, 150, 60); + ctx.fillStyle = "#AA00FF77"; + ctx.fillRect(10, 10, 130, 40); + const dataFromCanvas = ctx.getImageData(0, 0, 150, 60); + + const doc = new jsPDF({ + orientation: "p", + unit: "px", + format: "a4", + floatPrecision: 2 + }); + doc.addImage(dataFromCanvas, 10, 10, 150, 60); + + comparePdf(doc.output(), "rgba_alpha.pdf", "addimage"); + }); + } +}); diff --git a/types/index.d.ts b/types/index.d.ts index 8a6b91fbb..02310ae91 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -499,7 +499,12 @@ declare module "jspdf" { | "DeviceN"; export interface ImageOptions { - imageData: string | HTMLImageElement | HTMLCanvasElement | Uint8Array; + imageData: + | string + | HTMLImageElement + | HTMLCanvasElement + | Uint8Array + | RGBAData; x: number; y: number; width: number; @@ -646,6 +651,13 @@ declare module "jspdf" { yStep?: number; } + // Single dimensional array of RGBA values. For example from canvas getImageData. + export interface RGBAData { + data: Uint8ClampedArray; + width: number; + height: number; + } + export interface PubSub { subscribe( topic: string, @@ -920,7 +932,12 @@ declare module "jspdf" { // jsPDF plugin: addImage addImage( - imageData: string | HTMLImageElement | HTMLCanvasElement | Uint8Array, + imageData: + | string + | HTMLImageElement + | HTMLCanvasElement + | Uint8Array + | RGBAData, format: string, x: number, y: number, @@ -931,7 +948,12 @@ declare module "jspdf" { rotation?: number ): jsPDF; addImage( - imageData: string | HTMLImageElement | HTMLCanvasElement | Uint8Array, + imageData: + | string + | HTMLImageElement + | HTMLCanvasElement + | Uint8Array + | RGBAData, x: number, y: number, w: number, diff --git a/types/jspdf-tests.ts b/types/jspdf-tests.ts index a097a14ae..7e5b34b28 100644 --- a/types/jspdf-tests.ts +++ b/types/jspdf-tests.ts @@ -649,3 +649,22 @@ function test_nullStyleArgument() { doc.ellipse(0, 0, 0, 0, null); doc.circle(0, 0, 0, null); } + +function test_addImageWithRGBAData() { + const doc = new jsPDF(); + const rgbaData = new Uint8ClampedArray(16); + const imageData = { + data: rgbaData, + width: 2, + height: 2 + }; + + doc.addImage({ + imageData: imageData, + x: 0, + y: 0, + width: 100, + height: 100, + compression: "FAST" + }); +}