Switch to @resvg/resvg-wasm for generate-images (#35415)
				
					
				
			Use the WASM module of [`resvg-js`](https://github.com/thx/resvg-js) to replace `fabric` and the problematic native `canvas` dependency. WASM works cross-platform so we can include it in the main `package.json`.
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -74,7 +74,6 @@ cpu.out | |||||||
| /tests/*.ini | /tests/*.ini | ||||||
| /tests/**/*.git/**/*.sample | /tests/**/*.git/**/*.sample | ||||||
| /node_modules | /node_modules | ||||||
| /tools/node_modules |  | ||||||
| /.venv | /.venv | ||||||
| /yarn.lock | /yarn.lock | ||||||
| /yarn-error.log | /yarn-error.log | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						| @@ -230,7 +230,7 @@ node-check: | |||||||
|  |  | ||||||
| .PHONY: clean-all | .PHONY: clean-all | ||||||
| clean-all: clean ## delete backend, frontend and integration files | clean-all: clean ## delete backend, frontend and integration files | ||||||
| 	rm -rf $(WEBPACK_DEST_ENTRIES) node_modules tools/node_modules | 	rm -rf $(WEBPACK_DEST_ENTRIES) node_modules | ||||||
|  |  | ||||||
| .PHONY: clean | .PHONY: clean | ||||||
| clean: ## delete backend and integration files | clean: ## delete backend and integration files | ||||||
| @@ -847,10 +847,6 @@ node_modules: pnpm-lock.yaml | |||||||
| 	pnpm install --frozen-lockfile | 	pnpm install --frozen-lockfile | ||||||
| 	@touch node_modules | 	@touch node_modules | ||||||
|  |  | ||||||
| tools/node_modules: tools/package.json |  | ||||||
| 	cd tools && pnpm install |  | ||||||
| 	@touch tools/node_modules |  | ||||||
|  |  | ||||||
| .venv: uv.lock | .venv: uv.lock | ||||||
| 	uv sync | 	uv sync | ||||||
| 	@touch .venv | 	@touch .venv | ||||||
| @@ -925,7 +921,7 @@ generate-gitignore: ## update gitignore files | |||||||
| 	$(GO) run build/generate-gitignores.go | 	$(GO) run build/generate-gitignores.go | ||||||
|  |  | ||||||
| .PHONY: generate-images | .PHONY: generate-images | ||||||
| generate-images: | node_modules tools/node_modules ## generate images (requires cairo development packages) | generate-images: | node_modules ## generate images | ||||||
| 	cd tools && node generate-images.js $(TAGS) | 	cd tools && node generate-images.js $(TAGS) | ||||||
|  |  | ||||||
| .PHONY: generate-manpage | .PHONY: generate-manpage | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ | |||||||
|     "@github/text-expander-element": "2.9.2", |     "@github/text-expander-element": "2.9.2", | ||||||
|     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", |     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", | ||||||
|     "@primer/octicons": "19.15.5", |     "@primer/octicons": "19.15.5", | ||||||
|  |     "@resvg/resvg-wasm": "2.6.2", | ||||||
|     "@silverwind/vue3-calendar-heatmap": "2.0.6", |     "@silverwind/vue3-calendar-heatmap": "2.0.6", | ||||||
|     "@techknowlogick/license-checker-webpack-plugin": "0.3.0", |     "@techknowlogick/license-checker-webpack-plugin": "0.3.0", | ||||||
|     "add-asset-webpack-plugin": "3.0.0", |     "add-asset-webpack-plugin": "3.0.0", | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -35,6 +35,9 @@ importers: | |||||||
|       '@primer/octicons': |       '@primer/octicons': | ||||||
|         specifier: 19.15.5 |         specifier: 19.15.5 | ||||||
|         version: 19.15.5 |         version: 19.15.5 | ||||||
|  |       '@resvg/resvg-wasm': | ||||||
|  |         specifier: 2.6.2 | ||||||
|  |         version: 2.6.2 | ||||||
|       '@silverwind/vue3-calendar-heatmap': |       '@silverwind/vue3-calendar-heatmap': | ||||||
|         specifier: 2.0.6 |         specifier: 2.0.6 | ||||||
|         version: 2.0.6(tippy.js@6.3.7)(vue@3.5.18(typescript@5.8.3)) |         version: 2.0.6(tippy.js@6.3.7)(vue@3.5.18(typescript@5.8.3)) | ||||||
| @@ -797,6 +800,10 @@ packages: | |||||||
|   '@primer/octicons@19.15.5': |   '@primer/octicons@19.15.5': | ||||||
|     resolution: {integrity: sha512-FCXPTlXlHvAS3rRBd1C/xVBYSYzPPwS8tNcUxnvUYK6L4/d+zUy2KExLtzW+L9xKo2z8J9uY+c1VCsNRf+b4MQ==} |     resolution: {integrity: sha512-FCXPTlXlHvAS3rRBd1C/xVBYSYzPPwS8tNcUxnvUYK6L4/d+zUy2KExLtzW+L9xKo2z8J9uY+c1VCsNRf+b4MQ==} | ||||||
|  |  | ||||||
|  |   '@resvg/resvg-wasm@2.6.2': | ||||||
|  |     resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} | ||||||
|  |     engines: {node: '>= 10'} | ||||||
|  |  | ||||||
|   '@rolldown/pluginutils@1.0.0-beta.29': |   '@rolldown/pluginutils@1.0.0-beta.29': | ||||||
|     resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} |     resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} | ||||||
|  |  | ||||||
| @@ -5481,6 +5488,8 @@ snapshots: | |||||||
|     dependencies: |     dependencies: | ||||||
|       object-assign: 4.1.1 |       object-assign: 4.1.1 | ||||||
|  |  | ||||||
|  |   '@resvg/resvg-wasm@2.6.2': {} | ||||||
|  |  | ||||||
|   '@rolldown/pluginutils@1.0.0-beta.29': {} |   '@rolldown/pluginutils@1.0.0-beta.29': {} | ||||||
|  |  | ||||||
|   '@rollup/plugin-commonjs@22.0.2(rollup@2.79.2)': |   '@rollup/plugin-commonjs@22.0.2(rollup@2.79.2)': | ||||||
|   | |||||||
| Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 3.8 KiB | 
| Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 4.7 KiB | 
| Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 4.2 KiB | 
| Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 13 KiB | 
| @@ -1,5 +1,5 @@ | |||||||
| #!/usr/bin/env node | #!/usr/bin/env node | ||||||
| import {loadSVGFromString, Canvas, Rect, util} from 'fabric/node'; // eslint-disable-line import-x/no-unresolved | import {initWasm, Resvg} from '@resvg/resvg-wasm'; | ||||||
| import {optimize} from 'svgo'; | import {optimize} from 'svgo'; | ||||||
| import {readFile, writeFile} from 'node:fs/promises'; | import {readFile, writeFile} from 'node:fs/promises'; | ||||||
| import {argv, exit} from 'node:process'; | import {argv, exit} from 'node:process'; | ||||||
| @@ -27,37 +27,23 @@ async function generate(svg, path, {size, bg}) { | |||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const {objects, options} = await loadSVGFromString(svg); |   const resvgJS = new Resvg(svg, { | ||||||
|   const canvas = new Canvas(); |     fitTo: { | ||||||
|   canvas.setDimensions({width: size, height: size}); |       mode: 'width', | ||||||
|   const ctx = canvas.getContext('2d'); |       value: size, | ||||||
|   ctx.scale(options.width ? (size / options.width) : 1, options.height ? (size / options.height) : 1); |     }, | ||||||
|  |     ...(bg && {background: 'white'}), | ||||||
|   if (bg) { |   }); | ||||||
|     canvas.add(new Rect({ |   const renderedImage = resvgJS.render(); | ||||||
|       left: 0, |   const pngBytes = renderedImage.asPng(); | ||||||
|       top: 0, |   await writeFile(outputFile, Buffer.from(pngBytes)); | ||||||
|       height: size * (1 / (size / options.height)), |  | ||||||
|       width: size * (1 / (size / options.width)), |  | ||||||
|       fill: 'white', |  | ||||||
|     })); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   canvas.add(util.groupSVGElements(objects, options)); |  | ||||||
|   canvas.renderAll(); |  | ||||||
|  |  | ||||||
|   let png = Buffer.from([]); |  | ||||||
|   for await (const chunk of canvas.createPNGStream()) { |  | ||||||
|     png = Buffer.concat([png, chunk]); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   await writeFile(outputFile, png); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| async function main() { | async function main() { | ||||||
|   const gitea = argv.slice(2).includes('gitea'); |   const gitea = argv.slice(2).includes('gitea'); | ||||||
|   const logoSvg = await readFile(new URL('../assets/logo.svg', import.meta.url), 'utf8'); |   const logoSvg = await readFile(new URL('../assets/logo.svg', import.meta.url), 'utf8'); | ||||||
|   const faviconSvg = await readFile(new URL('../assets/favicon.svg', import.meta.url), 'utf8'); |   const faviconSvg = await readFile(new URL('../assets/favicon.svg', import.meta.url), 'utf8'); | ||||||
|  |   await initWasm(await readFile(new URL(import.meta.resolve('@resvg/resvg-wasm/index_bg.wasm')))); | ||||||
|  |  | ||||||
|   await Promise.all([ |   await Promise.all([ | ||||||
|     generate(logoSvg, '../public/assets/img/logo.svg', {size: 32}), |     generate(logoSvg, '../public/assets/img/logo.svg', {size: 32}), | ||||||
|   | |||||||
| @@ -1,21 +0,0 @@ | |||||||
| { |  | ||||||
|   "name": "gitea-tools", |  | ||||||
|   "version": "1.0.0", |  | ||||||
|   "description": "Build tools for Gitea", |  | ||||||
|   "type": "module", |  | ||||||
|   "private": true, |  | ||||||
|   "dependencies": { |  | ||||||
|     "fabric": "^6.7.1", |  | ||||||
|     "svgo": "^4.0.0", |  | ||||||
|     "fast-glob": "^3.3.3" |  | ||||||
|   }, |  | ||||||
|   "optionalDependencies": { |  | ||||||
|     "canvas": "^3.2.0" |  | ||||||
|   }, |  | ||||||
|   "pnpm": { |  | ||||||
|     "onlyBuiltDependencies": ["canvas"], |  | ||||||
|     "overrides": { |  | ||||||
|       "canvas": "3.2.0" |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||