package pictadd
import (
"bytes"
"context"
"fmt"
"image"
"image/draw"
"image/jpeg"
"time"
// Adds png support.
_ "image/png"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"unicode"
"github.com/anthonynsimon/bild/transform"
"github.com/google/uuid"
"github.com/perlw/pict/internal/pkg/store"
)
type FileWithMetadata struct {
Filename string
Tags []string
Title string
Timestamp int64
}
func generateThumbnail(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("could not open source file: %w", err)
}
data, err := ioutil.ReadAll(srcFile)
if err != nil {
return fmt.Errorf("could not read source file: %w", err)
}
srcFile.Close()
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return fmt.Errorf("could not decode image: %w", err)
}
srcW := float64(img.Bounds().Dx())
srcH := float64(img.Bounds().Dy())
dstW, dstH := 160, 120
dstHW, dstHH := dstW/2, dstH/2
if srcW > srcH {
ratio := srcH / srcW
dstH = int(float64(dstW) * ratio)
} else {
ratio := srcW / srcH
dstW = int(float64(dstH) * ratio)
}
resized := transform.Resize(img, dstW, dstH, transform.Lanczos)
thumbImg := image.NewRGBA(image.Rect(0, 0, 160, 120))
draw.Draw(
thumbImg, image.Rect(
dstHW-(dstW/2), dstHH-(dstH/2), 160, 120,
),
resized, image.Point{}, draw.Src,
)
dstFile, err := os.Create(dst)
if err != nil {
return fmt.Errorf("could not create destination: %w", err)
}
err = jpeg.Encode(dstFile, thumbImg, nil)
if err != nil {
return fmt.Errorf("could not encode destination image: %w", err)
}
dstFile.Close()
return nil
}
func generatePreview(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("could not open source file: %w", err)
}
data, err := ioutil.ReadAll(srcFile)
if err != nil {
return fmt.Errorf("could not read source file: %w", err)
}
srcFile.Close()
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return fmt.Errorf("could not decode image: %w", err)
}
srcW := float64(img.Bounds().Dx())
srcH := float64(img.Bounds().Dy())
dstW, dstH := 1000, 1000
if srcW > srcH {
ratio := srcH / srcW
dstH = int(float64(dstW) * ratio)
} else {
ratio := srcW / srcH
dstW = int(float64(dstH) * ratio)
}
dstHW, dstHH := dstW/2, dstH/2
resized := transform.Resize(img, dstW, dstH, transform.Lanczos)
thumbImg := image.NewRGBA(image.Rect(0, 0, dstW, dstH))
draw.Draw(
thumbImg, image.Rect(
dstHW-(dstW/2), dstHH-(dstH/2), dstW, dstH,
),
resized, image.Point{}, draw.Src,
)
dstFile, err := os.Create(dst)
if err != nil {
return fmt.Errorf("could not create destination: %w", err)
}
err = jpeg.Encode(dstFile, thumbImg, nil)
if err != nil {
return fmt.Errorf("could not encode destination image: %w", err)
}
dstFile.Close()
return nil
}
// Option is an option for the pictadd app.
type Option func(*App)
// WithStoreProvider sets the store provider.
func WithStoreProvider(provider store.WriteProvider) Option {
return func(a *App) {
a.store = provider
}
}
// WithGoAhead skips any yes/no questions and just runs the import.
func WithGoAhead() Option {
return func(a *App) {
a.goAhead = true
}
}
// WithImageStorageDir specifies where to put image files on disk.
func WithImageStorageDir(storageDir string) Option {
return func(a *App) {
a.storageDir = storageDir
}
}
// App is the actual pictadd app.
type App struct {
store store.WriteProvider
goAhead bool
storageDir string
}
// New sets up a new pictadd app.
func New(options ...Option) (*App, error) {
var a App
for _, opt := range options {
opt(&a)
}
if a.store == nil {
return nil, fmt.Errorf("missing store")
}
if a.storageDir == "" {
return nil, fmt.Errorf("missing storage dir")
}
return &a, nil
}
// FixThumbnails loops through images and generates thumbnails.
func (a *App) FixThumbnails(filesWithMetadata []FileWithMetadata) error {
for _, data := range filesWithMetadata {
thumbname := strings.TrimSuffix(
data.Filename, filepath.Ext(data.Filename),
) + "_th.jpg"
if err := generateThumbnail(data.Filename, thumbname); err != nil {
fmt.Printf("could not generate thumbnail for %s: %s\n", data.Filename, err.Error())
continue
}
fmt.Printf("%s generated\n", thumbname)
}
return nil
}
// FixPreviews loops through images and generates thumbnails.
func (a *App) FixPreviews(filesWithMetadata []FileWithMetadata) error {
for _, data := range filesWithMetadata {
name := strings.TrimSuffix(
data.Filename, filepath.Ext(data.Filename),
) + "_p.jpg"
if err := generatePreview(data.Filename, name); err != nil {
fmt.Printf("could not generate preview for %s: %s\n", data.Filename, err.Error())
continue
}
fmt.Printf("%s generated\n", name)
}
return nil
}
// Import imports images and tags into database.
func (a *App) Import(filesWithMetadata []FileWithMetadata) error {
if len(filesWithMetadata) == 0 {
return fmt.Errorf("no files to add")
}
records := make([]store.ImageRecord, len(filesWithMetadata))
for i, data := range filesWithMetadata {
records[i].UUID = uuid.New().String()
records[i].Filename = path.Join(
a.storageDir, records[i].UUID+path.Ext(data.Filename),
)
records[i].OrigFilename = data.Filename
records[i].Title = data.Title
records[i].Tags = data.Tags
records[i].Timestamp = data.Timestamp
}
// TODO: Prettyprint with colors n stuff.
if !a.goAhead {
for _, record := range records {
fmt.Printf("%s ->\n", record.OrigFilename)
fmt.Printf("\t%s @ %s\n", record.Filename, time.Unix(record.Timestamp, 0).UTC().Format(time.DateTime))
fmt.Printf("\ttitle: %s\n", record.Title)
fmt.Printf("\ttags: %v\n", record.Tags)
}
fmt.Printf(
"The above actions will be commited to the image store. Proceed? (y/N): ",
)
var response rune
fmt.Scanf("%c", &response)
if unicode.ToLower(response) != 'y' {
return fmt.Errorf("aborted by user")
}
}
// TODO: Rollback file copies.
for _, record := range records {
src, err := os.Open(record.OrigFilename)
if err != nil {
return fmt.Errorf("could not open source file: %w", err)
}
dstFilename := path.Join(
a.storageDir, record.UUID+path.Ext(record.OrigFilename),
)
dst, err := os.OpenFile(dstFilename, os.O_WRONLY|os.O_CREATE, 0755)
if err != nil {
return fmt.Errorf("could not open/create destination file: %w", err)
}
written, err := io.Copy(dst, src)
if err != nil {
return fmt.Errorf("could not write to destination: %w", err)
}
fmt.Printf(
"%s->%s, copied %dkb\n",
record.OrigFilename, dstFilename, written/1024,
)
thumbname := strings.TrimSuffix(
dstFilename, filepath.Ext(dstFilename),
) + "_th.jpg"
if err := generateThumbnail(dstFilename, thumbname); err != nil {
fmt.Printf(
"could not generate thumbnail for %s: %s\n", dstFilename, err.Error(),
)
continue
}
fmt.Printf("%s generated\n", thumbname)
previewname := strings.TrimSuffix(
dstFilename, filepath.Ext(dstFilename),
) + "_p.jpg"
if err := generatePreview(dstFilename, previewname); err != nil {
fmt.Printf(
"could not generate preview for %s: %s\n", dstFilename, err.Error(),
)
continue
}
fmt.Printf("%s generated\n", previewname)
}
// TODO: Rollback file copies.
if err := a.store.StoreImageRecords(
context.Background(), records,
); err != nil {
return fmt.Errorf("could not store records: %w", err)
}
fmt.Println("~~~all done~~~")
return nil
}