first commit
This commit is contained in:
492
tools/icongen/main.go
Normal file
492
tools/icongen/main.go
Normal file
@@ -0,0 +1,492 @@
|
||||
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>
|
||||
`
|
||||
Reference in New Issue
Block a user