Rework 'make generate-images' (#12316)
* Rework 'make generate-images' - Remove external dependencies and replace it with a node script that does does the same. - Move detail removal from gitea-sm.png to favicon.png - Remove favicon.ico and its generation, it is unused and we already serve favicon.png in its place. Fixes: https://github.com/go-gitea/gitea/issues/12314 * use proper centering value for preserveAspectRatio * fix lint * use fabric * better linting fix * fix typo * mention detail-remove class in docs
| @ -33,6 +33,10 @@ overrides: | ||||
|       worker: true | ||||
|     rules: | ||||
|       no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, location, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top] | ||||
|   - files: ["build/generate-images.js"] | ||||
|     rules: | ||||
|       import/no-unresolved: [0] | ||||
|       import/no-extraneous-dependencies: [0] | ||||
| 
 | ||||
| rules: | ||||
|   accessor-pairs: [2] | ||||
|  | ||||
							
								
								
									
										30
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						| @ -648,34 +648,8 @@ update-translations: | ||||
| 
 | ||||
| .PHONY: generate-images | ||||
| generate-images: | ||||
| 	$(eval TMPDIR := $(shell mktemp -d 2>/dev/null || mktemp -d -t 'gitea-temp')) | ||||
| 	mkdir -p $(TMPDIR)/images | ||||
| 	inkscape -f $(PWD)/assets/logo.svg -w 880 -h 880 -e $(PWD)/public/img/gitea-lg.png | ||||
| 	inkscape -f $(PWD)/assets/logo.svg -w 512 -h 512 -e $(PWD)/public/img/gitea-512.png | ||||
| 	inkscape -f $(PWD)/assets/logo.svg -w 192 -h 192 -e $(PWD)/public/img/gitea-192.png | ||||
| 	inkscape -f $(PWD)/assets/logo.svg -w 120 -h 120 -jC -i layer1 -e $(TMPDIR)/images/sm-1.png | ||||
| 	inkscape -f $(PWD)/assets/logo.svg -w 120 -h 120 -jC -i layer2 -e $(TMPDIR)/images/sm-2.png | ||||
| 	composite -compose atop $(TMPDIR)/images/sm-2.png $(TMPDIR)/images/sm-1.png $(PWD)/public/img/gitea-sm.png | ||||
| 	inkscape -f $(PWD)/assets/logo.svg -w 200 -h 200 -e $(PWD)/public/img/avatar_default.png | ||||
| 	inkscape -f $(PWD)/assets/logo.svg -w 180 -h 180 -e $(PWD)/public/img/favicon.png | ||||
| 	inkscape -f $(PWD)/assets/logo.svg -w 128 -h 128 -e $(TMPDIR)/images/128-raw.png | ||||
| 	inkscape -f $(PWD)/assets/logo.svg -w 64 -h 64 -e $(TMPDIR)/images/64-raw.png | ||||
| 	inkscape -f $(PWD)/assets/logo.svg -w 32 -h 32 -jC -i layer1 -e $(TMPDIR)/images/32-1.png | ||||
| 	inkscape -f $(PWD)/assets/logo.svg -w 32 -h 32 -jC -i layer2 -e $(TMPDIR)/images/32-2.png | ||||
| 	composite -compose atop $(TMPDIR)/images/32-2.png $(TMPDIR)/images/32-1.png $(TMPDIR)/images/32-raw.png | ||||
| 	inkscape -f $(PWD)/assets/logo.svg -w 16 -h 16 -jC -i layer1 -e $(TMPDIR)/images/16-raw.png | ||||
| 	zopflipng -m -y $(TMPDIR)/images/128-raw.png $(TMPDIR)/images/128.png | ||||
| 	zopflipng -m -y $(TMPDIR)/images/64-raw.png $(TMPDIR)/images/64.png | ||||
| 	zopflipng -m -y $(TMPDIR)/images/32-raw.png $(TMPDIR)/images/32.png | ||||
| 	zopflipng -m -y $(TMPDIR)/images/16-raw.png $(TMPDIR)/images/16.png | ||||
| 	rm -f $(TMPDIR)/images/*-*.png | ||||
| 	convert $(TMPDIR)/images/16.png $(TMPDIR)/images/32.png \
 | ||||
| 					$(TMPDIR)/images/64.png $(TMPDIR)/images/128.png \
 | ||||
| 					$(PWD)/public/img/favicon.ico | ||||
| 	convert -flatten $(PWD)/public/img/favicon.png $(PWD)/public/img/apple-touch-icon.png | ||||
| 
 | ||||
| 	rm -rf $(TMPDIR)/images | ||||
| 	$(foreach file, $(shell find public/img -type f -name '*.png' ! -name 'loading.png'),zopflipng -m -y $(file) $(file);) | ||||
| 	npm install --no-save --no-package-lock xmldom fabric imagemin-zopfli | ||||
| 	node build/generate-images.js | ||||
| 
 | ||||
| .PHONY: pr\#%
 | ||||
| pr\#%: clean-all | ||||
|  | ||||
| @ -115,6 +115,7 @@ | ||||
|   <g | ||||
|      inkscape:groupmode="layer" | ||||
|      id="layer3" | ||||
|      class="detail-remove" | ||||
|      inkscape:label="Layer 3" | ||||
|      style="display:inline"> | ||||
|     <g | ||||
| @ -157,4 +158,4 @@ | ||||
|          style="fill:none;stroke:#609926;stroke-width:2.68000007;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> | ||||
|     </g> | ||||
|   </g> | ||||
| </svg> | ||||
| </svg> | ||||
| Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB | 
							
								
								
									
										80
									
								
								build/generate-images.js
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						| @ -0,0 +1,80 @@ | ||||
| #!/usr/bin/env node
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| const imageminZopfli = require('imagemin-zopfli'); | ||||
| const {fabric} = require('fabric'); | ||||
| const {DOMParser, XMLSerializer} = require('xmldom'); | ||||
| const {readFile, writeFile} = require('fs').promises; | ||||
| const {resolve} = require('path'); | ||||
| 
 | ||||
| function exit(err) { | ||||
|   if (err) console.error(err); | ||||
|   process.exit(err ? 1 : 0); | ||||
| } | ||||
| 
 | ||||
| function loadSvg(svg) { | ||||
|   return new Promise((resolve) => { | ||||
|     fabric.loadSVGFromString(svg, (objects, options) => { | ||||
|       resolve({objects, options}); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function generate(svg, outputFile, {size, bg, removeDetail} = {}) { | ||||
|   const parser = new DOMParser(); | ||||
|   const serializer = new XMLSerializer(); | ||||
|   const document = parser.parseFromString(svg); | ||||
| 
 | ||||
|   if (removeDetail) { | ||||
|     for (const el of Array.from(document.getElementsByTagName('g') || [])) { | ||||
|       for (const attribute of Array.from(el.attributes || [])) { | ||||
|         if (attribute.name === 'class' && attribute.value === 'detail-remove') { | ||||
|           el.parentNode.removeChild(el); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   svg = serializer.serializeToString(document); | ||||
| 
 | ||||
|   const {objects, options} = await loadSvg(svg); | ||||
|   const canvas = new fabric.Canvas(); | ||||
|   canvas.setDimensions({width: size, height: size}); | ||||
|   const ctx = canvas.getContext('2d'); | ||||
|   ctx.scale(options.width ? (size / options.width) : 1, options.height ? (size / options.height) : 1); | ||||
| 
 | ||||
|   if (bg) { | ||||
|     canvas.add(new fabric.Rect({ | ||||
|       left: 0, | ||||
|       top: 0, | ||||
|       height: size * (1 / (size / options.height)), | ||||
|       width: size * (1 / (size / options.width)), | ||||
|       fill: 'white', | ||||
|     })); | ||||
|   } | ||||
| 
 | ||||
|   canvas.add(fabric.util.groupSVGElements(objects, options)); | ||||
|   canvas.renderAll(); | ||||
| 
 | ||||
|   let png = Buffer.from([]); | ||||
|   for await (const chunk of canvas.createPNGStream()) { | ||||
|     png = Buffer.concat([png, chunk]); | ||||
|   } | ||||
| 
 | ||||
|   png = await imageminZopfli({more: true})(png); | ||||
|   await writeFile(outputFile, png); | ||||
| } | ||||
| 
 | ||||
| async function main() { | ||||
|   const svg = await readFile(resolve(__dirname, '../assets/logo.svg'), 'utf8'); | ||||
|   await generate(svg, resolve(__dirname, '../public/img/gitea-lg.png'), {size: 880}); | ||||
|   await generate(svg, resolve(__dirname, '../public/img/gitea-512.png'), {size: 512}); | ||||
|   await generate(svg, resolve(__dirname, '../public/img/gitea-192.png'), {size: 192}); | ||||
|   await generate(svg, resolve(__dirname, '../public/img/gitea-sm.png'), {size: 120}); | ||||
|   await generate(svg, resolve(__dirname, '../public/img/avatar_default.png'), {size: 200}); | ||||
|   await generate(svg, resolve(__dirname, '../public/img/favicon.png'), {size: 180, removeDetail: true}); | ||||
|   await generate(svg, resolve(__dirname, '../public/img/apple-touch-icon.png'), {size: 180, bg: true}); | ||||
| } | ||||
| 
 | ||||
| main().then(exit).catch(exit); | ||||
| 
 | ||||
| @ -155,10 +155,9 @@ Note: When working on frontend code, set `USE_SERVICE_WORKER` to `false` in `app | ||||
| 
 | ||||
| SVG icons are built using the `make svg` target which compiles the icon sources defined in `build/generate-svg.js` into the output directory `public/img/svg`. Custom icons can be added in the `web_src/svg` directory. | ||||
| 
 | ||||
| ### Building Images | ||||
| ### Building the Logo | ||||
| 
 | ||||
| To build the images, ImageMagick, `inkscape` and `zopflipng` binaries must be available in | ||||
| your `PATH` to run `make generate-images`. | ||||
| The PNG versions of the logo are built from a single SVG source file `assets/logo.svg` using the `make generate-images` target. To run it, Node.js and npm must be available. The same process can also be used to generate a custom logo PNGs from a SVG source file. It's possible to remove parts of the SVG logo for the favicon build by adding a `detail-remove` class to the SVG nodes to be removed. | ||||
| 
 | ||||
| ### Updating the API | ||||
| 
 | ||||
|  | ||||
| Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.5 KiB | 
| Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB | 
| Before Width: | Height: | Size: 88 KiB | 
| Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.8 KiB | 
| Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB | 
| Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB | 
| Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB | 
| Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.6 KiB | 
 silverwind
						silverwind