You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
206 lines
5.0 KiB
206 lines
5.0 KiB
|
7 months ago
|
package core
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/base64"
|
||
|
|
"fmt"
|
||
|
|
"image"
|
||
|
|
"log"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"sync/atomic"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/disintegration/imaging"
|
||
|
|
_ "golang.org/x/image/bmp"
|
||
|
|
_ "golang.org/x/image/webp"
|
||
|
|
)
|
||
|
|
|
||
|
|
// ResampleFilter aliases imaging.ResampleFilter so callers don't need to
|
||
|
|
// import the imaging package directly.
|
||
|
|
type ResampleFilter = imaging.ResampleFilter
|
||
|
|
|
||
|
|
var (
|
||
|
|
ResampleLanczos = imaging.Lanczos
|
||
|
|
ResampleCubic = imaging.CatmullRom
|
||
|
|
ResampleLinear = imaging.Linear
|
||
|
|
ResampleBox = imaging.Box
|
||
|
|
)
|
||
|
|
|
||
|
|
// Run processes all files in summary using cfg, sending a Progress value on
|
||
|
|
// the returned channel for every image (success or failure).
|
||
|
|
// The channel is closed when all images are done.
|
||
|
|
// If cfg.TSFile is set, the TypeScript file is written after all workers finish.
|
||
|
|
func Run(summary *ImageSummary, cfg *Config) <-chan Progress {
|
||
|
|
ch := make(chan Progress, len(summary.Files))
|
||
|
|
total := len(summary.Files)
|
||
|
|
|
||
|
|
go func() {
|
||
|
|
defer close(ch)
|
||
|
|
|
||
|
|
var (
|
||
|
|
wg sync.WaitGroup
|
||
|
|
mu sync.Mutex
|
||
|
|
counter atomic.Int32
|
||
|
|
tsmu sync.Mutex
|
||
|
|
tsEntries []ProcessResult
|
||
|
|
)
|
||
|
|
|
||
|
|
jobs := make(chan string, total)
|
||
|
|
|
||
|
|
for i := 0; i < cfg.Workers; i++ {
|
||
|
|
wg.Add(1)
|
||
|
|
go func() {
|
||
|
|
defer wg.Done()
|
||
|
|
for inputPath := range jobs {
|
||
|
|
result := ProcessOne(inputPath, cfg)
|
||
|
|
current := int(counter.Add(1))
|
||
|
|
|
||
|
|
if result.Success && cfg.TSFile != "" {
|
||
|
|
tsmu.Lock()
|
||
|
|
tsEntries = append(tsEntries, result)
|
||
|
|
tsmu.Unlock()
|
||
|
|
}
|
||
|
|
|
||
|
|
mu.Lock()
|
||
|
|
ch <- Progress{Current: current, Total: total, Result: result}
|
||
|
|
mu.Unlock()
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, f := range summary.Files {
|
||
|
|
jobs <- f
|
||
|
|
}
|
||
|
|
close(jobs)
|
||
|
|
wg.Wait()
|
||
|
|
|
||
|
|
// Write TypeScript file after all workers are done
|
||
|
|
if cfg.TSFile != "" && len(tsEntries) > 0 {
|
||
|
|
if err := WriteTSFile(cfg.TSFile, tsEntries); err != nil {
|
||
|
|
ch <- Progress{
|
||
|
|
Current: total,
|
||
|
|
Total: total,
|
||
|
|
Result: ProcessResult{
|
||
|
|
Filename: cfg.TSFile,
|
||
|
|
Success: false,
|
||
|
|
Error: fmt.Errorf("error writing TypeScript file: %v", err),
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
|
||
|
|
return ch
|
||
|
|
}
|
||
|
|
|
||
|
|
// ProcessOne processes a single image and returns the result.
|
||
|
|
func ProcessOne(inputPath string, cfg *Config) ProcessResult {
|
||
|
|
start := time.Now()
|
||
|
|
|
||
|
|
result := ProcessResult{
|
||
|
|
Filename: filepath.Base(inputPath),
|
||
|
|
}
|
||
|
|
|
||
|
|
if fi, err := os.Stat(inputPath); err == nil {
|
||
|
|
result.InputSize = fi.Size()
|
||
|
|
}
|
||
|
|
|
||
|
|
outputPath := BuildOutputPath(inputPath, cfg)
|
||
|
|
result.OutputPath = outputPath
|
||
|
|
|
||
|
|
base := filepath.Base(outputPath)
|
||
|
|
result.ImageKey = SanitizeTSKey(base[:len(base)-len(filepath.Ext(base))])
|
||
|
|
|
||
|
|
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
|
||
|
|
result.Error = fmt.Errorf("error creating directory: %v", err)
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
f, err := os.Open(inputPath)
|
||
|
|
if err != nil {
|
||
|
|
result.Error = fmt.Errorf("error opening file: %v", err)
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
defer f.Close()
|
||
|
|
|
||
|
|
img, srcFormat, err := image.Decode(f)
|
||
|
|
if err != nil {
|
||
|
|
result.Error = fmt.Errorf("error decoding image: %v", err)
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
if cfg.Verbose {
|
||
|
|
bounds := img.Bounds()
|
||
|
|
log.Printf(" %s: format=%s size=%dx%d",
|
||
|
|
result.Filename, srcFormat, bounds.Dx(), bounds.Dy())
|
||
|
|
}
|
||
|
|
|
||
|
|
resized := imaging.Resize(img, cfg.Width, cfg.Height, GetResampleFilter(cfg.Resample))
|
||
|
|
|
||
|
|
// Resolve the output format
|
||
|
|
outputFormat := cfg.Format
|
||
|
|
if outputFormat == "" {
|
||
|
|
if cfg.B64Enabled || cfg.TSFile != "" {
|
||
|
|
srcExt := strings.TrimPrefix(strings.ToLower(filepath.Ext(inputPath)), ".")
|
||
|
|
outputFormat = ExtToFormat(srcExt)
|
||
|
|
} else {
|
||
|
|
outputFormat = strings.TrimPrefix(strings.ToLower(filepath.Ext(outputPath)), ".")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Encode into a buffer (always — enables b64 and avoids double-open)
|
||
|
|
var buf bytes.Buffer
|
||
|
|
mimeType, encErr := EncodeImage(&buf, resized, outputFormat, cfg.Quality)
|
||
|
|
if encErr != nil {
|
||
|
|
result.Error = fmt.Errorf("error encoding image: %v", encErr)
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build base64 data URI when needed
|
||
|
|
if cfg.B64Enabled || cfg.TSFile != "" {
|
||
|
|
encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||
|
|
result.B64Data = "data:" + mimeType + ";base64," + encoded
|
||
|
|
}
|
||
|
|
|
||
|
|
// Write output (skip when -ts: writing is deferred to WriteTSFile)
|
||
|
|
if cfg.TSFile == "" {
|
||
|
|
var writeErr error
|
||
|
|
if cfg.B64Enabled {
|
||
|
|
writeErr = os.WriteFile(outputPath, []byte(result.B64Data), 0644)
|
||
|
|
} else {
|
||
|
|
writeErr = os.WriteFile(outputPath, buf.Bytes(), 0644)
|
||
|
|
}
|
||
|
|
if writeErr != nil {
|
||
|
|
result.Error = fmt.Errorf("error writing output file: %v", writeErr)
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
if fi, err := os.Stat(outputPath); err == nil {
|
||
|
|
result.OutputSize = fi.Size()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
result.Success = true
|
||
|
|
result.Duration = time.Since(start)
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
// WriteTSFile writes a TypeScript module exporting all images as data URIs.
|
||
|
|
func WriteTSFile(path string, entries []ProcessResult) error {
|
||
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
var sb strings.Builder
|
||
|
|
sb.WriteString("export const images: Record<string, string> = {\n")
|
||
|
|
for _, e := range entries {
|
||
|
|
fmt.Fprintf(&sb, " %s: %q,\n", e.ImageKey, e.B64Data)
|
||
|
|
}
|
||
|
|
sb.WriteString("}\n")
|
||
|
|
|
||
|
|
return os.WriteFile(path, []byte(sb.String()), 0644)
|
||
|
|
}
|