PDFKit: Unknown image format error for PNG - javascript

I'm using the prebuilt version of PDFKit in the browser, and when attempting to add a PNG I get the following error:
Uncaught Error: Unknown image
PDFImage.open format.util.js:546
module.exports.image deflate.js:773
img.onload PDFrenderer.js:195
I'm converting my images to PNG on the server (to crop them into circles) and the mime type of the images returned by the server is 'image/png'. I'm unsure whether the method I'm using to convert the PNG into an ArrayBuffer is incorrect.
Here is the code I use to fetch the PNG and convert it into an ArrayBuffer:
var img = new Image, ctxData;
img.onError = function () {
throw new Error('Cannot load image: "' + url + '"');
}
img.onload = function () {
var canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
ctxData = canvas.toDataURL('image/png').slice('data:image/png;base64,'.length);
ctxData = atob(ctxData);
document.body.removeChild(canvas);
var buffer = [];
for (var i = 0, l = ctxData.length; i < l; i++) {
buffer.push(ctxData.charCodeAt(i));
buffer._isBuffer = true;
buffer.readUInt16BE = function (offset, noAssert) {
var len = this.length;
if (offset >= len) return;
var val = this[offset] << 8;
if (offset + 1 < len)
l |= this[offset + 1];
return val;
}
}
pdf.image(buffer);
}
img.src = url;
This works fine for JPEGs when this line is changed from
ctxData = canvas.toDataURL('image/png').slice('data:image/png;base64,'.length);
to
ctxData = canvas.toDataURL('image/jpeg').slice('data:image/jpeg;base64,'.length);
however I need to be able to pass in PNGs so that I can place I can place round images over any background.
I've also tried passing in the full url (e.g. 'http://mysite.dev/userimages/1234/roundavatar.png'), however I'm then presented with the following error:
Uncaught TypeError: undefined is not a function
PDFImage.open util.js:535
module.exports.image deflate.js:773
Has anyone had any success adding PNGs to PDFkit via the browser, and if so what method did you use?

I found a solution as per this https://github.com/devongovett/pdfkit/blob/master/lib/image.coffee just make sure your name your png files with at capital ending .PNG, it worked for me.

Related

Sending image manipulated via JS in AJAX POST request

I'm a server-side dev learning the ropes of vanilla JS. I need to clear my concepts regarding sending an Ajax POST request for an image object I'm creating in JS - this question is about that.
Imagine a web app where users upload photos for others to see. At the point of each image's upload, I use vanilla JS to confirm the image's mime-type (via interpreting magic numbers), and then resize the image for optimization purposes.
After resizing, I do:
var canvas = document.createElement('canvas');
canvas.width = resized_width;
canvas.height = resized_height;
var ctx = canvas.getContext("2d");
ctx.drawImage(source_img, 0, 0, resized_width, resized_height);
var resized_img = new Image();
resized_img.src = canvas.toDataURL("image/jpeg",0.7);
return resized_img;
The image object returned has to be sent to the backend via an Ajax request. Something like:
function overwrite_default_submit(e) {
e.preventDefault();
var form = new FormData();
form.append("myfile", resized_img, img_name);
var xhr = new XMLHttpRequest();
xhr.open('POST', e.target.action);
// xhr.send(form); // uncomment to really send the request
}
However, the image object returned after resizing is essentially an HTML element like so <img src="data:image/jpeg;base64>. Whereas the object expected in the FormData object ought to be a File object, e.g. something like: File { name: "example.jpg", lastModified: 1500117303000, lastModifiedDate: Date 2017-07-15T11:15:03.000Z, webkitRelativePath: "", size: 115711, type: "image/jpeg" }.
So what do I do to fix this issue? Would prefer to learn the most efficient way of doing things here.
Btw, I've seen an example on SO of using the JS FILE object, but I'd prefer a more cross-browser method, given File garnered support from Safari, Opera Mobile and built-in Android browsers relatively recently.
Moreover, only want pure JS solutions since I'm using this as an exercise to learn the ropes. JQuery is on my radar, but for later.
The rest of my code is as follows (only included JPEG processing for brevity):
var max_img_width = 400;
var wranges = [max_img_width, Math.round(0.8*max_img_width), Math.round(0.6*max_img_width),Math.round(0.4*max_img_width),Math.round(0.2*max_img_width)];
// grab the file from the input and process it
function process_user_file(e) {
var file = e.target.files[0];
var reader = new FileReader();
reader.onload = process_image;
reader.readAsArrayBuffer(file.slice(0,25));
}
// checking file type programmatically (via magic numbers), getting dimensions and returning a compressed image
function process_image(e) {
var img_width;
var img_height;
var view = new Uint8Array(e.target.result);
var arr = view.subarray(0, 4);
var header = "";
for(var i = 0; i < arr.length; i++) {
header += arr[i].toString(16);
}
switch (header) {
case "ffd8ffe0":
case "ffd8ffe1":
case "ffd8ffe2":
case "ffd8ffe3":
case "ffd8ffe8":
// magic numbers represent type = "image/jpeg";
// use the 'slow' method to get the dimensions of the media
img_file = browse_image_btn.files[0];
var fr = new FileReader();
fr.onload = function(){
var dataURL = fr.result;
var img = new Image();
img.onload = function() {
img_width = this.width;
img_height = this.height;
resized_img = resize_and_compress(this, img_width, img_height, 80);
}
img.src = dataURL;
};
fr.readAsDataURL(img_file);
to_send = browse_image_btn.files[0];
load_rest = true;
subform.disabled = false;
break;
default:
// type = "unknown"; // Or one can use the blob.type as fallback
load_rest = false;
subform.disabled = true;
browse_image_btn.value = "";
to_send = null;
break;
}
}
// resizing (& compressing) image
function resize_and_compress(source_img, img_width, img_height, quality){
var new_width;
switch (true) {
case img_width < wranges[4]:
new_width = wranges[4];
break;
case img_width < wranges[3]:
new_width = wranges[4];
break;
case img_width < wranges[2]:
new_width = wranges[3];
break;
case img_width < wranges[1]:
new_width = wranges[2];
break;
case img_width < wranges[0]:
new_width = wranges[1];
break;
default:
new_width = wranges[0];
break;
}
var wpercent = (new_width/img_width);
var new_height = Math.round(img_height*wpercent);
var canvas = document.createElement('canvas');
canvas.width = new_width;
canvas.height = new_height;
var ctx = canvas.getContext("2d");
ctx.drawImage(source_img, 0, 0, new_width, new_height);
console.log(ctx);
var resized_img = new Image();
resized_img.src = canvas.toDataURL("image/jpeg",quality/100);
return resized_img;
}
Update: I'm employing the following:
// converting image data uri to a blob object
function dataURItoBlob(dataURI,mime_type) {
var byteString = atob(dataURI.split(',')[1]);
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); }
return new Blob([ab], { type: mime_type });
}
Where the dataURI parameter is canvas.toDataURL(mime_type,quality/100)
You should call the canvas.toBlob() to get the binary instead of using a base64 string.
it's async so you would have to add a callback to it.
canvas.toBlob(function(blob) {
resized_img.onload = function() {
// no longer need to read the blob so it's revoked
URL.revokeObjectURL(this.url);
};
// Preview the image using createObjectURL
resized_img.src = URL.createObjectURL(blob);
// Attach the blob to the FormData
var form = new FormData();
form.append("myfile", blob, img_name);
}, "image/jpeg", 0.7);
See to this SO post: How to get base64 encoded data from html image
I think you need to call 'canvas.toDataURL()' to get the actual base64 stream of the image.
var image = canvas.toDataURL();
Then upload it with a Form: Upload a base64 encoded image using FormData?
var data = new FormData();
data.append("image_data", image);
Untested, but this should be about it.

How to add a 16x16 grid of pseudo-random tiles as a background in canvas (or other web language)

I have recently been working on a web based project using canvas on HTML5. The program consists of a 16x16 grid of tiles that have been pseudo-randomly generated. I am relatively new to canvas, but have built this program in several other environments, none of which however compile successfully to a web based language. this is the main code section that is giving me bother:
var A = 8765432352450986;
var B = 8765432352450986;
var M = 2576436549074795;
var X = 1;
var rx = 0;
var ry = 0;
this.image = new Image();
var i = 0;
var ii = 0;
while(i < 16)
{
while(ii < 16)
{
this.image = new Image();
this.image.src = "textures/grass.png";
x = (((A*X)+B)%M)%M;
if((x/2)%1 == 0)
{
this.image.src = "textures/grass.png";
}
if((x/8)%1 == 0)
{
this.image.src = "textures/hill.png";
}
if((x/21)%1 == 0)
{
this.image.src = "textures/trees.png";
}
if((x/24)%1 == 0)
{
this.image.src = "textures/sea.png";
}
if((x/55)%1 == 0)
{
this.image.src = "textures/mountain.png";
}
if((x/78)%1 == 0)
{
this.image.src = "textures/lake.png";
}
if((x/521)%1 == 0)
{
this.image.src = "textures/volcano.png";
}
if((x/1700)%1 == 0)
{
this.image.src = "textures/shrine.png";
}
if((x/1890)%1 == 0)
{
this.image.src = "textures/outpost.png";
}
if((x/1999)%1 == 0)
{
this.image.src = "textures/civ.png";
}
ctx = myGameArea.context;
ctx.drawImage(this.image,rx, ry, 20, 20);
ii ++;
rx += 20;
}
i ++;
rx = 0;
ry += 16;
}
I would like canvas to draw along the lines of this code above, effectively generating a grid like this
pre generated grid image
(please try and ignore the obvious bad tile drawings, I planned on either finding an artist or trying slightly harder on them when I get the game fully working.)
The black square is a separate movable object. I haven't got as far as implementing it in this version, but if you have any suggestions for it please tell me
in the full html file I have now, the canvas renders but none of the background (using the w3schools tutorials, I can make objects render however)
In short: how do I render a background consisting of a 16x16 grid of pseudo-random tiles on an event triggered or on page loaded, using canvas or if that does not work another web based technology
Thank you for your time.
A few problems but the main one is that you need to give an image some time to load before you can draw it to the canvas.
var image = new Image();
image.src = "image.png";
// at this line the image may or may not have loaded.
// If not loaded you can not draw it
To ensure an image has loaded you can add a onload event handler to the image
var image = new Image();
image.src = "image.png";
image.onload = function(){ ctx.drawImage(image,0,0); }
The onload function will be called after all the current code has run.
To load many images you want to know when all have loaded. One way to do this is to count the number of images you are loading, and then use the onload to count the number of images that have loaded. When the loaded count is the same as the loading count you know all have loaded and can then call a function to draw what you want with the images.
// Array of image names
const imageNames = "grass,hill,trees,sea,mountain,lake,volcano,shrine,outpost,civ".split(",");
const images = []; // array of images
const namedImages = {}; // object with named images
// counts of loaded and waiting toload images
var loadedCount = 0;
var imageCount = 0;
// tile sizes
const tileWidth = 20;
const tileHeight = 20;
// NOT SURE WHERE YOU GOT THIS FROM so have left it as you had in your code
// Would normally be from a canvas element via canvasElement.getContext("2d")
var ctx = myGameArea.context;
// seeded random function encapsulated in a singleton
// You can set the seed by passing it as an argument rand(seed) or
// just get the next random by not passing the argument. rand()
const rand = (function(){
const A = 8765432352450986;
const B = 8765432352450986; // This value should not be the same as A?? left as is so you get the same values
const M = 2576436549074795;
var seed = 1;
return (x = seed) => seed = ((A * x) + B) % M;
}());
// function loads an image with name
function addImage(name){
const image = new Image;
image.src = "textures/" + name + ".png";
image.onload = () => {
loadedCount += 1;
if(loadedCount === imageCount){
if(typeof allImagesLoaded === "function"){
allImagesLoaded();
}
}
}
imageCount += 1;
images.push(image);
namedImages[name] = image;
}
imageNames.forEach(addImage); // start loading all the images
// This function draws the tiles
function allImagesLoaded(){ /// function that is called when all the images have been loaded
var i, x, y, image;
for(i = 0; i < 256; i += 1){ // loop 16 by 16 times
ctx.drawImage(
images[Math.floor(rand()) % images.length]; //random function does not guarantee an integer so must floor
(i % 16) * tileWidth, // x position
Math.floor(i / 16) * tileHeight, // y position
tileWidth, tileHeight // width and height
);
}
}

Black images from canvas.toDataURL()

I have created a web application where users can upload images from their filesystem but in few cases I only get black image data on the server side. I have never got this issue in my development environment on lan even with the original images causing the error. This failure occurs overall only sporadic but especially one user is affected. All users should work with the latest firefox. I have read most of the question about this topic and I dont think this is related to a security issue, the jpg/png setting or preserveDrawingBuffer. Because I have never got this problem on lan with a fast PC and the fact that often the last selected images are affected it seems to be a synchronization problem.
To avoid this I work with $.Deferred and Callbacks but may be I missed something. First I load and convert the selected images directly into a list of canvas. The upload is disabled until this is finished. During the upload I just loop through the canvas list and read the data (canvas.toDataURL()) with a $.Deferred and transfer it to the server via ajax.
Get the images to the page:
var file = document.getElementById('fileSelect');
if (file != undefined) {
file.onchange = function (e) {
btnUpload.SetEnabled(false);
loadImgs(e, function () {
btnUpload.SetEnabled(true);
});
};
}
function loadImgs(e, callback) {
for (var t = 0; t < e.target.files.length; t++) {
if ($('.CanvasClass').length <= 20) {
var myTmpImg = new Image();
var myName = e.target.files[t].name;
myTmpImg.src = URL.createObjectURL(e.target.files[t]);
myTmpImg.onload = function () {
var GrenzeX = 1024;
var GrenzeY = 768;
var myFactor = 1;
if (myTmpImg.naturalWidth > GrenzeX || myTmpImg.naturalHeight > GrenzeY) {
if ((GrenzeX / myTmpImg.naturalWidth) < (GrenzeY / myTmpImg.naturalHeight)) {
myFactor = GrenzeX / myTmpImg.naturalWidth;
} else {
myFactor = GrenzeY / myTmpImg.naturalHeight;
}
}
var myCanvas = document.createElement('canvas');
myCanvas.className = "CanvasClass";
myCanvas.height = this.naturalHeight * myFactor;
myCanvas.width = this.naturalWidth * myFactor;
var ctx = myCanvas.getContext("2d");
ctx.drawImage(this, 0, 0, myCanvas.width, myCanvas.height);
$("#myImages").append(myCanvas);
myMetaData.push(new newImgData('', '', myName));
}
}
}
if (callback && typeof callback == "function") {
callback();
}
}
Read and transfer the Data:
$('.CanvasClass').each(function () {
var fd = new FormData();
var base64Data = getBase64Image(this, false);
base64Data.then(function (result) {
//Create Ajax Request
});
});
function getBase64Image(myCanvas, modus) {
var deferred = $.Deferred();
var dataURL = myCanvas.toDataURL("image/jpeg", 0.9);
if (modus === true) {
deferred.resolve(dataURL);
} else {
deferred.resolve(dataURL.replace(/^data:image\/(png|jpeg);base64,/, ""));
}
return deferred.promise();
}
I can also exclude issues during the data transfer because I logged the size of the images on clientside before uploading and compared this to the data at serverside and it was the same.

using angularjs and JIC to upload file to S3

I have angular upload to S3 working, but I would like to compress (or shrink) the image file down to height 250 px. Using angular upload:
$scope.onFileSelect = function($files) {
$scope.files = $files;
$scope.upload = [];
for (var i = 0; i < $files.length; i++) {
var file = $files[i];
console.log('file: ' + JSON.stringify(file));
file.progress = parseInt(0);
....
and then process with the upload. This works great... except for the size issue. So, I ended up trying to use a library called JIC (https://github.com/brunobar79/J-I-C) to handle the compression. The result of my console.log(file) is:
file: {"webkitRelativePath":"","lastModifiedDate":"2014-04-25T19:35:22.000Z","name":"pooltable.jpg","type":"image/jpeg","size":1922990}
I try passing this file to compress and then proceed to upload to S3, but it does not work:
var file = compress($files[i], 50, $files[i].type);
Here is the compress code from JIC which I modified to fit my heigh requirement:
var compress = function(source_img_obj, quality, output_format){
var mime_type = "image/jpeg";
if(output_format != undefined && output_format == "png"){
mime_type = "image/png";
}
var cvs = document.createElement('canvas');
var ratio = 250 / source_img_obj.naturalHeight;
cvs.width = ratio * source_img_obj.naturalWidth;
cvs.height = 250;
var ctx = cvs.getContext("2d").drawImage(source_img_obj, 0, 0);
var newImageData = cvs.toDataURL(mime_type, ratio/100);
var result_image_obj = new Image();
result_image_obj.src = newImageData;
return result_image_obj;
};
The problem here is that my source_img_obj doesn't come from HTML, it comes from the user uploading the image from their local machine so it does not have a src property and I get this error:
Failed to execute 'drawImage' on 'CanvasRenderingContext2D': No function was found that matched the signature provided.
I don't actually want to draw the images into my html file, I just want to shrink them and upload them directly to the server.

<img>width returns 0 under heavy load

The below handleFiles method is being passed files from both drag and drop and a file input. After it gets the data url for a given file it passes it to the processImage function. This function creates a new image and sets the src and file for that image. I then take different actions based on the width of the incoming image and insert the image into the dom. However, I noticed when dropping in a bunch of images imageWidth will get set to 0. I have confirmed the image.src is correctly set and that dropping the same image in by itself works fine. I also have confirmed that if I remove the width calculations the image does display correctly on the page. When I enter the debugger I can confirm that immediately after imageWidth is set to 0 i.width returns a correct value. At first I thought it might be a threading issue in Chrome, but then when I saw it happen in FireFox as well I became alarmed. I have not been able to pinpoint a certain threshold, but the more load you put on the browser the less likely it is to correctly get the width.
I am seeing the problem in both FireFox 16.0.2 and Chrome 23.0.1271.95.
function handleFiles(files) {
for (var i = 0; i < files.length; i++) {
var file = files[i];
if( !isImage(file) ) {
continue;
}
var reader = new FileReader();
reader.onloadend = function(e) {
var dataURL = e.target.result;
processImage(file, dataURL);
}
reader.readAsDataURL(file);
}
}
function processImage(file, dataURL) {
var i = new Image();
i.src = dataURL;
i.file = file;
//console.log(i);
var maxWidth = 600;
var imageWidth = i.width;
......
}
As with all images, they may need time to load before they will tell you their width:
var i = new Image();
i.onload = function() {
//console.log(i);
var maxWidth = 600;
var imageWidth = this.width;
}
i.src = dataURL;
i.file = file;
The width (and height) might be 0 because it's not loaded yet.
Try adding the load event like so:
function processImage(file, dataURL) {
var i = new Image();
i.addEventListener("load", function () {
var maxWidth = 600;
var imageWidth = i.width;
......
});
i.src = dataURL;
i.file = file;
}

Categories

Resources