Blurry corners and poor rendering when using Phantomjs (or wkhtmltoimage) screengrab on Docker

1.1k views Asked by At

I'm trying to generate images (jpg or png) from HTML and I've already tried PhantomJS (via jonnyw/php-phantomjs in php) and wkhtmltoimage but they both have the same problem when generating the image. Any border radius, images, or fonts all have really bad jagged edges and aren't crisp at all.

At first I thought it was no fonts being loaded but my font-icons work fine, they're just really poor quality. I have 100 quality set and I get the same results when using Phantomjs or wkhtmltoimage on any website.

enter image description here

Does anyone know what could be causing this?

UPDATE

enter image description here

UPDATE 2

Here's the code used from jonnyw/php-phantomjs:

        $client = Client::getInstance();
        $client->isLazy();
        $client->getEngine()->setPath('phantomjs');
        $client->getEngine()->debug(true);

        $width  = 560;
        $height = 670;
        $top    = 1;
        $left   = 1;

        $request = $client->getMessageFactory()->createCaptureRequest('https://myurltoscreengrab.com', 'GET');
        $request->setOutputFile('uploads/stats/test.png');
        $request->setFormat('png');

        $request->setViewportSize($width, $height);
        $request->setCaptureDimensions($width, $height, $top, $left);

        $response = $client->getMessageFactory()->createResponse();

        // Send the request
        $client->send($request, $response);

JS Being Used

/**
 * Set up page and script parameters
 */
var page       = require('webpage').create(),
    system     = require('system'),
    response   = {},
    debug      = [],
    logs       = [],
    procedure  = {},
    resources  = 0,
    timeout;

/**
 * Global variables
 */


/**
 * Define width & height of capture
 */


var rectTop    = 1,
    rectLeft   = 1,
    rectWidth  = 530,
    rectHeight = 670;

if(rectWidth && rectHeight) {

    debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Set capture clipping size ~ top: ' + rectTop + ' left: ' + rectLeft + ' ' + rectWidth + 'x' + rectHeight);

    page.clipRect = {
        top: rectTop,
        left: rectLeft,
        width: rectWidth,
        height: rectHeight
    };
}


/**
 * Define paper size.
 */


/**
 * Define viewport size.
 */

var viewportWidth  = 530,
    viewportHeight = 670;

if(viewportWidth && viewportHeight) {

    debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Set viewport size ~ width: ' + viewportWidth + ' height: ' + viewportHeight);

    page.viewportSize = {
        width: viewportWidth,
        height: viewportHeight
    };
}




/**
 * Define custom headers.
 */

page.customHeaders = {};



/**
 * Page settings
 */

page.settings.resourceTimeout = 5000;



/**
 * On resource timeout
 */
page.onResourceTimeout = function (error) {

response        = error;
response.status = error.errorCode;


};

/**
 * On resource requested
 */
page.onResourceRequested = function (req) {





    resources++;
    window.clearTimeout(timeout);
};

/**
 * On resource received
 */
page.onResourceReceived = function (res) {

    var resource = res; // To be removed in version 5.0


if(!response.status) {
    response = resource;
}



    if(!res.stage || res.stage === 'end') {

        resources--;

        if (resources === 0) {

            timeout = window.setTimeout(function() {
                procedure.execute('success');
            }, 300);
        }
    }
};

/**
 * Handle page errors
 */
page.onError = function (msg, trace) {

var error = {
    message: msg,
    trace: []
};

trace.forEach(function(t) {
    error.trace.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : ''));
});

logs.push(error);


};

/**
 * Handle global errors
 */
phantom.onError = function(msg, trace) {

var stack = [];

trace.forEach(function(t) {
    stack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : ''));
});

response.status  = 500;
response.content = msg;
response.console = stack;

system.stdout.write(JSON.stringify(response, undefined, 4));
phantom.exit(1);


};

/**
 * Open page
 */
page.open ('https://boxstat.co/widgets/image/stats/2898784/18/500/FFFFFF-EEEEEE-fafafa-333333-85bd4d-ffffff-e4f8cf-71b42f-fddfc1-bd6610-fad3c9-c85639-fac9c9-c52e2e', 'GET', '', function (status) {


page.evaluate(function() {

    var styles = {};

    for(var property in styles) {
        document.body.style[property] = styles[property];
    }
});

    window.setTimeout(function () { 
        procedure.execute(status); 
    }, 4800);
});

/**
 * Execute procedure
 */
procedure.execute = function (status) {

if (status === 'success') {

    try {

        page.render('uploads/stats/test.png', {
            format: 'png',
            quality: 100,
        });

        response.content = page.evaluate(function () {
            return document.getElementsByTagName('html')[0].innerHTML
        });

    } catch(e) {

        response.status  = 500;
        response.content = e.message;
    }
}

response.console = logs;

system.stderr.write(debug.join('\\n') + '\\n');
system.stdout.write(JSON.stringify(response, undefined, 4));

phantom.exit();

};
1

There are 1 answers

0
xendi On

Add viewportSize and zoomFactor in your phantomjs like:

await page.property('viewportSize', { height: 1600, width: 3600 });
await page.property('zoomFactor', 4);

And/or add:

<script>
  window.devicePixelRatio = 4;
</script>

Try setting the zoom factor using a higher DPI for paper in relation to screen DPI:

page.zoomFactor = 300 / 96;   // or use / 72

Must be set after page size is defined.

I also found 2 functions that attempt to deal with this sort of problem...

Function 1

var makeHighResScreenshot = function(srcEl, destIMG, dpi) {
    var scaleFactor = Math.floor(dpi / 96);
    // Save original size of element
    var originalWidth = srcEl.offsetWidth;
    var originalHeight = srcEl.offsetHeight;
    // Save original document size
    var originalBodyWidth = document.body.offsetWidth;
    var originalBodyHeight = document.body.offsetHeight;

    // Add style: transform: scale() to srcEl
    srcEl.style.transform = "scale(" + scaleFactor + ", " + scaleFactor + ")";
    srcEl.style.transformOrigin = "left top";

    // create wrapper for srcEl to add hardcoded height/width
    var srcElWrapper = document.createElement('div');
    srcElWrapper.id = srcEl.id + '-wrapper';
    srcElWrapper.style.height = originalHeight*scaleFactor + 'px';
    srcElWrapper.style.width = originalWidth*scaleFactor + 'px';
    // insert wrapper before srcEl in the DOM tree
    srcEl.parentNode.insertBefore(srcElWrapper, srcEl);
    // move srcEl into wrapper
    srcElWrapper.appendChild(srcEl);

    // Temporarily remove height/width constraints as necessary
    document.body.style.width = originalBodyWidth*scaleFactor +"px";
    document.body.style.height = originalBodyHeight*scaleFactor +"px";

    window.scrollTo(0, 0); // html2canvas breaks when we're not at the top of the doc, see html2canvas#820
    html2canvas(srcElWrapper, {
        onrendered: function(canvas) {
            destIMG.src = canvas.toDataURL("image/png");
            srcElWrapper.style.display = "none";
            // Reset height/width constraints
            document.body.style.width = originalBodyWidth + "px";
            document.body.style.height = originalBodyHeight + "px";
        }
    });
};

Usage

var src = document.getElementById("screenshot-source");
var img = document.getElementById("screenshot-img");
makeHighResScreenshot(src, img, 192); // DPI of 192 is 4x resolution (2x normal DPI for both width and height)

Function 2

function takeHighResScreenshot(srcEl, destIMG, scaleFactor) {
    // Save original size of element
    var originalWidth = srcEl.offsetWidth;
    var originalHeight = srcEl.offsetHeight;
    // Force px size (no %, EMs, etc)
    srcEl.style.width = originalWidth + "px";
    srcEl.style.height = originalHeight + "px";

    // Position the element at the top left of the document because of bugs in html2canvas. The bug exists when supplying a custom canvas, and offsets the rendering on the custom canvas based on the offset of the source element on the page; thus the source element MUST be at 0, 0.
    // See html2canvas issues #790, #820, #893, #922
    srcEl.style.position = "absolute";
    srcEl.style.top = "0";
    srcEl.style.left = "0";

    // Create scaled canvas
    var scaledCanvas = document.createElement("canvas");
    scaledCanvas.width = originalWidth * scaleFactor;
    scaledCanvas.height = originalHeight * scaleFactor;
    scaledCanvas.style.width = originalWidth + "px";
    scaledCanvas.style.height = originalHeight + "px";
    var scaledContext = scaledCanvas.getContext("2d");
    scaledContext.scale(scaleFactor, scaleFactor);

    html2canvas(srcEl, { canvas: scaledCanvas })
    .then(function(canvas) {
        destIMG.src = canvas.toDataURL("image/png");
        srcEl.style.display = "none";
    });
};

Usage

var src = document.getElementById("screenshot-src");
var img = document.getElementById("screenshot-img");
takeHighResScreenshot(src, img, 2); // This time we provide desired scale factor directly, no more messing with DPI

I hope this helps you. Now let me tell you what I would do. I've been making browser automation scripts for a while now and PhantomJS is, IMO, not so good. Consider using NightmareJS. It's MUCH faster than Phantom and easier to use.