493 lines
12 KiB
Go
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>
|
|
`
|