442 lines
9.5 KiB
Go
442 lines
9.5 KiB
Go
package finstr
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
goimg "image"
|
|
"image/jpeg"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
"unicode"
|
|
|
|
"git.fireandbrimst.one/aw/finstr/crop"
|
|
"git.fireandbrimst.one/aw/finstr/initer"
|
|
"git.fireandbrimst.one/aw/go-resize"
|
|
goutil "git.fireandbrimst.one/aw/goutil/misc"
|
|
)
|
|
|
|
var dir string
|
|
var outdir string
|
|
var t *template.Template
|
|
var suffix string
|
|
var artsy bool
|
|
|
|
type link struct {
|
|
Path string
|
|
Name string
|
|
Space bool
|
|
}
|
|
|
|
type image struct {
|
|
FilenameMedium string
|
|
FilenameSmall string
|
|
}
|
|
|
|
type page struct {
|
|
Title string
|
|
Links []link
|
|
Images []image
|
|
Back link
|
|
Artsy bool
|
|
}
|
|
|
|
func replaceFirst(s string, search string, replace string) string {
|
|
return strings.Replace(s, search, replace, 1)
|
|
}
|
|
|
|
/*-------------------------*/
|
|
|
|
type calendarMonth struct {
|
|
year int
|
|
month int
|
|
}
|
|
|
|
func (m calendarMonth) String() string {
|
|
return fmt.Sprintf("%04d %v", m.year, time.Month(m.month).String())
|
|
}
|
|
|
|
func (m calendarMonth) laterThan(m2 calendarMonth) bool {
|
|
if m.year == m2.year {
|
|
return m.month > m2.month
|
|
} else {
|
|
return m.year > m2.year
|
|
}
|
|
}
|
|
|
|
func byMonthDesc(m []calendarMonth) func(int, int) bool {
|
|
return func(i, j int) bool { return m[i].laterThan(m[j]) }
|
|
}
|
|
|
|
/*-------------------------*/
|
|
|
|
type photo struct {
|
|
filename string
|
|
modified calendarMonth
|
|
tags []string
|
|
}
|
|
|
|
const dirImg = "img"
|
|
|
|
func dirOrig() string {
|
|
return filepath.Join(dir, dirImg)
|
|
}
|
|
|
|
func dirMedium() string {
|
|
return filepath.Join(outdir, "img_m")
|
|
}
|
|
|
|
func dirSmall() string {
|
|
return filepath.Join(outdir, "img_k")
|
|
}
|
|
|
|
func dirPages() string {
|
|
return filepath.Join(outdir, "pages")
|
|
}
|
|
|
|
func (p *photo) srcLocation(folder string) string {
|
|
return replaceFirst(p.filename, dirOrig(), filepath.Base(folder))
|
|
}
|
|
|
|
func (p *photo) filenameMedium() string {
|
|
return p.srcLocation(dirMedium())
|
|
}
|
|
|
|
func (p *photo) filenameSmall() string {
|
|
return p.srcLocation(dirSmall())
|
|
}
|
|
|
|
func (p *photo) img() image {
|
|
img := image{}
|
|
img.FilenameMedium = p.filenameMedium()
|
|
img.FilenameSmall = p.filenameSmall()
|
|
return img
|
|
}
|
|
|
|
func byModified(p []photo) func(int, int) bool {
|
|
return func(i, j int) bool {
|
|
if p[i].modified == p[j].modified {
|
|
return p[i].filename > p[j].filename
|
|
} else {
|
|
return p[i].modified.laterThan(p[j].modified)
|
|
}
|
|
}
|
|
}
|
|
|
|
/*-------------------------*/
|
|
|
|
func isASCII(r rune) bool {
|
|
return r <= 127
|
|
}
|
|
|
|
func convASCII(s string) string {
|
|
out := []rune(s)
|
|
for i, c := range out {
|
|
if (isASCII(c) && (unicode.IsLetter(c) || unicode.IsDigit(c))) || c == '.' {
|
|
// keep unchanged
|
|
} else if unicode.IsSpace(c) || unicode.IsPunct(c) {
|
|
out[i] = '-'
|
|
} else {
|
|
out[i] = 'x'
|
|
}
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
func filename(s string) string {
|
|
return url.QueryEscape(convASCII(s))
|
|
}
|
|
|
|
func filenameDate(date calendarMonth) string {
|
|
return fmt.Sprintf("%04d%02d", date.year, date.month)
|
|
}
|
|
|
|
/*-------------------------*/
|
|
|
|
func (p *page) String() string {
|
|
buf := new(bytes.Buffer)
|
|
p.Artsy = artsy
|
|
err := t.Execute(buf, p)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func (p *page) write(name string) {
|
|
goutil.OptPanic(goutil.WriteFile(filepath.Join(dirPages(), name+suffix), p.String()))
|
|
}
|
|
|
|
func singlePage(arg *pageArg, photos []photo) {
|
|
sort.Slice(photos, byModified(photos))
|
|
img := make([]image, 0)
|
|
for _, p := range photos {
|
|
img = append(img, p.img())
|
|
}
|
|
p := &page{Images: img, Title: arg.title(), Back: arg.back()}
|
|
p.write(arg.filename())
|
|
}
|
|
|
|
/*-------------------------*/
|
|
|
|
type pageArg struct {
|
|
tag string
|
|
date calendarMonth
|
|
}
|
|
|
|
func (arg *pageArg) filename() string {
|
|
if arg.tag != "" {
|
|
return filename(arg.tag)
|
|
} else {
|
|
return filenameDate(arg.date)
|
|
}
|
|
}
|
|
|
|
func (arg *pageArg) title() string {
|
|
if arg.tag != "" {
|
|
return arg.tag
|
|
} else {
|
|
return arg.date.String()
|
|
}
|
|
}
|
|
|
|
func (arg *pageArg) back() link {
|
|
if arg.tag != "" {
|
|
return link{Path: "ByTag.html", Name: "Back"}
|
|
} else {
|
|
return link{Path: "ByDate.html", Name: "Back"}
|
|
}
|
|
}
|
|
|
|
/*-------------------------*/
|
|
|
|
type generator struct {
|
|
photos []photo
|
|
}
|
|
|
|
func (g *generator) byTag() {
|
|
l := make([]link, 0)
|
|
photosWithTag := make(map[string][]photo)
|
|
for _, photo := range g.photos {
|
|
for _, tag := range photo.tags {
|
|
photosWithTag[tag] = append(photosWithTag[tag], photo)
|
|
}
|
|
}
|
|
tags := make([]string, 0)
|
|
for tag := range photosWithTag {
|
|
tags = append(tags, tag)
|
|
}
|
|
sort.Sort(sort.StringSlice(tags))
|
|
for _, tag := range tags {
|
|
photos := photosWithTag[tag]
|
|
l = append(l, link{Path: filename(tag) + ".html", Name: tag})
|
|
args := &pageArg{tag: tag}
|
|
singlePage(args, photos)
|
|
}
|
|
back := link{Path: "index.html", Name: "Back"}
|
|
p := &page{Links: l, Title: "photos by tag", Back: back}
|
|
p.write("ByTag")
|
|
}
|
|
|
|
func keys(m map[calendarMonth][]photo) []calendarMonth {
|
|
keys := make([]calendarMonth, 0)
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func (g *generator) byDate() {
|
|
l := make([]link, 0)
|
|
photosByMonth := make(map[calendarMonth][]photo)
|
|
for _, photo := range g.photos {
|
|
month := photo.modified
|
|
photosByMonth[month] = append(photosByMonth[month], photo)
|
|
}
|
|
months := keys(photosByMonth)
|
|
sort.Slice(months, byMonthDesc(months))
|
|
for _, month := range months {
|
|
photos := photosByMonth[month]
|
|
l = append(l, link{Path: filenameDate(month) + ".html", Name: month.String(), Space: true})
|
|
arg := &pageArg{date: month}
|
|
singlePage(arg, photos)
|
|
}
|
|
back := link{Path: "index.html", Name: "Back"}
|
|
p := page{Links: l, Title: "photos by date", Back: back}
|
|
p.write("ByDate")
|
|
}
|
|
|
|
func (g *generator) index() {
|
|
byDate := link{Path: "ByDate.html", Name: "By Date", Space: true}
|
|
byTag := link{Path: "ByTag.html", Name: "By Tag"}
|
|
l := []link{byDate, byTag}
|
|
back := link{Path: "/", Name: "Home"}
|
|
p := &page{Links: l, Title: "photos", Back: back}
|
|
p.write("index")
|
|
}
|
|
|
|
func (g *generator) generate() {
|
|
photoFiles := goutil.ListFilesExt(dirOrig(), ".jpg")
|
|
tagFiles := goutil.ListFilesExt(dirOrig(), ".tags")
|
|
photos := make(map[string]*photo)
|
|
|
|
for _, photoFile := range photoFiles {
|
|
name := goutil.TrimExt(path.Base(photoFile))
|
|
stat, err := os.Stat(photoFile)
|
|
goutil.OptPanic(err)
|
|
modified := stat.ModTime()
|
|
month := calendarMonth{year: modified.Year(), month: int(modified.Month())}
|
|
photos[name] = &photo{filename: photoFile, modified: month}
|
|
}
|
|
for _, tagFile := range tagFiles {
|
|
name := goutil.TrimExt(path.Base(tagFile))
|
|
tagString, err := goutil.ReadFile(tagFile)
|
|
goutil.OptPanic(err)
|
|
tags := strings.Split(tagString, "\n")
|
|
for _, tag := range tags {
|
|
if _, ok := photos[name]; !ok {
|
|
panic("No matching photo file for " + tagFile)
|
|
}
|
|
if tag = strings.TrimSpace(tag); tag != "" {
|
|
photos[name].tags = append(photos[name].tags, tag)
|
|
}
|
|
}
|
|
}
|
|
for _, v := range photos {
|
|
g.photos = append(g.photos, *v)
|
|
}
|
|
g.byDate()
|
|
g.byTag()
|
|
g.index()
|
|
}
|
|
|
|
func resizeAll(src string, dest string, w uint, h uint) {
|
|
thumbnail := (w == h)
|
|
rsz := func(w uint, h uint, img goimg.Image) goimg.Image { return resize.Resize(w, h, img, resize.Lanczos3) }
|
|
files := goutil.ListFilesExt(src, ".jpg")
|
|
for _, f := range files {
|
|
outfile := filepath.Join(dest, strings.TrimPrefix(f, src))
|
|
if goutil.PathExists(outfile) {
|
|
continue
|
|
}
|
|
fmt.Println("Converting " + f)
|
|
file, err := os.Open(f)
|
|
goutil.OptPanic(err)
|
|
img, err := jpeg.Decode(file)
|
|
goutil.OptPanic(err)
|
|
file.Close()
|
|
var m goimg.Image
|
|
if thumbnail {
|
|
bounds := img.Bounds()
|
|
if bounds.Dx() < bounds.Dy() {
|
|
m = rsz(w, 0, img)
|
|
} else {
|
|
m = rsz(0, h, img)
|
|
}
|
|
if !artsy {
|
|
m, err = crop.CropCenter(m, int(w), int(h))
|
|
goutil.OptPanic(err)
|
|
}
|
|
} else {
|
|
m = rsz(w, h, img)
|
|
}
|
|
out, err := os.Create(outfile)
|
|
defer out.Close()
|
|
goutil.OptPanic(err)
|
|
goutil.OptPanic(jpeg.Encode(out, m, &jpeg.Options{Quality: 95}))
|
|
}
|
|
}
|
|
|
|
func boundSize() {
|
|
fmt.Println("making medium images")
|
|
resizeAll(dirOrig(), dirMedium(), 0, 1080)
|
|
}
|
|
|
|
func makeThumbnails() {
|
|
fmt.Println("making thumbnails")
|
|
var size uint
|
|
if artsy {
|
|
size = 300
|
|
} else {
|
|
size = 200
|
|
}
|
|
resizeAll(dirMedium(), dirSmall(), size, size)
|
|
}
|
|
|
|
type Flags struct {
|
|
InDir string
|
|
OutDir string
|
|
Suffix string
|
|
Clean bool
|
|
}
|
|
|
|
func ensureFolders(clean bool) {
|
|
outfolders := []string{dirMedium(), dirSmall(), dirPages()}
|
|
for _, d := range outfolders {
|
|
if clean {
|
|
goutil.OptPanic(os.RemoveAll(d))
|
|
}
|
|
goutil.OptPanic(os.MkdirAll(d, 0755))
|
|
}
|
|
}
|
|
|
|
func readStyle() {
|
|
style, err := goutil.ReadFile(initer.InitFlag(dir))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if strings.TrimSpace(style) == "artsy" {
|
|
artsy = true
|
|
} else {
|
|
artsy = false
|
|
}
|
|
}
|
|
|
|
func Generate(fl Flags) (e error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
e = errors.New(fmt.Sprint("finstr: ", r))
|
|
}
|
|
}()
|
|
dir = fl.InDir
|
|
if fl.InDir != "" && fl.OutDir == "" {
|
|
outdir = fl.InDir
|
|
} else {
|
|
outdir = fl.OutDir
|
|
}
|
|
suffix = fl.Suffix
|
|
initer.InitializedOrDie(dir)
|
|
ensureFolders(fl.Clean)
|
|
readStyle()
|
|
boundSize()
|
|
makeThumbnails()
|
|
pageTemplate, err := goutil.ReadFile(initer.TemplateFile(dir))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t = template.Must(template.New("site").Parse(pageTemplate))
|
|
gen := &generator{}
|
|
gen.generate()
|
|
return nil
|
|
}
|
|
|
|
func Command() (goutil.CommandFlagsInit, goutil.CommandFunc) {
|
|
f := Flags{}
|
|
flagsInit := func(s *flag.FlagSet) {
|
|
s.StringVar(&f.InDir, "in", "", "use 'in' directory as input instead of current working directory")
|
|
s.StringVar(&f.OutDir, "out", "", "use 'out' directory as output instead of 'in' directory")
|
|
s.StringVar(&f.Suffix, "suffix", ".html", "file extension of generated pages")
|
|
s.BoolVar(&f.Clean, "clean", false, "remove old files in output directory before generating (use with caution)")
|
|
}
|
|
return flagsInit, func([]string) error {
|
|
return Generate(f)
|
|
}
|
|
}
|