Files
ip-echo/tools/icongen/main.go
2026-05-13 03:05:24 +03:30

493 lines
12 KiB
Go

package main
import (
"bytes"
"encoding/binary"
"fmt"
"image"
"image/color"
"image/png"
"math"
"os"
"path/filepath"
"time"
)
const (
resourceLanguageEnglishUS = 1033
resourceTypeIcon = 3
resourceTypeGroupIcon = 14
)
type iconImage struct {
ID uint16
Size int
PNG []byte
Planes uint16
Bits uint16
}
type relocation struct {
offset uint32
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
if err := os.MkdirAll("assets", 0o755); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join("cmd", "ipecho"), 0o755); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join("internal", "httpapi", "static"), 0o755); err != nil {
return err
}
if err := os.WriteFile(filepath.Join("assets", "icon.svg"), []byte(iconSVG), 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join("assets", "favicon.svg"), []byte(iconSVG), 0o644); err != nil {
return err
}
images, err := renderIconImages([]int{16, 24, 32, 48, 64, 128, 256})
if err != nil {
return err
}
ico, err := buildICO(images)
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join("assets", "ipecho.ico"), ico, 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join("assets", "favicon.ico"), ico, 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join("internal", "httpapi", "static", "favicon.ico"), ico, 0o644); err != nil {
return err
}
syso, err := buildWindowsResourceObject(images)
if err != nil {
return err
}
return os.WriteFile(filepath.Join("cmd", "ipecho", "rsrc_windows_amd64.syso"), syso, 0o644)
}
func renderIconImages(sizes []int) ([]iconImage, error) {
images := make([]iconImage, 0, len(sizes))
for index, size := range sizes {
var pngData bytes.Buffer
if err := png.Encode(&pngData, renderIcon(size)); err != nil {
return nil, err
}
images = append(images, iconImage{
ID: uint16(index + 1),
Size: size,
PNG: pngData.Bytes(),
Planes: 1,
Bits: 32,
})
}
return images, nil
}
func renderIcon(size int) image.Image {
const scale = 4
large := image.NewNRGBA(image.Rect(0, 0, size*scale, size*scale))
renderIconAt(large)
return downsample(large, scale)
}
func renderIconAt(img *image.NRGBA) {
size := float64(img.Bounds().Dx())
unit := size / 256
blue := color.NRGBA{R: 17, G: 103, B: 177, A: 255}
darkBlue := color.NRGBA{R: 10, G: 82, B: 148, A: 255}
white := color.NRGBA{R: 255, G: 255, B: 255, A: 255}
orange := color.NRGBA{R: 245, G: 159, B: 69, A: 255}
cyan := color.NRGBA{R: 65, G: 199, B: 215, A: 255}
green := color.NRGBA{R: 139, G: 214, B: 170, A: 255}
fillRoundedRect(img, 8*unit, 8*unit, 248*unit, 248*unit, 42*unit, darkBlue)
fillRoundedRect(img, 0, 0, 240*unit, 240*unit, 42*unit, blue)
drawLine(img, 76*unit, 128*unit, 180*unit, 128*unit, 10*unit, white)
drawLine(img, 128*unit, 76*unit, 128*unit, 180*unit, 10*unit, white)
fillCircle(img, 76*unit, 128*unit, 31*unit, white)
fillCircle(img, 180*unit, 128*unit, 31*unit, white)
fillCircle(img, 128*unit, 76*unit, 31*unit, white)
fillCircle(img, 128*unit, 180*unit, 31*unit, white)
fillCircle(img, 76*unit, 128*unit, 22*unit, orange)
fillCircle(img, 180*unit, 128*unit, 22*unit, cyan)
fillCircle(img, 128*unit, 76*unit, 22*unit, green)
fillCircle(img, 128*unit, 180*unit, 22*unit, white)
}
func fillRoundedRect(img *image.NRGBA, x0, y0, x1, y1, radius float64, c color.NRGBA) {
for y := int(math.Floor(y0)); y < int(math.Ceil(y1)); y++ {
for x := int(math.Floor(x0)); x < int(math.Ceil(x1)); x++ {
if insideRoundedRect(float64(x)+0.5, float64(y)+0.5, x0, y0, x1, y1, radius) {
setNRGBA(img, x, y, c)
}
}
}
}
func insideRoundedRect(x, y, x0, y0, x1, y1, radius float64) bool {
if x < x0 || x >= x1 || y < y0 || y >= y1 {
return false
}
cx := math.Min(math.Max(x, x0+radius), x1-radius)
cy := math.Min(math.Max(y, y0+radius), y1-radius)
dx := x - cx
dy := y - cy
return dx*dx+dy*dy <= radius*radius
}
func drawLine(img *image.NRGBA, x0, y0, x1, y1, radius float64, c color.NRGBA) {
dx := x1 - x0
dy := y1 - y0
steps := int(math.Ceil(math.Hypot(dx, dy)))
if steps < 1 {
steps = 1
}
for i := 0; i <= steps; i++ {
t := float64(i) / float64(steps)
fillCircle(img, x0+dx*t, y0+dy*t, radius, c)
}
}
func fillCircle(img *image.NRGBA, cx, cy, radius float64, c color.NRGBA) {
minX := int(math.Floor(cx - radius))
maxX := int(math.Ceil(cx + radius))
minY := int(math.Floor(cy - radius))
maxY := int(math.Ceil(cy + radius))
radius2 := radius * radius
for y := minY; y <= maxY; y++ {
for x := minX; x <= maxX; x++ {
dx := float64(x) + 0.5 - cx
dy := float64(y) + 0.5 - cy
if dx*dx+dy*dy <= radius2 {
setNRGBA(img, x, y, c)
}
}
}
}
func setNRGBA(img *image.NRGBA, x, y int, c color.NRGBA) {
if !image.Pt(x, y).In(img.Bounds()) {
return
}
offset := img.PixOffset(x, y)
img.Pix[offset+0] = c.R
img.Pix[offset+1] = c.G
img.Pix[offset+2] = c.B
img.Pix[offset+3] = c.A
}
func downsample(src *image.NRGBA, scale int) *image.NRGBA {
dst := image.NewNRGBA(image.Rect(0, 0, src.Bounds().Dx()/scale, src.Bounds().Dy()/scale))
for y := 0; y < dst.Bounds().Dy(); y++ {
for x := 0; x < dst.Bounds().Dx(); x++ {
var r, g, b, a uint32
for sy := 0; sy < scale; sy++ {
for sx := 0; sx < scale; sx++ {
offset := src.PixOffset(x*scale+sx, y*scale+sy)
r += uint32(src.Pix[offset+0])
g += uint32(src.Pix[offset+1])
b += uint32(src.Pix[offset+2])
a += uint32(src.Pix[offset+3])
}
}
count := uint32(scale * scale)
offset := dst.PixOffset(x, y)
dst.Pix[offset+0] = uint8(r / count)
dst.Pix[offset+1] = uint8(g / count)
dst.Pix[offset+2] = uint8(b / count)
dst.Pix[offset+3] = uint8(a / count)
}
}
return dst
}
func buildICO(images []iconImage) ([]byte, error) {
var buf bytes.Buffer
writeLE(&buf, uint16(0))
writeLE(&buf, uint16(1))
writeLE(&buf, uint16(len(images)))
imageOffset := 6 + len(images)*16
for _, img := range images {
buf.WriteByte(iconDimensionByte(img.Size))
buf.WriteByte(iconDimensionByte(img.Size))
buf.WriteByte(0)
buf.WriteByte(0)
writeLE(&buf, img.Planes)
writeLE(&buf, img.Bits)
writeLE(&buf, uint32(len(img.PNG)))
writeLE(&buf, uint32(imageOffset))
imageOffset += len(img.PNG)
}
for _, img := range images {
if _, err := buf.Write(img.PNG); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
func buildWindowsResourceObject(images []iconImage) ([]byte, error) {
section, relocations, err := buildResourceSection(images)
if err != nil {
return nil, err
}
const (
machineAMD64 = 0x8664
sectionHeaderSize = 40
fileHeaderSize = 20
symbolSize = 18
relocationSize = 10
imageRelAMD64Addr32NB = 0x0003
imageScnCntInitializedData = 0x00000040
imageScnAlign4Bytes = 0x00300000
imageScnMemRead = 0x40000000
imageSymClassStatic = 3
)
rawDataPointer := uint32(fileHeaderSize + sectionHeaderSize)
relocationPointer := rawDataPointer + uint32(len(section))
symbolPointer := relocationPointer + uint32(len(relocations)*relocationSize)
var buf bytes.Buffer
writeLE(&buf, uint16(machineAMD64))
writeLE(&buf, uint16(1))
writeLE(&buf, uint32(time.Now().Unix()))
writeLE(&buf, symbolPointer)
writeLE(&buf, uint32(1))
writeLE(&buf, uint16(0))
writeLE(&buf, uint16(0))
writeSectionName(&buf, ".rsrc")
writeLE(&buf, uint32(0))
writeLE(&buf, uint32(0))
writeLE(&buf, uint32(len(section)))
writeLE(&buf, rawDataPointer)
writeLE(&buf, relocationPointer)
writeLE(&buf, uint32(0))
writeLE(&buf, uint16(len(relocations)))
writeLE(&buf, uint16(0))
writeLE(&buf, uint32(imageScnCntInitializedData|imageScnAlign4Bytes|imageScnMemRead))
buf.Write(section)
for _, rel := range relocations {
writeLE(&buf, rel.offset)
writeLE(&buf, uint32(0))
writeLE(&buf, uint16(imageRelAMD64Addr32NB))
}
writeSectionName(&buf, ".rsrc")
writeLE(&buf, uint32(0))
writeLE(&buf, int16(1))
writeLE(&buf, uint16(0))
buf.WriteByte(imageSymClassStatic)
buf.WriteByte(0)
writeLE(&buf, uint32(4))
return buf.Bytes(), nil
}
func buildResourceSection(images []iconImage) ([]byte, []relocation, error) {
var b resourceBuilder
b.directory(0, 2)
rootEntryIcon := b.entry(resourceTypeIcon, 0)
rootEntryGroupIcon := b.entry(resourceTypeGroupIcon, 0)
iconTypeDir := b.directory(0, uint16(len(images)))
b.patchDirectoryEntry(rootEntryIcon, resourceTypeIcon, iconTypeDir)
iconEntryOffsets := make([]int, len(images))
for i, img := range images {
iconEntryOffsets[i] = b.entry(uint32(img.ID), 0)
}
groupTypeDir := b.directory(0, 1)
b.patchDirectoryEntry(rootEntryGroupIcon, resourceTypeGroupIcon, groupTypeDir)
groupEntry := b.entry(1, 0)
for i, img := range images {
nameDir := b.directory(0, 1)
b.patchDirectoryEntry(iconEntryOffsets[i], uint32(img.ID), nameDir)
langEntry := b.entry(resourceLanguageEnglishUS, 0)
dataEntry := b.dataEntry(img.PNG)
b.patchDataEntry(langEntry, resourceLanguageEnglishUS, dataEntry)
}
groupNameDir := b.directory(0, 1)
b.patchDirectoryEntry(groupEntry, 1, groupNameDir)
groupLangEntry := b.entry(resourceLanguageEnglishUS, 0)
groupData := buildGroupIconResource(images)
groupDataEntry := b.dataEntry(groupData)
b.patchDataEntry(groupLangEntry, resourceLanguageEnglishUS, groupDataEntry)
return b.bytes(), b.relocations, nil
}
type resourceBuilder struct {
buf bytes.Buffer
relocations []relocation
}
func (b *resourceBuilder) directory(namedEntries, idEntries uint16) int {
offset := b.buf.Len()
writeLE(&b.buf, uint32(0))
writeLE(&b.buf, uint32(0))
writeLE(&b.buf, uint16(0))
writeLE(&b.buf, uint16(0))
writeLE(&b.buf, namedEntries)
writeLE(&b.buf, idEntries)
return offset
}
func (b *resourceBuilder) entry(id uint32, offset uint32) int {
entryOffset := b.buf.Len()
writeLE(&b.buf, id)
writeLE(&b.buf, offset)
return entryOffset
}
func (b *resourceBuilder) patchDirectoryEntry(entryOffset int, id uint32, directoryOffset int) {
b.writeAt(entryOffset, id)
b.writeAt(entryOffset+4, uint32(directoryOffset)|0x80000000)
}
func (b *resourceBuilder) patchDataEntry(entryOffset int, id uint32, dataEntryOffset int) {
b.writeAt(entryOffset, id)
b.writeAt(entryOffset+4, uint32(dataEntryOffset))
}
func (b *resourceBuilder) dataEntry(data []byte) int {
b.align4()
dataEntryOffset := b.buf.Len()
writeLE(&b.buf, uint32(0))
writeLE(&b.buf, uint32(len(data)))
writeLE(&b.buf, uint32(0))
writeLE(&b.buf, uint32(0))
b.align4()
dataOffset := b.buf.Len()
b.writeAt(dataEntryOffset, uint32(dataOffset))
b.relocations = append(b.relocations, relocation{offset: uint32(dataEntryOffset)})
b.buf.Write(data)
b.align4()
return dataEntryOffset
}
func (b *resourceBuilder) align4() {
for b.buf.Len()%4 != 0 {
b.buf.WriteByte(0)
}
}
func (b *resourceBuilder) writeAt(offset int, value uint32) {
binary.LittleEndian.PutUint32(b.buf.Bytes()[offset:offset+4], value)
}
func (b *resourceBuilder) bytes() []byte {
return b.buf.Bytes()
}
func buildGroupIconResource(images []iconImage) []byte {
var buf bytes.Buffer
writeLE(&buf, uint16(0))
writeLE(&buf, uint16(1))
writeLE(&buf, uint16(len(images)))
for _, img := range images {
buf.WriteByte(iconDimensionByte(img.Size))
buf.WriteByte(iconDimensionByte(img.Size))
buf.WriteByte(0)
buf.WriteByte(0)
writeLE(&buf, img.Planes)
writeLE(&buf, img.Bits)
writeLE(&buf, uint32(len(img.PNG)))
writeLE(&buf, img.ID)
}
return buf.Bytes()
}
func iconDimensionByte(size int) byte {
if size >= 256 {
return 0
}
return byte(size)
}
func writeLE[T ~uint16 | ~uint32 | ~int16](buf *bytes.Buffer, value T) {
_ = binary.Write(buf, binary.LittleEndian, value)
}
func writeSectionName(buf *bytes.Buffer, name string) {
var raw [8]byte
copy(raw[:], name)
buf.Write(raw[:])
}
const iconSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="IP Echo">
<rect x="8" y="8" width="240" height="240" rx="42" fill="#0a5294"/>
<rect width="240" height="240" rx="42" fill="#1167b1"/>
<path d="M76 128h104M128 76v104" stroke="#fff" stroke-width="20" stroke-linecap="round"/>
<circle cx="76" cy="128" r="31" fill="#fff"/>
<circle cx="180" cy="128" r="31" fill="#fff"/>
<circle cx="128" cy="76" r="31" fill="#fff"/>
<circle cx="128" cy="180" r="31" fill="#fff"/>
<circle cx="76" cy="128" r="22" fill="#f59f45"/>
<circle cx="180" cy="128" r="22" fill="#41c7d7"/>
<circle cx="128" cy="76" r="22" fill="#8bd6aa"/>
<circle cx="128" cy="180" r="22" fill="#fff"/>
</svg>
`