I am using two files one to keep all the URLs and other variables and other to keep the scenario config
const projectId = "test"; //
let baseUrl = "someurl"; //
let scrollToSelector = "";
let removeSelector = "";
// Replace the values of the below array with the relative URLs of your website. E.g., "/about", "/contact", "/pricing", etc.
// Use just "/" to test the homepage of your website.
// Add as many relative URLs as you need.
const relativeUrls =[
"/about",
"/documentation",
"/case-studies/",
"/solutions/",
"/blog/"
];
relativeUrls.map(relativeUrl => {
if (relativeUrl === "/about") {
scrollToSelector = "a.wp-block-button__link";
removeSelector = ".is-style-image-banner"
console.log(scrollToSelector);
}
});
// Leave the below array as is if you want to test your website using the viewports listed below.
// The suported viewports are: phone (320px X 480px), tablet (1024px X 768px), and desktop (1280px X 1024px).
// No other viewports are supported.
// You can remove the viewports that you don't need, but at least one of them is required.
const viewports = [
"phone",
"tablet",
"desktop",
];
module.exports = {
baseUrl,
projectId,
relativeUrls,
viewports,
scrollToSelector,
removeSelector
};
mainConfig.js
const THREE_SECONDS_IN_MS = 3000;
const scenarios = [];
const viewports = [];
basicConfig.relativeUrls.map(relativeUrl => {
scenarios.push({
label: relativeUrl,
url: `${basicConfig.baseUrl}${relativeUrl}`,
delay: THREE_SECONDS_IN_MS,
requireSameDimensions: false,
scrollToSelector: basicConfig.scrollToSelector,
removeSelectors: [basicConfig.removeSelector]
// onReadyScript: "onReadyScript.js",
// readyEvent: "page_loaded"
});
});
basicConfig.viewports.map(viewport => {
if (viewport === "phone") {
pushViewport(viewport, 320, 480);
}
if (viewport === "tablet") {
pushViewport(viewport, 1024, 768);
}
if (viewport === "desktop") {
pushViewport(viewport, 1280, 1024);
}
});
function pushViewport(viewport, width, height) {
viewports.push({
name: viewport,
width,
height,
});
}
module.exports = {
id: basicConfig.projectId,
viewports,
scenarios,
paths: {
bitmaps_reference: "test/backstop_data/bitmaps_reference",
bitmaps_test: "test/backstop_data/bitmaps_test",
html_report: "test/backstop_data/html_report"
},
report: ["CI"],
engine: "puppeteer",
engineOptions: {
args: ["--no-sandbox"]
},
asyncCaptureLimit: 5,
asyncCompareLimit: 50,
};
scrollToSelector doesn't seem to be working. Is there any other way it should declared and called.
Your scrollToSelector need to be in the rectangular parentesis [], like that scrollToSelector: ['.a.wp-block-button__link']
Related
For a digital artwork I'm generating a canvas element in Vue which draws from an array of multiple images.
The images can be split in two categories:
SVG (comes with a fill-color)
PNG (just needs to be drawn as a regular image)
I came up with this:
const depict = (options) => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const myOptions = Object.assign({}, options);
if (myOptions.ext == "svg") {
return loadImage(myOptions.uri).then((img) => {
ctx.drawImage(img, 0, 0, 100, 100);
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = myOptions.clr;
ctx.fillRect(0, 0, 100, 100);
ctx.globalCompositeOperation = "source-over";
});
} else {
return loadImage(myOptions.uri).then((img) => {
ctx.fillStyle = myOptions.clr;
ctx.drawImage(img, 0, 0, 100, 100);
});
}
};
this.inputs.forEach(depict);
for context:
myOptions.clr = the color
myOptions.uri = the url of the image
myOptions.ext = the extension of the image
While all images are drawn correctly I can't figure out why the last fillStyle overlays the whole image. I just want all the svg's to have the fillStyle which is attached to them.
I tried multiple globalCompositeOperation in different orders. I also tried drawing the svg between ctx.save and ctx.restore. No succes… I might be missing some logic here.
So! I figured it out myself in the meantime :)
I created an async loop with a promise. Inside this I created a temporary canvas per image which I then drew to one canvas. I took inspiration from this solution: https://stackoverflow.com/a/6687218/15289586
Here is the final code:
// create the parent canvas
let parentCanv = document.createElement("canvas");
const getContext = () => parentCanv.getContext("2d");
const parentCtx = getContext();
parentCanv.classList.add("grid");
// det the wrapper from the DOM
let wrapper = document.getElementById("wrapper");
// this function loops through the array
async function drawShapes(files) {
for (const file of files) {
await depict(file);
}
// if looped > append parent canvas to to wrapper
wrapper.appendChild(parentCanv);
}
// async image loading worked best
const loadImage = (url) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`load ${url} fail`));
img.src = url;
});
};
// depict the file
const depict = (options) => {
// make a promise
return new Promise((accept, reject) => {
const myOptions = Object.assign({}, options);
var childCanv = document.createElement("canvas");
const getContext = () => childCanv.getContext("2d");
const childCtx = getContext();
if (myOptions.ext == "svg") {
loadImage(myOptions.uri).then((img) => {
childCtx.drawImage(img, 0, 0, 100, parentCanv.height);
childCtx.globalCompositeOperation = "source-in";
childCtx.fillStyle = myOptions.clr;
childCtx.fillRect(0, 0, parentCanv.width, parentCanv.height);
parentCtx.drawImage(childCanv, 0, 0);
accept();
});
} else {
loadImage(myOptions.uri).then((img) => {
// ctx.fillStyle = myOptions.clr;
childCtx.drawImage(img, 0, 0, 100, parentCanv.height);
parentCtx.drawImage(childCanv, 0, 0);
accept();
});
}
});
};
drawShapes(this.inputs);
I have to store the created polygon in ArcGIS. Once the polygon is stored in ArcGIS, it returns an ID (Object ID). With the object ID, the administrator can access the polygon in ArcGIS. I found a piece of code in one of our old systems the code is written in version 3xx.
function SendFeaturesToParent()
{
editingEnabled = false;
editToolbar.deactivate();
lyrMeters.clearSelection();
polygon = currentEVT.graphic.geometry;
var query = new Query();
query.geometry = polygon;
lyrAreas.applyEdits(null, [currentEVT.graphic], null);
var attributes = [];
var featureValues = [];
for (var x = 0; x < selectedfeatures.length; x++) {
featureValues.push("METER_ID: " + selectedfeatures[x].attributes["METER_ID"] + ", Type: " + selectedfeatures[x].attributes["Type"]);
attributes.push(selectedfeatures[x].attributes);
}
console.log("attributes"+ attributes);
//Send the array of meter values back to the parent page.
var objectId = lyrAreas._defnExpr;
objectId = objectId.split('=');
window.parent.postMessage(
{
event_id: 'my_cors_message',
data: attributes,
objectId: objectId[1]
},
"*" //or "www.parentpage.com"
);
$('#modelConfirm').modal('hide');
}
I need to implement in latest version of arcGIS API 4.23. What are the applyEdits do?
/**** modified code in 4.23 */
var token = '';
const PermitAreaURL = "url_1";
const locatorUrl = "url_2";
const streetmapURL = "url_3";
const streetmapLebelsURL = "url_4";
const MetersURL = "url_5";
const MetersWholeURL = "url_6";
require(["esri/config",
"esri/Map",
"esri/views/MapView",
"esri/layers/FeatureLayer",
"esri/layers/TileLayer",
"esri/layers/VectorTileLayer",
"esri/layers/GraphicsLayer",
"esri/widgets/Search",
"esri/widgets/Sketch/SketchViewModel",
"esri/geometry/geometryEngineAsync",
],
function (esriConfig, Map, MapView, FeatureLayer, TileLayer, VectorTileLayer, GraphicsLayer, Search, SketchViewModel, geometryEngineAsync) {
esriConfig.apiKey = "AAPK3f43082c24ae493196786c8b424e9f43HJcMvP1NYaqIN4p63qJnCswIPsyHq8TQHlNtMRLWokqJIWYIJjga9wIEzpy49c9v";
const graphicsLayer = new GraphicsLayer();
const streetmapTMLayer = new TileLayer({
url: streetmapURL
});
const streetmapLTMLayer = new VectorTileLayer({
url: streetmapLebelsURL
});
const lyrwholeMeters = new FeatureLayer({
url: MetersWholeURL,
outFields: ["*"],
});
const lyrMeters = new FeatureLayer({
url: MetersURL,
outFields: ["*"],
});
// const permitAreaUrl = new FeatureLayer({
// url: PermitAreaURL,
// outFields: ["*"],
// });
// console.log(lyrMeters);
const map = new Map({
basemap: "arcgis-topographic", // Basemap layer service
layers: [streetmapTMLayer, streetmapLTMLayer, lyrMeters, lyrwholeMeters, graphicsLayer]
});
const view = new MapView({
map: map,
center: [-95.9406, 41.26],
zoom: 16,
maxZoom: 21,
minZoom: 13,
container: "viewDiv" // Div element
});
view.when(() => {
const polygonSymbol = {
type: "simple-fill", // autocasts as new SimpleFillSymbol()
color: [207, 34, 171, 0.5],
outline: {
// autocasts as new SimpleLineSymbol()
color: [247, 34, 101, 0.9],
}
};
const sketchViewModel = new SketchViewModel({
view: view,
layer: graphicsLayer,
polygonSymbol: polygonSymbol,
});
sketchViewModel.create("polygon", { mode: "hybrid" });
// Once user is done drawing a rectangle on the map
// use the rectangle to select features on the map and table
sketchViewModel.on("create", async (event) => {
if (event.state === "complete") {
// this polygon will be used to query features that intersect it
const geometries = graphicsLayer.graphics.map(function (graphic) {
return graphic.geometry
});
const queryGeometry = await geometryEngineAsync.union(geometries.toArray());
selectFeatures(queryGeometry);
}
});
});
// This function is called when user completes drawing a rectangle
// on the map. Use the rectangle to select features in the layer and table
function selectFeatures(geometry) {
console.log(geometry.rings);
// create a query and set its geometry parameter to the
// rectangle that was drawn on the view
const query = {
geometry: geometry,
outFields: ["*"]
};
lyrwholeMeters.queryFeatures(query).then(function (results) {
var lyr = results.features;
console.log(lyr);
// save the polygon
lyr.applyEdits({
addFeatures: [geometry] /*updates*/
});
lyr.forEach(element => {
console.log(`MeterID-${element.attributes.METER_ID}, OBJECTID-${element.attributes.OBJECTID}, Passport_ID-${element.attributes.Passport_ID}`);
});
});
}
// search widget
const searchWidget = new Search({
view: view,
});
view.ui.add(searchWidget, {
position: "top-left",
index: 2
});
});
applyEdits method is the way you to add/update/delete features in a feature layer. Both version of the library have the method although in version 4 takes an object that contain the edits instead of the separated parameters. In your code, version 3, it is,
lyrAreas.applyEdits(null, [currentEVT.graphic], null);
first parameter is for new features, second for updates on features and third for features to delete.
While in version 4 it should be,
lyrAreas.applyEdits({
updateFeatures: [currentEVT.graphic] /*updates*/
});
the object goes all the informations about the edits, in this case you only have updates.
I am not completely sure about this line,
var objectId = lyrAreas._defnExpr;
objectId = objectId.split('=');
I am guessing is the definition expression of the feature layer, a sql expression, that is why in the next line it is split, to use later the value. Version 3 library did not expose the property but gives set and get methods.
Version 4 have the property and works in similar way. In this case for latest version it should be,
var objectId = lyrAreas.definitionExpression;
objectId = objectId.split('=');
So I do not think you will have
ArcGIS API v3 - FeatureLayer
ArcGIS API v4 - FeatureLayer
Using DataTables and Buttons (NOT TableTools, which is retired) extension. Some cells have progressbars and small icons. Is there a way to export these images (or at least their titles) to PDF? Found some possible hacks on this page, but all of them were for retired TableTools.
Checked https://datatables.net/reference/button/pdf and https://datatables.net/reference/api/buttons.exportData%28%29 but couldn't find any method to achieve this goal. Tested by adding this code:
stripHtml: false
but whole HTML code (like img src=...) was included in PDF file instead of images.
If exporting images isn't possible, is there a way to export at least alt or title attribute of each image? That would be enough.
I assume you are using pdfHtml5. dataTables is using pdfmake in order to export pdf files. When pdfmake is used from within a browser it needs images to be defined as base64 encoded dataurls.
Example : You have rendered a <img src="myglyph.png"> in the first column of some of the rows - those glyphs should be included in the PDF. First create an Image reference to the glyph :
var myGlyph = new Image();
myGlyph.src = 'myglyph.png';
In your customize function you must now
1) build a dictionary with all images that should be included in the PDF
2) replace text nodes with image nodes to reference images
buttons : [
{
extend : 'pdfHtml5',
customize: function(doc) {
//ensure doc.images exists
doc.images = doc.images || {};
//build dictionary
doc.images['myGlyph'] = getBase64Image(myGlyph);
//..add more images[xyz]=anotherDataUrl here
//when the content is <img src="myglyph.png">
//remove the text node and insert an image node
for (var i=1;i<doc.content[1].table.body.length;i++) {
if (doc.content[1].table.body[i][0].text == '<img src="myglyph.png">') {
delete doc.content[1].table.body[i][0].text;
doc.content[1].table.body[i][0].image = 'myGlyph';
}
}
},
exportOptions : {
stripHtml: false
}
}
Here is a an example of a getBase64Image function
function getBase64Image(img) {
var canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
return canvas.toDataURL("image/png");
}
If you just want to show the title of images in the PDF - or in any other way want to manipulate the text content of the PDF - then it is a little bit easier. The content of each column in each row can be formatted through the exportOptions.format.body callback :
buttons : [
{
extend : 'pdfHtml5',
exportOptions : {
stripHtml: false
format: {
body: function(data, col, row) {
var isImg = ~data.toLowerCase().indexOf('img') ? $(data).is('img') : false;
if (isImg) {
return $(data).attr('title');
}
return data;
}
}
}
]
The reason format.body cannot be used along with images is that is only let us pass data back to the text node part of the PDF document.
See also
http://pdfmake.org/#/gettingstarted (look for Images section)
https://github.com/bpampuch/pdfmake/blob/master/examples/images.js
Since no suggestions received, I had to make a hack in order to get PDF file formatted the way I want.
In case someone has the same issue, you can use hidden span to display image alt/title near image itself. I'm sure it's not the best practice, but it will do the trick. So the code will look like:
<img src='image.png' alt='some_title'/><span class='hidden'>some_title</span>
This way datatables will show only the image, while PDF file will contain text you need.
This is my CODE!
HTML
<div class="dt-btn"></div>
<table>
<thead><tr><th>No</th><th>IMAGE</th><th>NOTE</th></tr></thead>
<tbody>
<tr>
<td>{{$NO}}</td>
<td>{{$imgSRC}}</td>
<td>{{$NAME}}<br />
{{$GRADE}}<br />
{{$PROFILE}}<br />
{{$CODE}}<br />
</td>
</tr>
</tbody>
</table>
JAVASCRIPT
$.extend( true, $.fn.dataTable.defaults, {
buttons: [{
text: '<i class="bx bx-download font-medium-1"></i><span class="align-middle ml-25">Download PDF</span>',
className: 'btn btn-light-secondary mb-1 mx-1 dnPDF',
extend: 'pdfHtml5',
pageSize: 'A4',
styles: {
fullWidth: { fontSize: 11, bold: true, alignment: 'left', margin: [0,0,0,0] }
},
action: function ( e, dt, node, config ) {
var that = this;
setTimeout( function () {
$.fn.dataTable.ext.buttons.pdfHtml5.action.call(that, e, dt, node, config);
$( ".donePDF" ).remove();
$( ".dnPDF" ).prop("disabled", false);
}, 50);
},
customize: function(doc) {
doc.defaultStyle.fontSize = 11;
doc.defaultStyle.alignment = 'left';
doc.content[1].table.dontBreakRows = true;
if (doc) {
for (var i = 1; i < doc.content[1].table.body.length; i++) {
// 1st Column - display IMAGE
var imgtext = doc.content[1].table.body[i][0].text;
delete doc.content[1].table.body[i][0].text;
jQuery.ajax({
type: "GET",
dataType: "json",
url: "{{route('base64')}}",
data: { src: imgtext },
async: false,
success: function(resp) {
//console.log(resp.data);
doc.content[1].table.body[i][0] = {
margin: [0, 0, 0, 3],
alignment: 'center',
image: resp.data,
width: 80,
height: 136
};
}
});
// 2nd Column - display NOTE(4 line)
var bodyhtml = doc.content[1].table.body[i][1].text;
var bodytext = bodyhtml.split("\n");
var bodystyle = []
for (var j = 0; j < bodytext.length; j++) {
switch(j) {
case 0:
// NAME
var _text = { margin:[0, 0, 0, 3], color:"#000000", fillColor:'#ffffff', bold:true, fontSize:13, alignment:'center', text:bodytext[j] };
break;
case 1:
// GRADE
var _text = { margin:[0, 0, 0, 3], color:"blue", fillColor:'#ffffff', bold:false, fontSize:11, alignment:'left', text:bodytext[j] };
break;
case 3:
// CODE
var _text = { margin:[0, 0, 0, 3], color:"#000000", fillColor:'#ffffff', bold:true, fontSize:11, alignment:'left', text:bodytext[j] };
break;
default:
// OTHERS
var _text = { margin:[0, 0, 0, 3], color:"#000000", fillColor:'#ffffff', bold:false, fontSize:11, alignment:'left', text:bodytext[j] };
break;
}
bodystyle[j] = _text;
};
doc.content[1].table.body[i][1] = bodystyle;
}
}
},
exportOptions: {
columns: [ 1, 2 ],
stripNewlines: false,
stripHtml: true,
modifier: {
page: 'all' // 'all', 'current'
},
}
}],
columns: [
{ className: 'iNo', orderable: true, visible: true},
{ className: 'iIMG', orderable: false, visible: false },
{ className: 'iPDF', orderable: false, visible: false, responsivePriority: 10001 } ]
});
var table = $('#table').DataTable();
table.buttons().container().appendTo('.dt-btn');
$('.dnPDF').on('click', function(){
$(this).append('<span class="spinner-border spinner-border-sm donePDF" role="status" aria-hidden="true"></span>').closest('button').attr('disabled','disabled');
});
$.fn.dataTable.Buttons.defaults.dom.container.className = '';
$.fn.dataTable.Buttons.defaults.dom.button.className = 'btn';
PHP
public function base64(Request $request)
{
$request->validate([
'src' => 'required|string'
]);
$fTYPE = pathinfo($request->src, PATHINFO_EXTENSION);
$fDATA = #file_get_contents($request->src);
$imgDATA = base64_encode($fDATA);
$imgSRC = 'data:image/' . $fTYPE . ';base64,'.$imgDATA;
$error = ($imgDATA!='') ? 0 : 1;
$msg = ($error) ? 'Error' : 'Success';
return response()->json([ 'msg' => $msg, 'error'=> $error, 'data' => $imgSRC]);
}
[Sample][1]: https://i.stack.imgur.com/35Wlm.jpg
In addition to davidkonrad's answer. I created dynamically all base64 images and used them in Datatables pdfmake like this:
for (var i = 1; i < doc.content[2].table.body.length; i++) {
if (doc.content[2].table.body[i][1].text.indexOf('<img src=') !== -1) {
html = doc.content[2].table.body[i][1].text;
var regex = /<img.*?src=['"](.*?)['"]/;
var src = regex.exec(html)[1];
var tempImage = new Image();
tempImage.src = src;
doc.images[src] = getBase64Image(tempImage)
delete doc.content[2].table.body[i][1].text;
doc.content[2].table.body[i][1].image = src;
doc.content[2].table.body[i][1].fit = [50, 50];
}
//here i am removing the html links so that i can use stripHtml: true,
if (doc.content[2].table.body[i][2].text.indexOf('<a href="details.php?') !== -1) {
html = $.parseHTML(doc.content[2].table.body[i][2].text);
delete doc.content[2].table.body[i][1].text;
doc.content[2].table.body[i][2].text = html[0].innerHTML;
}
}
I believe I need to make follow up calls to get the results I am looking for. However, I cannot get the phone number and www url for any of the results I pull up.
I am a novice and need some help explaining where in this code I can get the correct results pulled. Please see the demo site here:
http://news.yeselectric.com/gmaps-excel-master/
Here is my JS code:
var store = (function () {
var searchBox,
infowindow,
markers = [],
myLatlng = new google.maps.LatLng(50.0019, 10.1419),
myOptions = { zoom: 6, center: myLatlng, mapTypeId: google.maps.MapTypeId.ROADMAP, mapTypeControl: false, streetViewControl:false },
customIcons = { iconblue: './icons/blue.png' };
var init = function() {
map = new google.maps.Map(document.getElementById("map-canvas"), myOptions);
input = (document.getElementById('pac-input'));
map.controls[google.maps.ControlPosition.TOP_LEFT].push(input);
searchBox = new google.maps.places.SearchBox((input));
store.listener();
},
listener = function() {
google.maps.event.addListener(searchBox, 'places_changed', function() {
//search for places
var places = searchBox.getPlaces();
for (var i = 0, marker; marker = markers[i]; i++) {
marker.setMap(null);
}
//set markers zero
markers = [];
$('.addaddress').empty();
//get new bounds
var bounds = new google.maps.LatLngBounds();
for (var i = 0, place; place = places[i]; i++) {
store.create_marker(place);
//append to the table
$('.addaddress').append('<tr><td>'+ place.name +'</td><td>'+ place.formatted_address +'</td><td>'+ place.formatted_phone_number +'</td><td>'+ place.website +'</td></tr>');
bounds.extend(place.geometry.location);
}
//set the map
map.fitBounds(bounds);
});
},
create_marker = function(info) {
//create a marker for each place
var marker = new google.maps.Marker({ map: map, icon: customIcons.iconblue, title: info.name, position: info.geometry.location });
//infowindow setup
google.maps.event.addListener(marker, "click", function() {
if (infowindow) {
infowindow.close();
}
infowindow = new google.maps.InfoWindow({content: info.name});
infowindow.open(map, marker);
});
//push the marker
markers.push(marker);
}
return {
init: init,
listener: listener,
create_marker: create_marker
};
})();
google.maps.event.addDomListener(window, 'load', store.init);
Google's API returns different place metadata, depending on which call you're using. To get more place details, you'll need to get the place's placeId and use that to make a call to place.getDetails()
Here's a more thorough answer: https://stackoverflow.com/a/9523345/2141296
When rendering to pdf, I need the html page to be 100% of the print width. Otherwise content gets cut off. Is there an easy way to do this?
I came up with a work-around, which gets the html width after rendering and then sets the zoom factor to force the correct width.
var page = require('webpage').create(),
system = require('system'),
dpi = 89.9, // strange, but that's what I get on Mac
pageWidth = 210; // in mm
var getWidth = function() {
return page.evaluate(function() {
// get width manually
// ...
return width;
});
};
page.paperSize = {format: 'A4', orientation: 'portrait'};
page.open(system.args[1], function (status) {
window.setTimeout(function () {
page.zoomFactor = (pageWidth / 25.4) * dpi / getWidth();
page.render(system.args[2]);
phantom.exit();
}, 200);
});
Looking for a straight forward solution, because getting the width requires me to do some tricks due to the framework I use.
Managed to solve this by adding the following CSS rule which will zoom out the page and make it fit:
html {
zoom: 0.60;
}
Edit rasterize.js as follows.
Make a backup of rasterize.js
Remove this block (approx. line 18-31)
if (system.args.length > 4) {
page.zoomFactor = system.args[4];
}
page.open(address, function (status) {
if (status !== 'success') {
console.log('Unable to load the address!');
phantom.exit();
} else {
window.setTimeout(function () {
page.render(output);
phantom.exit();
}, 200);
}
});
Replace it with this code block
if (system.args.length > 4) {
page.zoomFactor = parseFloat(system.args[4].split("=")[1]);
}
page.open(address, function (status) {
if (status !== 'success') {
console.log('Unable to load the address!');
phantom.exit(1);
} else {
window.setTimeout(function () {
console.log("zoomFactor: " + page.zoomFactor);
page.evaluate(function(zoom) {
return document.querySelector('body').style.zoom = zoom;
}, page.zoomFactor); // <-- your zoom here
page.render(output);
phantom.exit();
}, 200);
}
});
Then you can export html to pdf on-the-fly like this:
~/bin/phantomjs ~/bin/phantomjs/examples/rasterize.js \
'http://example.com/index.html' \
mysite.pdf paperformat=A4 zoom=0.4