@johnbillion/ghsvg
Version:
Create an SVG of your GitHub Sponsors
104 lines (98 loc) • 3.32 kB
text/typescript
import * as axios from 'axios';
import * as fs from 'fs';
import * as path from 'path';
export function compareDeps(a: string, b: string): number {
function clipNum(dep: string) {
return Number.parseInt((/clip-(\d+)/.exec(dep) as string[])[1]);
}
return clipNum(a) >= clipNum(b) ? 1 : -1;
}
export async function createSVG(
data: Record<string, any>[],
ghsOptions: Record<string, any>
): Promise<string> {
const imageFormat = 'png';
const imageWidth: number = ghsOptions.svgImageWidth;
const svgWidth: number = ghsOptions.svgWidth;
const imageSpacing: number = ghsOptions.svgImageSpace;
const rows: number = Math.floor(svgWidth / (imageWidth + imageSpacing));
const radius: number = Math.floor(imageWidth / 2);
const defs: string[] = [];
const images: string[] = [];
let count = 0;
const cacheDir = 'cache';
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir);
}
await Promise.all(
data.map(async (sponsor: Record<string, string>, index: number) => {
const imageX = index % rows;
const imageY = Math.floor(index / rows);
const locationX = imageX * (imageWidth + imageSpacing);
const locationY = imageY * (imageWidth + imageSpacing);
const avatarCache = path.join(
cacheDir,
sponsor.avatarUrl.split('/u/')[1].split('?')[0] + '.' + imageFormat
);
const avatar = await getAvatar(
sponsor.avatarUrl,
avatarCache,
imageFormat
);
defs.push(` <clipPath id="clip-${index}">
<circle cx="${locationX + radius}" cy="${
locationY + radius
}" r="${radius}"/>
</clipPath>`);
images.push(
` <a xlink:href="${
sponsor.htmlUrl || sponsor.url
}" class="sponsor-svg" target="_blank" id="${sponsor.login}">
<image clip-path="url(#clip-${index})" x="${locationX}" y="${locationY}" width="${imageWidth}" height="${imageWidth}" xlink:href="${avatar}"/>
</a>`
);
if (!ghsOptions.quiet) {
process.stdout.write(
`Fetching sponsors avatars ${Math.floor(
(++count / data.length) * 100
)} % complete... \r`
);
}
})
);
return `<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${svgWidth}" height="${
Math.ceil(data.length / rows) * (imageWidth + imageSpacing)
}">
<defs>
${defs.sort(compareDeps).join('\n')}
</defs>
<style>.sponsor-svg { cursor: pointer; }</style>
${images.sort(compareDeps).join('\n')}
</svg>
`;
}
export function getBase64Image(data: Buffer, format: string): string {
return `data:image/${format};base64,${Buffer.from(data).toString('base64')}`;
}
export async function getAvatar(
avatarUrl: string,
avatarCache: string,
format: string
): Promise<string> {
if (!fs.existsSync(avatarCache)) {
return await new Promise((resolve, reject) => {
axios.default
.get(avatarUrl, {responseType: 'arraybuffer'})
.then(resp => {
fs.writeFileSync(avatarCache, resp.data, {mode: 0o644});
resolve(getBase64Image(resp.data, format));
})
.catch(function (err: any) {
reject(err);
});
});
} else {
return getBase64Image(fs.readFileSync(avatarCache), format);
}
}