package main import ( "context" "errors" "flag" "fmt" "image" "image/png" "log/slog" "net/http" "os" "os/signal" "path/filepath" "strconv" "time" "github.com/gen2brain/avif" "golang.org/x/image/draw" "golang.org/x/sync/errgroup" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) func main() { var ( mdb string out string ) flag.StringVar(&mdb, "mdb", "", "`path` to master.mdb") flag.StringVar(&out, "o", "", "output `dir`ectory") flag.Parse() ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) go func() { <-ctx.Done() stop() }() db, err := sqlitex.NewPool(mdb, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly}) if err != nil { slog.Error("mdb", slog.Any("err", err)) os.Exit(1) } if err := os.MkdirAll(out, 0777); err != nil { slog.Error("output", slog.Any("err", err)) os.Exit(1) } skill, err := skillIconIDs(ctx, db) if err != nil { slog.Error("skills", slog.Any("err", err)) } chara, err := charaIDs(ctx, db) if err != nil { slog.Error("characters", slog.Any("err", err)) } rank, err := rankIDs(ctx, db) if err != nil { slog.Error("ranks", slog.Any("err", err)) } g, ctx := errgroup.WithContext(ctx) tick := time.NewTicker(250 * time.Millisecond) idouts(ctx, g, tick, skill, filepath.Join(out, "skill"), "https://media.gametora.com/umamusume/skills/icon/%d.png", image.Pt(64, 64), image.Pt(32, 32), image.Pt(16, 16)) idouts(ctx, g, tick, chara, filepath.Join(out, "chara"), "https://gametora.com/images/umamusume/characters/icons/chr_icon_%d.png", image.Pt(256, 256), image.Pt(64, 64), image.Pt(32, 32), image.Pt(16, 16)) idouts(ctx, g, tick, rank, filepath.Join(out, "rank"), "https://media.gametora.com/umamusume/ui/rank/%02d.png", image.Pt(128, 128), image.Pt(64, 64), image.Pt(32, 32), image.Pt(16, 16)) idouts(ctx, g, tick, rank, filepath.Join(out, "rank", "simple"), "https://media.gametora.com/umamusume/ui/rank/simple/%02d.png", image.Pt(50, 50), image.Pt(32, 32), image.Pt(16, 16)) if err := g.Wait(); err != nil { slog.Error("output", slog.Any("err", err)) } } var pngenc = &png.Encoder{CompressionLevel: png.BestCompression} func idouts(ctx context.Context, g *errgroup.Group, tick *time.Ticker, ids []int32, p, urlf string, sizes ...image.Point) { for _, id := range ids { g.Go(func() error { n := strconv.Itoa(int(id)) p := filepath.Join(p, n) _, err := os.Stat(p) if !errors.Is(err, os.ErrNotExist) { slog.DebugContext(ctx, "exists", slog.String("path", p)) return nil } if err := os.MkdirAll(p, 0777); err != nil { return err } url := fmt.Sprintf(urlf, id) select { case <-ctx.Done(): return ctx.Err() case <-tick.C: // continue on } img, err := dlimg(ctx, url) if err != nil { return err } slog.InfoContext(ctx, "resize", slog.String("url", url)) rs := resize(img, sizes...) for _, r := range rs { g.Go(func() error { m := r.Bounds().Max o := filepath.Join(p, fmt.Sprintf("%dx%d.png", m.X, m.Y)) slog.InfoContext(ctx, "write", slog.String("path", o)) f, err := os.Create(o) if err != nil { return err } defer f.Close() return pngenc.Encode(f, r) }) g.Go(func() error { m := r.Bounds().Max o := filepath.Join(p, fmt.Sprintf("%dx%d.avif", m.X, m.Y)) slog.InfoContext(ctx, "write", slog.String("path", o)) f, err := os.Create(o) if err != nil { return err } defer f.Close() return avif.Encode(f, r) }) } return nil }) } } func dlimg(ctx context.Context, url string) (image.Image, error) { slog.InfoContext(ctx, "download", slog.String("url", url)) ctx, stop := context.WithTimeout(ctx, 5*time.Second) defer stop() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() slog.InfoContext(ctx, "decode", slog.String("url", url)) img, _, err := image.Decode(resp.Body) return img, err } func resize(img image.Image, sizes ...image.Point) []image.Image { rs := make([]image.Image, 0, len(sizes)) for _, sz := range sizes { d := image.NewNRGBA(image.Rectangle{Min: image.Point{}, Max: sz}) draw.CatmullRom.Scale(d, d.Bounds(), img, img.Bounds(), draw.Src, nil) rs = append(rs, d) } return rs }