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) } }