render-social-preview.mjs 2.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. import { existsSync } from 'node:fs'
  2. import { mkdir, readFile } from 'node:fs/promises'
  3. import { dirname, resolve } from 'node:path'
  4. import { spawnSync } from 'node:child_process'
  5. import { fileURLToPath } from 'node:url'
  6. const scriptDir = dirname(fileURLToPath(import.meta.url))
  7. const sourceRoot = resolve(scriptDir, '..')
  8. const optionValue = (name, fallback) => {
  9. const prefix = `${name}=`
  10. const match = process.argv.slice(2).find(arg => arg.startsWith(prefix))
  11. return match ? match.slice(prefix.length) : fallback
  12. }
  13. const source = resolve(sourceRoot, optionValue('--source', '.github/social-preview.svg'))
  14. const output = resolve(sourceRoot, optionValue('--output', '.github/social-preview.png'))
  15. const width = Number(optionValue('--width', '1280'))
  16. const height = Number(optionValue('--height', '640'))
  17. if (!existsSync(source)) {
  18. throw new Error(`Social preview source not found: ${source}`)
  19. }
  20. if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
  21. throw new Error(`Invalid social preview size: ${width}x${height}`)
  22. }
  23. await mkdir(dirname(output), { recursive: true })
  24. const result = spawnSync(
  25. 'rsvg-convert',
  26. [
  27. source,
  28. '--width',
  29. String(width),
  30. '--height',
  31. String(height),
  32. '--format',
  33. 'png',
  34. '--output',
  35. output
  36. ],
  37. {
  38. cwd: sourceRoot,
  39. encoding: 'utf8',
  40. stdio: 'pipe'
  41. }
  42. )
  43. if (result.status !== 0) {
  44. const details = [result.stderr, result.stdout].filter(Boolean).join('\n')
  45. throw new Error(`Failed to render social preview with rsvg-convert.${details ? `\n${details}` : ''}`)
  46. }
  47. const png = await readFile(output)
  48. const pngSignature = '89504e470d0a1a0a'
  49. if (png.subarray(0, 8).toString('hex') !== pngSignature) {
  50. throw new Error(`Rendered file is not a PNG: ${output}`)
  51. }
  52. const actualWidth = png.readUInt32BE(16)
  53. const actualHeight = png.readUInt32BE(20)
  54. if (actualWidth !== width || actualHeight !== height) {
  55. throw new Error(`Expected ${width}x${height}, got ${actualWidth}x${actualHeight}: ${output}`)
  56. }
  57. console.log(`Rendered ${output.replace(`${sourceRoot}/`, '')} (${actualWidth}x${actualHeight}).`)