Размытые углы и плохой рендеринг при использовании Phantomjs (или wkhtmltoimage) screengrab на Docker

Я пытаюсь генерировать изображения (jpg или png) из HTML, и я уже пробовал PhantomJS (через jonnyw/php-phantomjs в php) и wkhtmltoimage, но они оба имеют одинаковую проблему при создании изображения. Любой радиус границы, изображения или шрифты имеют очень плохие зубчатые края и совсем не хрустящие.

Сначала я думал, что это не шрифты, загружаемые, но мои значки шрифтов работают нормально, они просто очень низкого качества. У меня есть 100 наборов качества, и я получаю те же результаты при использовании Phantomjs или wkhtmltoimage на любом веб-сайте.

введите описание изображения здесь

Кто-нибудь знает, что может быть причиной этого?

UPDATE

введите описание изображения здесь

ОБНОВЛЕНИЕ 2

Здесь код, используемый из 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

/**
 * 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

Добавьте viewportSize и zoomFactor в свои phantomjs, например:

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

И/или добавить:

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

Попробуйте установить коэффициент масштабирования с использованием более высокого DPI для бумаги относительно экрана DPI:

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

Должен быть установлен после определения размера страницы.

Я также нашел 2 функции, которые пытаются справиться с этой проблемой...

Функция 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";
        }
    });
};

Использование

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)

Функция 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";
    });
};

Использование

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

Надеюсь, это поможет вам. Теперь позвольте мне сказать вам, что я буду делать. Я уже делал сценарии для автоматизации браузера, а PhantomJS - это ИМО, не так хорошо. Рассмотрите возможность использования NightmareJS. Это намного быстрее, чем Phantom и проще в использовании.