diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..581f392 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,10 @@ +builds: + - goos: + - windows + - darwin + - linux +archive: + format: binary + +snapshot: + name_template: SNAPSHOT \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..97626ba --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0a76216 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a1105b8 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app-builder.iml b/app-builder.iml new file mode 100644 index 0000000..eacc75a --- /dev/null +++ b/app-builder.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/commands/icon.go b/commands/icon.go new file mode 100644 index 0000000..7d4e033 --- /dev/null +++ b/commands/icon.go @@ -0,0 +1,214 @@ +package commands + +import ( + "image" + "os" + + "github.com/disintegration/imaging" + "image/png" + "bufio" + "text/template" + "bytes" + "sync" + "runtime" + "os/exec" + "io/ioutil" + "path" + "encoding/json" + "fmt" + "github.com/develar/app-builder/util" + "strings" +) + +type IconInfo struct { + File string `json:"file"` + Size int `json:"size"` +} + +type Icns2PngMapping struct { + Id string + Size int +} + +var icns2PngMappingList = []Icns2PngMapping{ + {"is32", 16}, + {"il32", 32}, + {"ih32", 48}, + {"icp6", 64}, + {"it32", 128}, + {"ic08", 256}, + {"ic09", 512}, +} + +type ConvertIcnsToPngResult struct { + MaxIconPath string `json:"maxIconPath"` + Icons []IconInfo `json:"icons"` +} + +func ConvertIcnsToPng(inFile string) error { + tempDir, err := util.TempDir(os.Getenv("ELECTRON_BUILDER_TMP_DIR"), ".iconset") + if err != nil { + return err + } + + var maxIconPath string + var result []IconInfo + + sizeList := []int{24, 96} + outFileTemplate := path.Join(tempDir, "icon_{{.Width}}x{{.Height}}.png") + if runtime.GOOS == "darwin" && os.Getenv("FORCE_ICNS2PNG") == "" { + output, err := exec.Command("iconutil", "--convert", "iconset", "--output", tempDir, inFile).CombinedOutput() + if err != nil { + fmt.Println(string(output)) + return err + } + + iconFiles, err := ioutil.ReadDir(tempDir) + if err != nil { + return err + } + + for _, item := range icns2PngMappingList { + fileName := fmt.Sprintf("icon_%dx%d.png", item.Size, item.Size) + if contains(iconFiles, fileName) { + // list sorted by size, so, last assignment is a max size + maxIconPath = path.Join(tempDir, fileName) + result = append(result, IconInfo{maxIconPath, item.Size}) + } else { + sizeList = append(sizeList, item.Size) + } + } + } else { + outputBytes, err := exec.Command("icns2png", "--extract", "--output", tempDir, inFile).CombinedOutput() + output := string(outputBytes) + if err != nil { + fmt.Println(output) + return err + } + + namePrefix := strings.TrimSuffix(path.Base(inFile), path.Ext(inFile)) + + for _, item := range icns2PngMappingList { + if strings.Contains(output, item.Id) { + // list sorted by size, so, last assignment is a max size + maxIconPath = path.Join(tempDir, fmt.Sprintf("%s_%dx%dx32.png", namePrefix, item.Size, item.Size)) + result = append(result, IconInfo{maxIconPath, item.Size}) + } else { + sizeList = append(sizeList, item.Size) + } + } + } + + err = multiResizeImage(maxIconPath, outFileTemplate, &result, sizeList, nil) + if err != nil { + return err + } + + serializedResult, err := json.Marshal(ConvertIcnsToPngResult{ + MaxIconPath: maxIconPath, + Icons: result, + }) + if err != nil { + return err + } + + _, err = os.Stdout.Write(serializedResult) + if err != nil { + return err + } + + return nil +} + +func contains(files []os.FileInfo, name string) bool { + for _, fileInfo := range files { + if fileInfo.Name() == name { + return true + } + } + return false +} + +func multiResizeImage(inFile string, outFileNameTemplateString string, result *[]IconInfo, wList []int, hList []int) (error) { + reader, err := os.Open(inFile) + if err != nil { + return err + } + + originalImage, _, err := image.Decode(reader) + if err != nil { + return err + } + + if hList == nil || len(hList) == 0 { + hList = wList + } + + outFileNameTemplate := template.Must(template.New("name").Parse(outFileNameTemplateString)) + + var waitGroup sync.WaitGroup + + imageCount := len(wList) + waitGroup.Add(imageCount) + + for i := 0; i < imageCount; i++ { + w := wList[i] + h := hList[i] + + outFilePath, err := computeName(outFileNameTemplate, map[string]interface{}{ + "Width": w, + "Height": h, + }) + if err != nil { + return err + } + + *result = append(*result, IconInfo{ + File: outFilePath, + Size: w, + }) + go resizeImage(originalImage, w, h, outFilePath, &waitGroup) + } + + waitGroup.Wait() + return nil +} + +func computeName(template *template.Template, data interface{}) (string, error) { + outFileNameBuffer := &bytes.Buffer{} + err := template.Execute(outFileNameBuffer, data) + if err != nil { + return "", err + } + return outFileNameBuffer.String(), nil +} + +func resizeImage(originalImage image.Image, w int, h int, outFileName string, waitGroup *sync.WaitGroup) error { + defer waitGroup.Done() + newImage := imaging.Resize(originalImage, w, h, imaging.Lanczos) + return saveImage(newImage, outFileName) +} + +func saveImage(image *image.NRGBA, outFileName string) error { + outFile, err := os.Create(outFileName) + if err != nil { + return err + } + + writer := bufio.NewWriter(outFile) + err = png.Encode(writer, image) + if err != nil { + return err + } + + flushError := writer.Flush() + closeError := outFile.Close() + if flushError != nil { + return flushError + } + if closeError != nil { + return closeError + } + + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..b55c25e --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "gopkg.in/alecthomas/kingpin.v2" + "os" + "github.com/develar/app-builder/commands" + "log" +) + +var ( + app = kingpin.New("app-builder", "app-builder").Version("0.1.0") + + icnsToPng = app.Command("icns-to-png", "convert ICNS to PNG") + icnsToPngInFile = icnsToPng.Flag("input", "input ICNS file").Short('i').Required().String() +) + +func main() { + switch kingpin.MustParse(app.Parse(os.Args[1:])) { + case icnsToPng.FullCommand(): + err := commands.ConvertIcnsToPng(*icnsToPngInFile) + if err != nil { + log.Fatal(err) + } + } +} diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..4458809 --- /dev/null +++ b/publish.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -e + +BASEDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +if [ -z "$GITHUB_TOKEN" ] ; then + SEC=`security find-generic-password -l GH_TOKEN -g 2>&1` + export GITHUB_TOKEN=`echo "$SEC" | grep "password" | cut -d \" -f 2` +fi + +NAME=app-builder +VERSION=0.1.0 + +OUT_DIR="$BASEDIR/dist/out" +rm -rf "$OUT_DIR" + +publish() +{ + outDir=$1 + archiveName="$NAME-v$VERSION-$2" + archiveFile="$OUT_DIR/$archiveName.7z" + + cd "$BASEDIR/dist/$outDir" + 7za a -mx=9 -mfb=64 "$archiveFile" . +} + +publish "darwinamd64" mac +publish "linux386" linux-ia32 +publish "linuxamd64" linux-x64 +publish "windows386" win-ia32 +publish "windowsamd64" win-x64 + +tool-releaser develar/app-builder "v$VERSION" master "" "$OUT_DIR/*.7z" \ No newline at end of file diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..7926669 --- /dev/null +++ b/release.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +goreleaser --rm-dist "$@" \ No newline at end of file diff --git a/util/tempfile.go b/util/tempfile.go new file mode 100644 index 0000000..8ea4ed2 --- /dev/null +++ b/util/tempfile.go @@ -0,0 +1,73 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package util + +import ( + "os" + "path/filepath" + "strconv" + "sync" + "time" +) + +// Random number state. +// We generate random temporary file names so that there's a good +// chance the file doesn't exist yet - keeps the number of tries in +// TempFile to a minimum. +var rand uint32 +var randmu sync.Mutex + +func reseed() uint32 { + return uint32(time.Now().UnixNano() + int64(os.Getpid())) +} + +func nextPrefix() string { + randmu.Lock() + r := rand + if r == 0 { + r = reseed() + } + r = r*1664525 + 1013904223 // constants from Numerical Recipes + rand = r + randmu.Unlock() + return strconv.Itoa(int(1e9 + r%1e9))[1:] +} + +// TempDir creates a new temporary directory in the directory dir +// with a name beginning with prefix and returns the path of the +// new directory. If dir is the empty string, TempDir uses the +// default directory for temporary files (see os.TempDir). +// Multiple programs calling TempDir simultaneously +// will not choose the same directory. It is the caller's responsibility +// to remove the directory when no longer needed. +func TempDir(dir, suffix string) (name string, err error) { + if dir == "" { + dir = os.TempDir() + } + + nConflict := 0 + for i := 0; i < 10000; i++ { + try := filepath.Join(dir, nextPrefix() + suffix) + err = os.Mkdir(try, 0700) + if os.IsExist(err) { + if nConflict++; nConflict > 10 { + randmu.Lock() + rand = reseed() + randmu.Unlock() + } + continue + } + if os.IsNotExist(err) { + if _, err := os.Stat(dir); os.IsNotExist(err) { + return "", err + } + } + if err == nil { + name = try + } + break + } + return +}