From b6ed102d71d9dd9bf5bd238c988918460c4552cc Mon Sep 17 00:00:00 2001 From: Antti Palola Date: Thu, 9 Sep 2021 11:30:22 +0300 Subject: [PATCH] Feature/add rgba array support (#3124) Co-authored-by: Antti Palola Co-authored-by: Lukas Hollaender --- src/index.js | 1 + src/modules/addimage.js | 25 +++++++- src/modules/rgba_support.js | 81 ++++++++++++++++++++++++ test/reference/blackpixel_rgba.pdf | Bin 0 -> 3227 bytes test/reference/rgba.pdf | Bin 0 -> 36987 bytes test/reference/rgba_alpha.pdf | Bin 0 -> 30238 bytes test/specs/rgba.spec.js | 96 +++++++++++++++++++++++++++++ types/index.d.ts | 28 ++++++++- types/jspdf-tests.ts | 19 ++++++ 9 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 src/modules/rgba_support.js create mode 100644 test/reference/blackpixel_rgba.pdf create mode 100644 test/reference/rgba.pdf create mode 100644 test/reference/rgba_alpha.pdf create mode 100644 test/specs/rgba.spec.js 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 0000000000000000000000000000000000000000..0a99749a8f9e57c30f25c51d62060ce595f87993 GIT binary patch literal 3227 zcmcIm?{3;i5YOj*iun>n><$zq*@+%n$jg;9Nb3zr-jVK;2a2%n+AYX|~{-*cB~wPL&_;V;JF4g17I3xzr0Lxe4eI7i(Oub4OHFtqFs z(pfGmwUe`rP;%z|ZWcLQQlL{PyxUM@WVT!w-Y;Y&2qTPud3}{@qQE=T$!G0Mte7|3 z%v}K#!6lgYI*skZq)YJLoAgzhn~K4_k;!s5xK|y$L-Z3)5DW)%q`TLV0iFe;v**V^P{LQ|V|yCwiCEe`P$r0nBlM$_ zP&sfg`~{Vmr-N`fKOQRMkFdBfE1jQA?MkHwNQT{F`Tb0qcon^;fZ^sze{m!IowU{`CK(y$QSH+6rf zrym_1HCy%W89&c#Jkt&?J)BIyyU@4Ta0_qx<#SLz$Jo7#e%$LJA%Id8r<0Kc$`zt~ zpbaA`00Nb$5GZ`2+E7$jB0EsjIwB(|N)C}-5XG2)Bua|y_=2>+8Kl&KvZAtv!lJT= z!o@aDDRi%d7syeZSH#u>{PHk#$lYR(7%Voi&fvvOPAKpe{$3*VI2Ji$)1h)2UJQQ| z-KZm^;M?n0&VRxG%U!L_3O-PHLhX`fx+P9<-1Gjx~tI$#}#)5=_W}KW|DoY z?(7ElVv=f1fz2x2v2&a0D;(6MWTh|ofOZ)AAzVSllY|IekXAfop#zV3oD;e=o{)%s zjVC)zxd64- zlCO2~QQJIAg*;n~?NZB`DIs5$nVIC}l;qpId{Nf?P2~8Z;bcz-&+z7?~T8v^Q#{3|Krs1S+l(@KI#Zi4{v^Fx3jS+ z{_FU=d)m9<_*!Rt#@!`6m@QBLxA}yRW)*pY550W)-cl!W$9iF^vw52FuY{idm}h$Q z>Vyx-+}5YM>YCZylwH>H3&O(~?fDMgetr8X(ADRxMyrWliQ>9~OsQbAMXmMUDA6{&SB zZmZVOxUpKt1TK{ggUKS(6+OGIor2fm|K~z> z8O`RV?4?%Iv)+#?i95TmGH>-MUx9(s2xQ?L|3$~ z^L-xK{ZZM}1yK_6yg#es4^DThPWi0ARp-aMeM4`zughY)Z@bNRXKTFp3w;xRr=9X5 zzRREYRVwCtx>XnWJ8@MQZ5!}xcjuux-tKsD$P05TMiTD0_}-3b&{oOzctP8bH}bPp znislSK7wcYZ`$2UIWoqQ-zm0TSv7`E4y4pK|6>O Kad7Z**#8G$0@SPk literal 0 HcmV?d00001 diff --git a/test/reference/rgba_alpha.pdf b/test/reference/rgba_alpha.pdf new file mode 100644 index 0000000000000000000000000000000000000000..63c42688de196af58fae2e13b8482df2f74f0fb5 GIT binary patch literal 30238 zcmeI5(Qex|6o&8l6z3*biy@XsTedV90cyK$jdpNm2k5Y%7qZMa3T)Ap|yo< zdy!pkZ_*vga^gtsl8!OTi{k{gO!0`nzMmp}_aB`OhR3|oWJiDg{l`D72`;GF73+4L zvzxU9=QLbMhVQ9Na^QG&!kmfB)FzE&24dUrqnyWKPhG>giw$1jHG~g-i#Pn>J#+dh z$+0?vR?++9gvn&C2hrcRgE8l=OcwbicrL^9S)R&p#oUJIz^}}OSL_25uG_%=F8KJ# zn~i1@IxWB9wINzDXXL^_v7hvn%i?kjExC3>8FSvm^9;`Q?M|T3xY75g2QLe~+d@{N z7~*{}rx#{YAA$2)E4H?zFJrJ-@=yi6iBkCO#B@8aY|lEu3Q<_zOB%lnsM z3Qd8x9hP5&=j{#Ne*ECaJ++)ay@JkHC>YO{@rMmglc&bue}xE*;b2072Zi+UiHK$T z^bEM3^Sh^H`%G3RVmbDKW&*!^gue77>;xQ)Ki3P*;~^YQ$KCq)B^GD#N@m9swF;91 zOosg>^YfEoJN{u71^)1&xE+3!hs!uR!2LhGFy7t&!9+Z~HvST>F9x{&&%zw{n(DIn zs4aj$y!cITRx`Eyzt^v?X+Mu=S2Dsq?pE+oCHf2?O~tj^yN34>!m-&rW69SF5#3^sfg0c6S#Y*WKUXKhcW_5CI}U1c(3;AOb{y z2oM1xKm>>Y5g-CYz%l}V{r-vmzc~SuN%76alK>=O@*;pX5FiI_aKS_aIcP%9hFWsa z1{X{;kb@@lY^Ws%ZE(Rv1373y&xTrZ&;}PwG?0TP^lYdl2W@b{L<2c!LeGX;a?l1B zOf-;#CiHBmB?oPA!9)W&XhP41T5`|^7fdvegC_KBs3iw&aKS_aIcP%9hFWsa1{X{; zkb@@lY^Ws%ZE(Rv1373y&xTrZ&;}PwG?0TP^lYdl2W@b{L<2c!LeGX;a?l1BOf-;# zCiHBmB?oPA!9)W&XhP41T5`|^7fdvegC_KBs3iw&aKS_aIcP%9hFWsa1{X{;kb@@l zY^Ws%ZE(RvgNcJwO9Y4j5g-CYfCvx)B0vO)01+SpM1Tko0U~e+0`-{0{d~34R7F#n z!?}amkl>ujYYaOqlAwC!XTSU$VRB-Th1l)l0S6rqhWU^~0XnBkA7MyidDH?OXrA5RkAnEbt*3~DN_ixm7?FF?RiZPE@0Ot1QIOHBxRcjcz^=# zwc!^{x6;OIwXQ4tdTq-^H!8JWv)(-fbL(E!e&Td4Vux0A { + 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" + }); +}