Convert HTML to PDF Using OffScreenCanvas in React - jspdf

OK so the question is self-explanatory.
Currently, I am doing the following
Using html2canvas npm package to convert my html to canvas.
Converting Canvas to image
canvas.toDataURL("image/png");
Using jsPDF to create a pdf using this image after some manipulations.
So basically all of this process is CPU intensive and leads to unresponsive page.
I'm trying to offload the process to a web worker and I'm unable to postMessage either HTML Node or Canvas Element to my worker thread.
Hence, trying to use OffScreenCanvas but I'm stuck about how to go on with this.

The first step can not be done in a Web-Worker. You need access to the DOM to be able to draw it, and Workers don't have access to the DOM.
The second step could be done on an OffscreenCanvas, html2canvas does accept a { canvas } parameter that you can also set to an OffscreenCanvas.
But once you get a context from an OffscreenCanvas you can't transfer it anymore, so you won't be able to pass that OffscreenCanvas to the Worker and you won't win anything from it since everything will still be done on the UI thread.
So the best we can do is to let html2canvas initialize an HTMLCanvasElement, draw on it and then convert it to an image in a Blob. Blobs can traverse realms without any cost of copying and the toBlob() method can have its compression part be done asynchronously.
The third step can be done in a Worker since this PR.
I don't know react so you will have to rewrite it, but here is a bare JS implementation:
script.js
const dpr = window.devicePixelRatio;
const worker = new Worker( "worker.js" );
worker.onmessage = makeDownloadLink;
worker.onerror = console.error;
html2canvas( target ).then( canvas => {
canvas.toBlob( (blob) => worker.postMessage( {
source: blob,
width: canvas.width / dpr, // retina?
height: canvas.height / dpr // retina?
} ) );
} );
worker.js
importScripts( "https://unpkg.com/jspdf#latest/dist/jspdf.umd.min.js" );
onmessage = ({ data }) => {
const { source, width, height } = data;
const reader = new FileReaderSync();
const data_url = reader.readAsDataURL( source );
const doc = new jspdf.jsPDF( { unit: "px", format: [ width, height ], orientation: width > height ? "l" : "p" });
doc.addImage( data_url, "PNG", 0, 0, width, height, { compression: "NONE" } );
postMessage( doc.output( "blob" ) );
};
And since StackSnippet's over-protected iframes do break html2canvas, here is an outsourced live example.

Related

Konva load stage with images and background video

Hey guys so I have this konva js app that has your typical background video just like the demo or close to it. I am able to play the video background and add image objects on top of it etc. Now I want a way to save the stage in its current state. So I do state.stage.toJSON() and as expected it creates a serialized JSON object. Now here's where I am hung up. when I load the stage like so state.stage = Konva.Node.create(data.stage, "container"); the stage data gets loaded (it is the correct size and so forth) yet there is no background video, no images or anything how do I fix this? I don't even know if stage.toJSON is correct but the point is I need to save it, leave the page and load it back up at a future date.
state.backgroundVideo = new Konva.Image({
image: state.video,
draggable: false
});
state.video.addEventListener("loadedmetadata", function(e) {
state.backgroundVideo.width(state.width);
state.backgroundVideo.height(state.height);
});
state.anim = new Konva.Animation(function() {
// do nothing, animation just need to update the layer
}, state.layer);
state.layer.add(state.backgroundVideo);
state.layer.batchDraw();
const canvas = document.getElementsByTagName("canvas")[0];
state.canvas = canvas;
node.toJSON() doesnt serialize image or video objects from Konva.Image attributes.
To solve the issue you can save video or image src into custom attribute:
image.setAttr('source', imageURL);
A simple string will be serialized into JSON. Then after you created a node from the JSON Konva.Node.create(data.stage, "container");
You need to find such nodes and restore image or video manually
// "video-background" is a sample here
// you can define your own selector
const videoNode = stage.findOne('.video-background');
const source = videoNode.getAttr('source');
const video = document.createElement('video');
video.src = source;
videoNode.image(video);
For more information take a look here:
https://konvajs.org/docs/data_and_serialization/Complex_Load.html
https://konvajs.org/docs/data_and_serialization/Best_Practices.html
thanks #lavrton your comments really helped me figure it out. Rather than loading the stage directly however I just saved the data from the stage used the data to rebuild the stage in the exact same way. Heres the code I used to get the data to save.
Heres my code:
const groups = state.stage.find("Group");
const stageData = [];
for (let x = 0; x < groups.length; x++) {
let g = groups[x];
let i = g.findOne("Image").getAttrs();
let group = { x: g.getX(), y: g.getY() };
let image = { x: i.width, y: i.height };
let obj = { image, group, jsonId: g.getAttr("jsonId") };
stageData.push(obj);
}
console.log("stageData", stageData);

Debugging in WebGL

I am learning WebGL and I can feel that my speed is so slow because I am having a hard time debugging my code. Is there any extension or tool with help of which I can know the value of buffer, attribpointer, matrixes, etc.
I googled and learned about chrome extension spector.js but this does not work with me. I think it supposes to show me frames or context but when I click it shows nothing.
When I click red button after a few seconds it shows:
No frames detected. Try moving the camera or implementing requestAnimationFrame.
Yes, WebGL is hard to debug and I'm not sure anything will make it a whole lot easier. Most bugs are not something a debugger can find that easily. Certain bugs like un-renderable textures or buffers on the correct size already get reported by the browser. Other bugs though are usually math bugs, logic bugs, or data bugs. For example there is no easy way to step through a WebGL shader.
In any case, if you want to use spector you need to structure your code to be spector friendly. Spector is looking for frames based on requestAnimationFrame.
So, let's take this example which is the last example from this page.
The code has a main function that looks like this
function main() {
// Get A WebGL context
/** #type {HTMLCanvasElement} */
var canvas = document.querySelector("#canvas");
var gl = canvas.getContext("webgl");
if (!gl) {
return;
}
// setup GLSL program
var program = webglUtils.createProgramFromScripts(gl, ["vertex-shader-3d", "fragment-shader-3d"]);
...
}
main();
I changed it to this. I renamed main to init and made it so I pass in the gl context.
function init(gl) {
// setup GLSL program
var program = webglUtils.createProgramFromScripts(gl, ["vertex-shader-3d", "fragment-shader-3d"]);
...
}
Then I made a new main that looks like this
function main() {
// Get A WebGL context
/** #type {HTMLCanvasElement} */
var canvas = document.querySelector("#canvas");
var gl = canvas.getContext("webgl");
if (!gl) {
return;
}
const startElem = document.querySelector('button');
startElem.addEventListener('click', start, {once: true});
function start() {
// run the initialization in rAF since spector only captures inside rAF events
requestAnimationFrame(() => {
init(gl);
});
// make so more frames so spector has something to look at.
// Note: a normal webgl app would have a rAF loop: https://webglfundamentals.org/webgl/lessons/webgl-animation.html
requestAnimationFrame(() => {});
requestAnimationFrame(() => {});
requestAnimationFrame(() => {});
requestAnimationFrame(() => {});
requestAnimationFrame(() => {});
}
}
main();
And I added a button to my html
<button type="button">start</button>
<canvas id="canvas"></canvas>
The code is the way it is because we need to get a webgl context first or else spector will not notice the canvas (there will be nothing to select). After when turn to turn on spector, and only after that click the start button to run our code. We need to execute our code in a requestAnimationFrame because that is what spector is looking for. It only records WebGL functions between frames.
Whether or not it will help you find any bugs though is another matter.
note that, if you are on Mac, Safari also has a WebGL debugger built in but just like spector it's only designed for frames. It requires you to draw something each frame so this worked
function start() {
// I'm not sure running the init code in a rAF is important in Safari but it worked
requestAnimationFrame(() => {
init(gl);
});
// by default safari tries to capture 3 frames so let's give it some frames
// Note: a normal webgl app would have a rAF loop: https://webglfundamentals.org/webgl/lessons/webgl-animation.html
requestAnimationFrame(() => { gl.clear(gl.COLOR_BUFFER_BIT); });
requestAnimationFrame(() => { gl.clear(gl.COLOR_BUFFER_BIT); });
requestAnimationFrame(() => { gl.clear(gl.COLOR_BUFFER_BIT); });
requestAnimationFrame(() => { gl.clear(gl.COLOR_BUFFER_BIT); });
requestAnimationFrame(() => { gl.clear(gl.COLOR_BUFFER_BIT); });
}
Another thing you can do is use a helper to call gl.getError after every WebGL function. Here's a script you can use
<script src="https://greggman.github.io/webgl-helpers/webgl-gl-error-check.js"></script>
You can either download it or you can just include it via the link above. Example (open the javascript console to see th error)
const gl = document.createElement('canvas').getContext('webgl');
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.vertexAttribPointer(0, 1, gl.BYE, false, 0, 0);
<script src="https://greggman.github.io/webgl-helpers/webgl-gl-error-check.js"></script>

transparent background possible for webassembly gl canvas in browser?

I'm trying to find a way to draw, by OpenGL calls in C++, to a HTML element, in such a way that anything behind the canvas (background image, HTML text, ...) is visible where the GL context framebuffer has no opaque color to be drawn, or maybe even with the ability to use blending.
I've tried setting opacity to zero in the glClearColor() call. I use emscripten to compile my C++ code, and when I use it to generate a module loader, I've noticed that there's code in the generated output to make the canvas element's background color black when no background color has explicitly been set. I've tried disabling this behaviour, in the hopes of gaining transparency, to no avail.
I know that Unity's WebGL version supports transparent canvases, as is demonstrated here. But I'm not sure if the Unity of which spoken actually uses WebAssembly, because I do know the JavaScript end to a canvas element can be used to draw on a transparent background.
Is this already possible? Will it ever be?
Of course it's 100% possible because WebGL can do it. You can always fork the emscripten libraries,change a few lines, and use your fork. Unfortunately I can't give you an answer unless you specify how your are intializing OpenGL in emscripten. SDL? EGL? GLEW? GLUT? Each will have a different answer.
The first thing I would do is go look at the source for these libraries.
For SDL we see this
$SDL: {
defaults: {
width: 320,
height: 200,
// If true, SDL_LockSurface will copy the contents of each surface back to the Emscripten HEAP so that C code can access it. If false,
// the surface contents are captured only back to JS code.
copyOnLock: true,
// If true, SDL_LockSurface will discard the contents of each surface when SDL_LockSurface() is called. This greatly improves performance
// of SDL_LockSurface(). If discardOnLock is true, copyOnLock is ignored.
discardOnLock: false,
// If true, emulate compatibility with desktop SDL by ignoring alpha on the screen frontbuffer canvas. Setting this to false will improve
// performance considerably and enables alpha-blending on the frontbuffer, so be sure to properly write 0xFF alpha for opaque pixels
// if you set this to false!
opaqueFrontBuffer: true
},
and this
var webGLContextAttributes = {
antialias: ((SDL.glAttributes[13 /*SDL_GL_MULTISAMPLEBUFFERS*/] != 0) && (SDL.glAttributes[14 /*SDL_GL_MULTISAMPLESAMPLES*/] > 1)),
depth: (SDL.glAttributes[6 /*SDL_GL_DEPTH_SIZE*/] > 0),
stencil: (SDL.glAttributes[7 /*SDL_GL_STENCIL_SIZE*/] > 0),
alpha: (SDL.glAttributes[3 /*SDL_GL_ALPHA_SIZE*/] > 0)
};
for EGL there's this
var LibraryEGL = {
$EGL__deps: ['$Browser'],
$EGL: {
// This variable tracks the success status of the most recently invoked EGL function call.
errorCode: 0x3000 /* EGL_SUCCESS */,
defaultDisplayInitialized: false,
currentContext: 0 /* EGL_NO_CONTEXT */,
currentReadSurface: 0 /* EGL_NO_SURFACE */,
currentDrawSurface: 0 /* EGL_NO_SURFACE */,
alpha: false,
For GLUT there is this
glutCreateWindow: function(name) {
var contextAttributes = {
antialias: ((GLUT.initDisplayMode & 0x0080 /*GLUT_MULTISAMPLE*/) != 0),
depth: ((GLUT.initDisplayMode & 0x0010 /*GLUT_DEPTH*/) != 0),
stencil: ((GLUT.initDisplayMode & 0x0020 /*GLUT_STENCIL*/) != 0),
alpha: ((GLUT.initDisplayMode & 0x0008 /*GLUT_ALPHA*/) != 0)
};
which seem like they might lead to an answer?
Otherwise if you can't be bothered to read through the source then you can force it. Add this code to the top of your html file before any other scripts
<script>
(function() {
if (typeof HTMLCanvasElement !== "undefined") {
wrapGetContext(HTMLCanvasElement);
}
if (typeof OffscreenCanvas !== "undefined") {
wrapGetContext(OffscreenCanvas);
}
function wrapGetContext(ContextClass) {
const isWebGL = /webgl/i;
ContextClass.prototype.getContext = function(origFn) {
return function(type, attributes) {
if (isWebGL.test(type)) {
attributes = Object.assign({}, attributes || {}, {alpha: true});
}
return origFn.call(this, type, attributes);
};
}(ContextClass.prototype.getContext);
}
}());
</script>
I tested that with this sample, changed this line
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
to this
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
and it worked for me.

Printing with only pdf.js (without the viewer)

There are already two questions about this, but the specific question remains unanswered.
I have a multipage pdf document. I have successfully used pdf.js core to write a page to a canvas. Soon I'm sure I'll have prev / next paging functions so I can see each page. See my existing code at bottom. The reason I am not using the viewer is because my boss doesn't like it. He wants a totally different interface that is extremely minimal and simple, and I have successfully built that minus the full print, ala:
Please note that there is no url to a pdf document. I get the data from a api call that returns base64 data. My code loads the data from an array into the pdf.
My current print function seems like a hack, and I know it isn't how the pdf.js viewer does it. I want to print the pdf as the pdf.js viewer does; meaning, true to form. I'm not sure if converting each page to an image gets it done, but I remain open to suggestions.
The problems are: extra margin not in the original document, and headers / footers. Not to mention pdf documents actually can contain different page layouts and sizes for each page and throwing an image tag into a new window completely ignores that.
I did check the pdf.js examples on Github. None of them are print and they say that using the pdf.js without the viewer is not supported and we are on our own if we do that (is that right?). Anyway, nothing on Google that I could find.
var pdfDocument;
var canvas;
// load from base64 data
loadPdfDocument(file.Content);
function print() {
var doc = document.getElementById('document');
var html = doc.innerHTML;
if (canvas)
html = "<img src='" + canvas.toDataURL() + "'";
var win = window.open('', '', '');
win.document.write(html);
win.document.close();
win.focus();
win.print();
win.close();
};
function loadPdfDocument(base64Data) {
PDFJS.disableWorker = true;
var pdfData = base64ToUint8Array(base64Data);
PDFJS.getDocument(pdfData).then(function(pdf) {
canvas = document.getElementById('pdfPage');
pdfDocument = pdf;
loadPdfPage(1);
});
}
function base64ToUint8Array(base64) {
var raw = atob(base64); // convert base 64 string to raw string
var uint8Array = new Uint8Array(raw.length);
for (var i = 0; i < raw.length; i++) {
uint8Array[i] = raw.charCodeAt(i);
}
return uint8Array;
}
function loadPdfPage(pageIndex) {
pdfDocument.getPage(pageIndex).then(function(page) {
var scale = 1;
var viewport = page.getViewport(scale);
var context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
page.render({
canvasContext: context,
viewport: viewport
});
});
}
<div id="document">
<canvas id="pdfPage" />
</div>

Can you Print a wxWindow in wxWidgets?

I am developing a program for windows using wxWidgets. I'm trying to implement a print function that will print a wxPanel (or wxWindow) to the printer. wxWidgets has a nice handy class that does this, if you draw into a DC.
Is there a way to get wxWidgets to draw a wxPanel or wxWindow in a DC?
I tried to use the HandlePrintClient (in response to a WM_PRINTCLIENT) function, but this just draws the background.
I also tried to create a printer DC and send it trough a similar function to HandlePrint, but the wxWidget stuff seems to be to tightly coupled with BeginPrint.
Is there some way to do what I want to do? Perhaps a class already written that will tack a wxScrolledWindow and send it to the printer? The window will have other controls and windows, like a wxGrid on it.
In the end, it is probably easier to draw what you want into the the printDC. However, with some care, you can use BLIT to copy what is displayed in your panel into the PrintDC without having to redraw everything.
So, in your override of the wxPrintout::OnPrintPage you can write something like this:
wxClientDC frameDC( wxGetApp().GetTopWindow() );
GetDC()->StretchBlit(0,0,5000,5000,
&frameDC, 0, 0, 500,500 );
This will copy everything displayed in your applications top level wondow into the printerDC.
The snag is that the print preview window tends to obliterate your top level frame contents when it pops up. If you have a large monitor and a small application window you can arrange things so they do not overlap
void MyFrame::OnPrint(wxCommandEvent& )
{
wxPrintPreview *preview = new wxPrintPreview(new MyPrintout(this), new MyPrintout(this));
wxPreviewFrame *frame = new wxPreviewFrame(preview, this,
"Demo Print Preview",
wxPoint(600, 100), // move preview window out of the way
wxSize(500, 500));
//frame->Centre(wxBOTH);
frame->Initialize();
frame->Show(true);
A better approach would be to BLIT the frame display into a memoryDC before popping up the print preview, then BLIT from the MemoryDC to the printerDC.
Something along these lines:
void MyFrame::OnPrint(wxCommandEvent& )
{
// save the display before it is clobbered by the print preview
static wxMemoryDC memDC;
static wxBitmap bitmap(500,500);
memDC.SelectObject( bitmap );
wxClientDC frameDC( wxGetApp().GetTopWindow() );
memDC.Blit(0,0,5000,5000,
&frameDC, 0, 0 );
wxPrintPreview *preview = new wxPrintPreview(new MyPrintout(memDC), new MyPrintout(memDC));
wxPreviewFrame *frame = new wxPreviewFrame(preview, this,
"Demo Print Preview",
wxPoint(600, 100), // move preview window out of the way
wxSize(500, 500));
frame->Centre(wxBOTH);
frame->Initialize();
frame->Show(true);
}
and then
class MyPrintout : public wxPrintout
{
wxMemoryDC & myMemDC;
public:
MyPrintout( wxMemoryDC & memDC)
: myMemDC( memDC )
{
}
bool OnPrintPage( int PageNum )
{
// copy saved dispay to printer DC
GetDC()->StretchBlit(0,0,5000,5000,
&myMemDC, 0, 0, 500,500 );
return true;
}
};

Resources