qsl designer

This commit is contained in:
2026-06-11 21:54:35 +02:00
parent 6150498a9e
commit 408b29896c
252 changed files with 13989 additions and 277 deletions
+122
View File
@@ -0,0 +1,122 @@
package qslcard
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// TemplateDir returns the asset folder of a stored template:
// <baseDir>/templates/<id>. baseDir is <dataDir>/qsl.
func TemplateDir(baseDir string, id int64) string {
return filepath.Join(baseDir, "templates", fmt.Sprintf("%d", id))
}
// IsDraftPhoto reports whether a photo reference still points at the user's
// original file (absolute path) rather than a name inside the template dir.
func IsDraftPhoto(ref string) bool { return filepath.IsAbs(ref) }
// ImportPhotos copies every absolute-path photo the template references into
// dir and rewrites the references to the copied names (img_<hash8>.<ext>).
// Users move files around — a saved template must never depend on the
// original location. Already-relative references are left untouched.
func ImportPhotos(t *Template, dir string) error {
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("create template dir: %w", err)
}
imported := map[string]string{} // original path → copied name
importOne := func(ref string) (string, error) {
if !IsDraftPhoto(ref) {
return ref, nil
}
if name, ok := imported[ref]; ok {
return name, nil
}
name, err := copyPhoto(ref, dir)
if err != nil {
return "", err
}
imported[ref] = name
return name, nil
}
name, err := importOne(t.Hero.Photo)
if err != nil {
return fmt.Errorf("import hero photo: %w", err)
}
t.Hero.Photo = name
for i := range t.Elements {
if t.Elements[i].Type != ElemInsert {
continue
}
name, err := importOne(t.Elements[i].Photo)
if err != nil {
return fmt.Errorf("import insert photo: %w", err)
}
t.Elements[i].Photo = name
}
return nil
}
// copyPhoto copies src into dir under a content-derived name so identical
// photos dedupe and re-saving is idempotent.
func copyPhoto(src, dir string) (string, error) {
in, err := os.Open(src)
if err != nil {
return "", fmt.Errorf("open photo %s: %w", src, err)
}
defer in.Close()
h := sha256.New()
if _, err := io.Copy(h, in); err != nil {
return "", fmt.Errorf("hash photo %s: %w", src, err)
}
ext := strings.ToLower(filepath.Ext(src))
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" {
return "", fmt.Errorf("unsupported photo type %q (jpeg/png only)", ext)
}
name := "img_" + hex.EncodeToString(h.Sum(nil))[:8] + ext
dst := filepath.Join(dir, name)
if _, err := os.Stat(dst); err == nil {
return name, nil // identical content already imported
}
if _, err := in.Seek(0, io.SeekStart); err != nil {
return "", fmt.Errorf("rewind photo %s: %w", src, err)
}
out, err := os.Create(dst)
if err != nil {
return "", fmt.Errorf("create %s: %w", dst, err)
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return "", fmt.Errorf("copy photo to %s: %w", dst, err)
}
if err := out.Close(); err != nil {
return "", fmt.Errorf("flush %s: %w", dst, err)
}
return name, nil
}
// PhotoExistsIn returns a Validate callback that checks photo references
// against a template asset dir (relative names) or the filesystem (drafts).
func PhotoExistsIn(dir string) func(string) bool {
return func(ref string) bool {
p := ref
if !IsDraftPhoto(ref) {
p = filepath.Join(dir, ref)
}
_, err := os.Stat(p)
return err == nil
}
}
// RemoveTemplateDir deletes a template's asset folder (photos + preview).
func RemoveTemplateDir(baseDir string, id int64) error {
dir := TemplateDir(baseDir, id)
if err := os.RemoveAll(dir); err != nil {
return fmt.Errorf("remove template dir %s: %w", dir, err)
}
return nil
}