|
const puppeteer = require("puppeteer"); |
|
const dateformat = require("dateformat"); |
|
const fastPng = require("fast-png"); |
|
const fs = require("fs"); |
|
const path = require("path"); |
|
const convertLength = require("convert-length"); |
|
const ora = require("ora"); |
|
|
|
const bootup = /*js*/ ` |
|
window.exporter = async (manager) => { |
|
manager.props.exporting = true; |
|
manager.resize(); |
|
|
|
const { |
|
units, |
|
dimensions, |
|
pixelsPerInch, |
|
pixelRatio = 1 |
|
} = manager.props; |
|
|
|
let width = await window.exporter_convertLength(dimensions[0], units, 'px', { |
|
pixelsPerInch, |
|
roundPixel: true |
|
}); |
|
let height = await window.exporter_convertLength(dimensions[1], units, 'px', { |
|
pixelsPerInch, |
|
roundPixel: true |
|
}); |
|
|
|
width *= pixelRatio; |
|
height *= pixelRatio; |
|
console.log([ |
|
'', |
|
' Input Size:', |
|
' ' + dimensions[0] + 'x' + dimensions[1] + ' ' + units, |
|
' @' + pixelRatio + 'x', |
|
' ' + pixelsPerInch + ' DPI', |
|
'', |
|
' Outpt Size:', |
|
' ' + width + 'x' + height + ' px', |
|
'' |
|
].join('\\n')) |
|
|
|
const tileSize = Math.min(width, height, 1024); |
|
|
|
const offCanvas = document.createElement('canvas'); |
|
offCanvas.width = offCanvas.height = tileSize; |
|
const offContext = offCanvas.getContext("2d"); |
|
|
|
function draw (context) { |
|
manager.props.exporting = true; |
|
manager.render(); |
|
} |
|
|
|
await tileRenderer(); |
|
|
|
async function tileRenderer() { |
|
await window.tileRenderStart(width, height, manager.settings); |
|
|
|
const xTiles = Math.ceil(width / tileSize); |
|
const yTiles = Math.ceil(height / tileSize); |
|
|
|
const tiles = []; |
|
for (let y = 0; y < yTiles; y++) { |
|
for (let x = 0; x < xTiles; x++) { |
|
const xPos = x * tileSize; |
|
const yPos = y * tileSize; |
|
const curWidth = Math.min(width - xPos, tileSize); |
|
const curHeight = Math.min(height - yPos, tileSize); |
|
tiles.push({ |
|
xPos, |
|
yPos, |
|
curWidth, |
|
curHeight |
|
}); |
|
} |
|
} |
|
|
|
let offset = 0; |
|
await tiles.reduce(async (p, tile, i, list) => { |
|
await p; |
|
|
|
manager.props.region = { |
|
x: tile.xPos, |
|
y: tile.yPos, |
|
width: tile.curWidth, |
|
height: tile.curHeight |
|
}; |
|
let dataURL; |
|
if (manager.props.gl) { |
|
draw(); |
|
dataURL = manager.props.canvas.toDataURL('image/png'); |
|
} else { |
|
offContext.save(); |
|
offContext.clearRect(0, 0, tileSize, tileSize); |
|
|
|
offContext.translate(-tile.xPos, -tile.yPos); |
|
offContext.scale((width / manager.props.width) / manager.props.scaleX, (height / manager.props.height) / manager.props.scaleY); |
|
|
|
manager.props.context = offContext; |
|
draw(); |
|
|
|
dataURL = offCanvas.toDataURL('image/png'); |
|
|
|
offContext.restore(); |
|
} |
|
|
|
const formOpt = { |
|
index: i, |
|
total: list.length, |
|
data: dataURL, |
|
x: tile.xPos, |
|
y: tile.yPos, |
|
width: tile.curWidth, |
|
height: tile.curHeight |
|
}; |
|
await window.fetch('/exporter/blit', { |
|
method: 'POST', |
|
cache: 'no-cache', |
|
headers: { |
|
'Accept': 'application/json', |
|
'Content-Type': 'application/json' |
|
}, |
|
credentials: 'same-origin', |
|
body: JSON.stringify({ |
|
...formOpt |
|
}) |
|
}) |
|
|
|
return new Promise(resolve => setTimeout(resolve)); |
|
}, Promise.resolve()); |
|
|
|
await window.tileRenderEnd(); |
|
} |
|
} |
|
`; |
|
|
|
async function startExport(opt) { |
|
const dir = opt.dir; |
|
|
|
const args = puppeteer |
|
.defaultArgs() |
|
.filter( |
|
arg => |
|
arg !== "--disable-gpu" && arg !== "about:blank" && arg !== "--headless" |
|
); |
|
|
|
const additionalArgs = `-–enable-gpu-rasterization |
|
--force-gpu-rasterization |
|
--enable-native-gpu-memory-buffers |
|
--enable-oop-rasterization |
|
--ignore-gpu-blacklist |
|
--use-skia-deferred-display-list |
|
--enable-surfaces-for-videos |
|
-–enable-zero-copy |
|
--enable-fast-unload`.split("\n"); |
|
args.push(...additionalArgs); |
|
args.push("--headless"); |
|
// args.push("--disable-gpu"); |
|
args.push("--canvas-msaa-sample-count=4"); |
|
// args.push("--use-gl=desktop"); |
|
args.push("about:blank"); |
|
// console.log(`args: ${args.join("\n")}`); |
|
const browser = await puppeteer.launch({ |
|
ignoreDefaultArgs: true, |
|
args |
|
}); |
|
|
|
const page = (await browser.pages())[0]; |
|
// const page = await browser.newPage(); |
|
|
|
page.on("console", consoleObj => console.log(consoleObj.text())); |
|
page.on("error", err => { |
|
console.log("error happened at the page: ", err.message); |
|
}); |
|
|
|
page.on("pageerror", pageerr => { |
|
console.log("pageerror occurred: ", pageerr.message); |
|
}); |
|
|
|
function toArrayBuffer(buf) { |
|
var ab = new ArrayBuffer(buf.length); |
|
var view = new Uint8Array(ab); |
|
for (var i = 0; i < buf.length; ++i) { |
|
view[i] = buf[i]; |
|
} |
|
return ab; |
|
} |
|
|
|
let buffer; |
|
let bufferSize; |
|
let filename; |
|
await page.setRequestInterception(true); |
|
|
|
let spinner; |
|
let rendering = true; |
|
const queue = []; |
|
let interval = setInterval(async () => { |
|
if (queue.length === 0 && !rendering) { |
|
clearInterval(interval); |
|
return; |
|
} |
|
|
|
if (queue.length === 0) { |
|
return; |
|
} |
|
|
|
const next = queue.shift(); |
|
if (next == null) { |
|
spinner.text = "Encoding PNG..."; |
|
spinner.render(); |
|
await new Promise(resolve => setTimeout(resolve, 1)); |
|
const buf = fastPng.encode({ |
|
...bufferSize, |
|
data: buffer |
|
}); |
|
spinner.text = "Writing file..."; |
|
spinner.render(); |
|
await new Promise(resolve => setTimeout(resolve, 1)); |
|
fs.writeFile(filename, buf, async err => { |
|
if (err) throw err; |
|
spinner.succeed("Finished writing: " + filename); |
|
await browser.close(); |
|
}); |
|
} else { |
|
const image = JSON.parse(next); |
|
const curBuf = Buffer.from( |
|
image.data.slice("data:image/png;base64,".length), |
|
"base64" |
|
); |
|
const decoded = fastPng.decode(curBuf); |
|
spinner.text = `Drawing Tile ${image.index + 1} / ${image.total}`; |
|
// console.log("Blitting", image.index + 1, image.total); |
|
blit( |
|
decoded.data, |
|
image.x, |
|
image.y, |
|
decoded.width, |
|
decoded.height, |
|
buffer, |
|
bufferSize.width, |
|
bufferSize.height |
|
); |
|
} |
|
}, 0); |
|
|
|
page.on("request", async request => { |
|
if (request.url().endsWith("/exporter/blit")) { |
|
// none of these work (it prints either undefined or empty value) |
|
queue.push(request.postData()); |
|
request.respond({ |
|
status: 200 |
|
}); |
|
} else { |
|
request.continue(); |
|
} |
|
}); |
|
await page.exposeFunction( |
|
"exporter_convertLength", |
|
(value, fromUnit, toUnit, opt) => { |
|
return convertLength(value, fromUnit, toUnit, opt); |
|
} |
|
); |
|
await page.exposeFunction( |
|
"tileRenderStart", |
|
async (width, height, opt = {}) => { |
|
spinner = ora("Loading...").start(); |
|
buffer = new Uint8ClampedArray(width * height * 4); |
|
bufferSize = { width, height }; |
|
filename = |
|
[opt.prefix || "", getTimeStamp(), opt.suffix || ""].join("-") + ".png"; |
|
} |
|
); |
|
await page.exposeFunction( |
|
"tileRenderBlit", |
|
async (data, x, y, tileWidth, tileHeight, width, height) => { |
|
blit( |
|
new Uint8ClampedArray(data), |
|
x, |
|
y, |
|
tileWidth, |
|
tileHeight, |
|
buffer, |
|
bufferSize.width, |
|
bufferSize.height |
|
); |
|
} |
|
); |
|
await page.exposeFunction("tileRenderEnd", async () => { |
|
rendering = false; |
|
queue.push(null); |
|
}); |
|
|
|
await page.evaluateOnNewDocument(bootup); |
|
|
|
try { |
|
await page.goto("http://localhost:9966/", { |
|
waitUntil: "load" |
|
}); |
|
} catch (err) { |
|
console.error(err); |
|
console.error( |
|
`Error going to route ${combinedUrl} in the directory ${path.relative( |
|
process.cwd(), |
|
dir |
|
)}` |
|
); |
|
} |
|
} |
|
|
|
function getTimeStamp() { |
|
const dateFormatStr = `yyyy.mm.dd-HH.MM.ss`; |
|
return dateformat(new Date(), dateFormatStr); |
|
} |
|
|
|
function blit( |
|
tileBuffer, |
|
tileX, |
|
tileY, |
|
tileWidth, |
|
tileHeight, |
|
atlasBuffer, |
|
atlasWidth, |
|
atlasHeight |
|
) { |
|
// for each row in the tile buffer, blit that row |
|
for (let row = 0; row < tileHeight; row++) { |
|
const atlasY = tileY + row; |
|
const atlasX = tileX; |
|
// if (atlasY < 0 || atlasX < 0) continue; |
|
if (atlasY >= atlasHeight || atlasX >= atlasWidth) break; |
|
const atlasXPixels = Math.min(tileWidth, atlasWidth - atlasX); |
|
|
|
const tileStartIndex = row * tileWidth; |
|
const tileEndIndex = tileStartIndex + atlasXPixels; |
|
const rowPixels = tileBuffer.subarray(tileStartIndex * 4, tileEndIndex * 4); |
|
|
|
const atlasStartIndex = atlasX + atlasY * atlasWidth; |
|
atlasBuffer.set(rowPixels, atlasStartIndex * 4); |
|
} |
|
} |
|
|
|
(async () => { |
|
await startExport({ |
|
dir: process.cwd() |
|
}); |
|
})(); |