Generating Long Event Images for QQ Music Albums: Front‑end and Server‑side Solutions, Pitfalls and Optimizations
To quickly create shareable long‑event images for QQ Music albums, the team evaluated client‑side DOM‑to‑image tools (html2canvas and SVG) that suffered from blur, incompleteness, slow rendering and oversized base64 data, then migrated rendering to the server using PhantomJS, node‑canvas and ultimately ImageMagick, whose combined script execution, mpc caching, and Q8 build provided the most balanced quality‑performance solution despite remaining operational quirks.
Background
The QQ Music "Album" feature needs a fast way to generate a long‑image that summarizes a major event, so fans can share it instantly. Traditional workflows (data collection → poster creation → distribution) are slow and costly.
Browser‑side approaches
Two DOM‑to‑image methods were tried.
1. html2canvas
html2canvas captures a page or a DOM fragment as a canvas. The core code is:
let imgBase64;
html2canvas(htm, {
onrendered: function (canvas) {
// generate base64 image data
imgBase64 = canvas.toDataURL();
}
});Common issues and fixes:
1) Blurred output – enlarge canvas width/height by 2× and keep CSS size at original scale.
<canvas width="200" height="100" style="width:100px;height:50px;"></canvas>2) Incomplete capture – manually pass the correct height to the rendering function.
return renderDocument(node.ownerDocument, options, width, height, index).then(function (canvas) {
if (typeof(options.onrendered) === "function") {
log("options.onrendered is deprecated, html2canvas returns a Promise containing the canvas");
options.onrendered(canvas);
}
return canvas;
});3) Slow rendering – html2canvas traverses the DOM and redraws everything on a canvas, which is inherently time‑consuming. Pre‑generating the image before user interaction is the simplest mitigation.
4) Crash on large base64 – transmitting a >500 KB base64 string can freeze or crash the client.
2. SVG‑based conversion
A lighter, faster method uses an SVG wrapper with a foreignObject containing the HTML.
// DOM to SVG conversion
let htm = '<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="auto"><foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml"><div>这里是页面内容...</div></div></foreignObject></svg>';
let DOMURL = window.URL || window.webkitURL || window;
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
let img = new Image();
let svg = new Blob([htm], {type: 'image/svg+xml;charset=utf-8'});
let url = DOMURL.createObjectURL(svg);
let imgBase64;
img.onload = function () {
ctx.drawImage(img, 0, 0);
imgBase64 = canvas.toDataURL();
};
img.src = url;Remaining problems:
• iOS blocks cross‑origin images, and the crossOrigin attribute cannot bypass the restriction. • The same base64 size issue as html2canvas can cause crashes.
Server‑side approaches
Because client‑side crashes were unacceptable, the team moved the rendering to the backend.
PhantomJS
PhantomJS (a headless WebKit browser) was used via the phantomjs-node library. Installation requires many system libraries (gcc, freetype, etc.). Core rendering code:
var sitepage = null;
var phInstance = null;
phantom.create()
.then(instance => {
phInstance = instance;
return instance.createPage();
})
.then(page => {
let htm = [
'<!DOCTYPE html>',
'<html lang="zh-cn">',
'<head>',
'<meta charset="utf-8">',
'</head>',
'<body style="background:#fff">',
'<div>' + new Date() + '</div>',
'</body>',
'</html>'
].join("");
page.property('content', htm);
page.render('./test.png').then(err => {
phInstance.exit();
}).catch(err => {
phInstance.exit();
})
})
.catch(error => {
phInstance.exit();
});Typical pitfalls and fixes:
1) No screenshot generated – caused by missing write permissions or filename rules (numeric names failed on some environments). 2) Blank output – missing fonts on the server; installing required fonts solved it. 3) Blurry output – scaling the viewport and using zoomFactor (e.g., 2×) helped. 4) Slow generation – a simple render took ~2 s; no easy optimisation was found. 5) Large file size – after 2× scaling the PNG grew to >6 MB; rendering with a lower quality (e.g., 85) reduced size.
node‑canvas
node‑canvas provides a Canvas API in Node.js. Example:
const { createCanvas, loadImage } = require('canvas');
const canvas = createCanvas(200, 200);
const ctx = canvas.getContext('2d');
ctx.font = '30px';
ctx.fillText('test', 50, 100);
loadImage('test.jpg').then(image => {
ctx.drawImage(image, 0, 0, 70, 70);
});Performance was lower than ImageMagick, so the project kept the latter for production.
ImageMagick / GraphicsMagick
ImageMagick was chosen for its rich feature set (over 90 formats, drawing, compositing). The gm Node wrapper was switched to ImageMagick mode:
var gm = require('gm');
var imageMagick = gm.subClass({ imageMagick: true });Key challenges and solutions:
1) Semi‑transparent masks – compute YUV from RGB to decide text color, then use .fill("rgba(0,0,0,.5)"). 2) Rounded avatar corners – draw a circle and fill it with the avatar image.
.fill("avatar.jpg")
.drawCircle(80,120,30,120)3) Emoji in nicknames – use a monospaced font, then draw the emoji image with .draw("image Over " + size + " " + url).
Performance optimisation steps:
• Combine multiple gm operations into a single script executed in a VM to avoid repeated process launches.
let sandbox = {
gm : imageMagick,
start : Date.now()
};
let offset = getOffset();
let qrcodeStr = getQrcodeStr();
let titleStr = (function(){
return [
'.fontSize(24)',
'.fill("gray")',
'.drawText(164,152,"我是标题")'
];
})();
let str = 'gm(828,'+ offset.height +',"#fff").font("'+ FONTS +'",48)'+ titleStr + qrcodeStr +'.quality(90).write("test.jpg",function(err){console.log(err || Date.now() - start)})';
let script = new vm.Script(str);
let context = vm.createContext(sandbox);
script.runInContext(context);• Use ImageMagick’s mpc cache format to keep image attributes in memory and avoid repeated decode/encode. • Prefer the Q8 build (8‑bit) over Q16 for faster processing at the cost of lower colour depth.
Summary
The front‑end DOM‑to‑image solutions (html2canvas, SVG) are convenient but suffer from blur, incompleteness, speed, and large base64 payloads that can crash mobile clients. Moving the rendering to the server with PhantomJS, node‑canvas, or ImageMagick resolves the crash issue, but each brings its own set of operational “坑”. ImageMagick, combined with code‑concatenation, mpc caching, and the Q8 build, currently offers the best trade‑off between quality and performance, though further optimisation is still ongoing.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tencent Music Tech Team
Public account of Tencent Music's development team, focusing on technology sharing and communication.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
