Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 137 additions & 23 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ let stream = require('stream');
let path = require('path');
let os = require('os');
let puppeteer = require('puppeteer');
let playwright = require('playwright');
let skiaCanvas = require('skia-canvas');
let typeofFn = typeof function(){};
let isFunction = x => typeof x === typeofFn;
let Handlebars = require('handlebars');
Expand Down Expand Up @@ -63,15 +65,24 @@ let Cytosnap = function( opts = {} ){

this.options = Object.assign( {
// top-level defaults -- none currently
engine: opts.engine || 'puppeteer',
puppeteer: {
args: opts.puppeteer?.args,
headless: true
},
playwright: {
args: opts.playwright?.args,
headless: true
},
skia: {},
}, opts );

// options to pass to puppeteer.launch()
this.options.puppeteer = Object.assign({
// defaults
args: opts.args, // backwards compat
headless: true
}, opts.puppeteer);

/*this.options.puppeteer = Object.assign({
// defaults
args: opts.args, // backwards compat
headless: true
}, opts.puppeteer);*/
this.running = false;
};

Expand Down Expand Up @@ -105,7 +116,17 @@ proto.start = function( next ){
let snap = this;

return Promise.try(function(){
return puppeteer.launch(snap.options.puppeteer);
if(snap.options.engine == 'puppeteer'){
return puppeteer.launch(snap.options.puppeteer);}
else if(snap.options.engine == 'playwright'){
return playwright.chromium.launch(snap.options.playwright);
}
else if(snap.options.engine == 'skia'){
return null;
}
else{
throw new Error ('Unsupported Engine' + snap.options.engine);
}
}).then(function( browser ){
snap.browser = browser;

Expand All @@ -117,6 +138,7 @@ proto.stop = function( next ){
let snap = this;

return Promise.try(function(){
if(snap.browser)
snap.browser.close();
}).then(function(){
snap.running = false;
Expand Down Expand Up @@ -146,16 +168,94 @@ proto.shot = function( opts, next ){
opts.quality = 0; // most compression
}


if( snap.options.engine === 'skia' ) {
return Promise.try(async function() {
const canvas = new skiaCanvas.Canvas(opts.width, opts.height);
const ctx = canvas.getContext("2d");
// Set background
ctx.fillStyle = opts.background;
ctx.fillRect(0, 0, opts.width, opts.height);


let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
opts.elements.forEach(el => {
if (el.group === 'nodes'&& el.position) {
minX = Math.min(minX, el.position.x);
minY = Math.min(minY, el.position.y);
maxX = Math.max(maxX, el.position.x);
maxY = Math.max(maxY, el.position.y);
}
});

// Calculate center of the bounding box
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;

ctx.translate(opts.width / 2, opts.height / 2);

ctx.scale(4.4, 4.4);

ctx.translate(-centerX, -centerY);

//Draw Nodes
opts.elements.forEach(el => {
if (el.group === 'nodes') {
ctx.fillStyle = el.data.color || "black"; // Default color if not specified
ctx.beginPath();
ctx.arc(el.position.x, el.position.y, 10, 0, 2 * Math.PI); // Circle for node
ctx.fill();
}
});

// Draw edges
opts.elements.forEach(el => {
if (el.group === 'edges') {
const sourceNode = opts.elements.find(n => n.data.id === el.data.source);
const targetNode = opts.elements.find(n => n.data.id === el.data.target);

if (sourceNode && targetNode) {
ctx.beginPath();
ctx.moveTo(sourceNode.position.x, sourceNode.position.y);
ctx.lineTo(targetNode.position.x, targetNode.position.y);
ctx.stroke();
}
}
});

let buffer = await canvas.toBuffer(opts.format, { quality: opts.quality });
const base64Image = buffer.toString("base64");

switch (opts.resolvesTo) {
case 'base64uri': return `data:image/${opts.format};base64,${base64Image}`;
case 'base64': return base64Image;
case 'stream': return getStream(base64Image).pipe(base64.decode());
default: throw new Error("Invalid resolve type: " + opts.resolvesTo);
}
})
.then(callbackifyValue(next))
.catch(callbackifyError(next));
}


return Promise.try(function(){
return writeExtensionsList();
}).then(function(){
return browserifyBrowserSrc();
}).then(function(){
return snap.browser.newPage();
if(snap.options.engine == 'puppeteer'){
return snap.browser.newPage();}
else if(snap.options.engine == 'playwright'){
return snap.browser.newPage();
}
}).then(function( puppeteerPage ){
page = puppeteerPage;
}).then(function(){
return page.setViewport({ width: opts.width, height: opts.height });
if(snap.options.engine == 'playwright'){
return page.setViewportSize({ width: opts.width, height: opts.height });}
else if(snap.options.engine == 'puppeteer'){
return page.setViewport({width: opts.width, height: opts.height});
}
}).then(function(){
let patchUri = function(uri){
if( os.platform() === 'win32' ){
Expand All @@ -164,7 +264,7 @@ proto.shot = function( opts, next ){
return uri;
}
};

if(snap.options.engine != 'skia')
return page.goto( 'file://' + patchUri(path.join(__dirname, './browser/index.html')) );
}).then(function(){
if( !isFunction( opts.style ) ){ return Promise.resolve(); }
Expand All @@ -180,16 +280,16 @@ proto.shot = function( opts, next ){
return page.evaluate( js );
}).then(function(){
let js = 'window.options = ( ' + JSON.stringify(opts) + ' )';

if(snap.options.engine != 'skia')
return page.evaluate( js );
}).then(function(){
let js = 'document.body.style.setProperty("background", "' + opts.background + '")';

if(snap.options.engine != 'skia')
return page.evaluate( js );
}).then(function(){

if(snap.options.engine != 'skia')
return page.evaluate(function(){
/* global window, options, cy, layoutFunction, styleFunction */
/*global window, options, cy, layoutFunction, styleFunction */
if( window.layoutFunction ){ options.layout = layoutFunction(); }

if( window.styleFunction ){ options.style = styleFunction(); }
Expand All @@ -198,24 +298,37 @@ proto.shot = function( opts, next ){

cy.add( options.elements );

let layoutDone = cy.promiseOn('layoutstop');

cy.makeLayout( options.layout ).run(); // n.b. makeLayout used in case cytoscape@2 support is desired
return new Promise(function(resolve) {
cy.makeLayout( options.layout ).run();
cy.one('layoutstop', resolve);
setTimeout(resolve, 0);
});

return layoutDone;
});
}).then(function(){
if( opts.resolveTo === 'json' ){ return null; } // can skip in json case
const screenshotoptions = {
type: opts.format,
encoding: 'base64'
};
if(opts.format == 'jpg'){screenshotoptions.quality = opts.quality}
if(snap.options.engine === 'playwright'){delete screenshotoptions.encoding;}
if(snap.options.engine != 'skia')
return page.screenshot(screenshotoptions);
}).then(function( screenshotResult ){

if(snap.options.engine === 'playwright' && Buffer.isBuffer(screenshotResult)){
screenshotResult = screenshotResult.toString('base64');
}


return page.screenshot({ type: opts.format, quality: opts.quality, encoding: 'base64' });
}).then(function( b64Img ){
switch( opts.resolvesTo ){
case 'base64uri':
return 'data:image/' + opts.format + ';base64,' + b64Img;
return 'data:image/' + opts.format + ';base64,' + screenshotResult;
case 'base64':
return b64Img;
return screenshotResult;
case 'stream':
return getStream( b64Img ).pipe( base64.decode() );
return getStream( screenshotResult ).pipe( base64.decode() );
case 'json':
return page.evaluate(function(){
let posns = {};
Expand All @@ -230,6 +343,7 @@ proto.shot = function( opts, next ){
throw new Error('Invalid resolve type specified: ' + opts.resolvesTo);
}
}).then(function( img ){
if(snap.options.engine != 'skia')
return page.close().then(function(){ return img; });
}).then( callbackifyValue(next) ).catch( callbackifyError(next) );
};
Expand Down
Loading