purge history since May 2016

This commit is contained in:
gutmet 2019-01-01 19:31:14 +01:00
commit a5bf332911
8 changed files with 674 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
finstr
.gitdist

18
License Normal file
View File

@ -0,0 +1,18 @@
finstr: A bullshit-free image gallery generator.
Copyright (C) 2016 Alexander Weinhold
Unless explicitly stated otherwise, the following applies:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

29
Readme.md Normal file
View File

@ -0,0 +1,29 @@
finstr
======
finstr is a bullshit-free image gallery generator. It produces pages according to a template and groups images by tag or month. Releases can be found at [releases.gutmet.org](https://releases.gutmet.org).
build
=======
If you want to build the executable yourself, you need go1.11+ with module support. Checkout the repository and run 'go build cmd/finstr.go'.
quick start
=====
Put the executable somewhere in your path, create a folder for your gallery, then
```
> cd YOUR_FOLDER
> finstr init
```
Put some .jpgs inside the img folder. Tag them by creating files with the same name but .tags suffix. Each line is a tag, then
```
> finstr gen
> cd pages
> firefox index.html
```
finstr has also been integrated into the static site generator [wombat](/wombat/about).

21
cmd/finstr.go Normal file
View File

@ -0,0 +1,21 @@
package main
import (
"fmt"
"git.gutmet.org/finstr.git"
"git.gutmet.org/finstr.git/initer"
"git.gutmet.org/goutil.git"
"os"
)
func main() {
commands := []goutil.Command{
goutil.NewCommandWithFlags("init", initer.Command, "initialize finstr directory"),
goutil.NewCommandWithFlags("gen", finstr.Command, "generate gallery"),
}
err := goutil.Execute(commands)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

40
crop/crop.go Normal file
View File

@ -0,0 +1,40 @@
package crop
import (
"errors"
"git.gutmet.org/goutil.git"
"image"
)
func boundInt(i, min, max int) int {
return goutil.IntMin(goutil.IntMax(i, min), max)
}
func bound(p image.Point, bounds image.Rectangle) image.Point {
min := bounds.Min
max := bounds.Max
return image.Point{X: boundInt(p.X, min.X, max.X), Y: boundInt(p.Y, min.Y, max.Y)}
}
type subImageSupport interface {
SubImage(rec image.Rectangle) image.Image
}
func Crop(img image.Image, topleft image.Point, w int, h int) (image.Image, error) {
bounds := img.Bounds()
topleft = bound(topleft, bounds)
bottomright := bound(image.Point{X: topleft.X + w, Y: topleft.Y + h}, bounds)
croparea := image.Rect(topleft.X, topleft.Y, bottomright.X, bottomright.Y)
if timg, ok := img.(subImageSupport); ok {
return timg.SubImage(croparea), nil
} else {
return nil, errors.New("Crop: image format does not support 'SubImage' method (cropping not supported)")
}
}
func CropCenter(img image.Image, w int, h int) (image.Image, error) {
bounds := img.Bounds()
center := image.Point{X: bounds.Min.X + bounds.Dx()/2, Y: bounds.Min.Y + bounds.Dy()/2}
topleft := image.Point{X: center.X - w/2, Y: center.Y - h/2}
return Crop(img, topleft, w, h)
}

426
finstr.go Normal file
View File

@ -0,0 +1,426 @@
package finstr
import (
"bytes"
"errors"
"flag"
"fmt"
"git.gutmet.org/finstr.git/crop"
"git.gutmet.org/finstr.git/initer"
"git.gutmet.org/go-resize.git"
"git.gutmet.org/goutil.git"
goimg "image"
"image/jpeg"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strings"
"text/template"
"time"
)
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 filename(s string) string {
return url.QueryEscape(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) {
fmt.Println(f + " already converted")
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("medium images")
fmt.Println("-------------")
resizeAll(dirOrig(), dirMedium(), 0, 1080)
fmt.Println()
}
func makeThumbnails() {
fmt.Println("thumbnails")
fmt.Println("----------")
var size uint
if artsy {
size = 300
} else {
size = 200
}
resizeAll(dirMedium(), dirSmall(), size, size)
fmt.Println()
}
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 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)
}
}

6
go.mod Normal file
View File

@ -0,0 +1,6 @@
module git.gutmet.org/finstr.git
require (
git.gutmet.org/go-resize.git v0.0.0-20180221191011-83c6a9932646
git.gutmet.org/goutil.git v0.0.0-20181222191234-18f9120c8c1e
)

131
initer/initer.go Normal file
View File

@ -0,0 +1,131 @@
package initer
import (
"flag"
"git.gutmet.org/goutil.git"
"os"
"path/filepath"
)
const htmlTemplate = `<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: Open Sans,Clear Sans,Verdana,Helvetica,Arial;
font-size : 12pt;
max-width: 850px;
margin-left: auto;
margin-right: auto;
background-color : black;
color: #E8E8E8;
text-align:justify;
}
a {
color : #0D0;
}
a:hover {
color : red;
}
img {
max-width: 100%;
}
div.gallery {
text-align: left;
}
h1 {
text-align: left;
}
</style>
</head>
<body>
<h1>{{.Title}}</h1>
<div class="gallery">
{{if .Links}}{{range $index, $link := .Links}}<a href="{{$link.Path}}">{{$link.Name}}</a><br>{{if $link.Space}}<br>{{end}}{{end}}{{end}}
{{if .Images}}{{range $index, $img := .Images}}<a href="../{{$img.FilenameMedium}}"><img src="../{{$img.FilenameSmall}}" /></a>{{if $.Artsy}}<br><br>{{end}}{{end}}{{end}}
</div>
<br><br>
<a href="{{.Back.Path}}">{{.Back.Name}}</a>
</body>
</html>
`
const mdTemplate = `---
title: {{.Title}}
---
{{if .Links}}{{range $index, $link := .Links}}<a href="{{$link.Path}}">{{$link.Name}}</a><br>{{if $link.Space}}<br>{{end}}{{end}}{{end}}
{{if .Images}}{{range $index, $img := .Images}}<a href="../{{$img.FilenameMedium}}"><img src="../{{$img.FilenameSmall}}" /></a>{{if $.Artsy}}<br><br>{{end}}{{end}}{{end}}
<br><br>
<a href="{{.Back.Path}}">{{.Back.Name}}</a>
`
type IniterFlags struct {
Dir string
Markdown bool
Artsy bool
}
func InitializedOrDie(dir string) {
paths := []string{imgFolder(dir), InitFlag(dir), TemplateFile(dir)}
for _, p := range paths {
if !goutil.PathExists(p) {
panic("Not a finstr folder? Did not find " + p)
}
}
}
func imgFolder(dir string) string {
return filepath.Join(dir, "img")
}
func InitFlag(dir string) string {
return filepath.Join(dir, ".finstr")
}
func readme(dir string) string {
return filepath.Join(imgFolder(dir), "Readme")
}
func TemplateFile(dir string) string {
return filepath.Join(dir, "template")
}
func Init(fl IniterFlags) error {
dir := fl.Dir
err := os.MkdirAll(imgFolder(dir), 0755)
var style string
if fl.Artsy {
style = "artsy"
} else {
style = "square"
}
var template string
if fl.Markdown {
template = mdTemplate
} else {
template = htmlTemplate
}
err = goutil.OptDo(err, func() error { return goutil.WriteFile(InitFlag(dir), style) })
err = goutil.OptDo(err, func() error { return goutil.WriteFile(readme(dir), "Put your .jpgs and .tags here") })
err = goutil.OptDo(err, func() error { return goutil.WriteFile(TemplateFile(dir), template) })
return err
}
func Command() (goutil.CommandFlagsInit, goutil.CommandFunc) {
f := IniterFlags{}
flagsInit := func(s *flag.FlagSet) {
s.BoolVar(&f.Markdown, "markdown", false, "init with Markdown template instead of HTML")
s.StringVar(&f.Dir, "dir", "", "initialize 'dir' instead of current working directory")
s.BoolVar(&f.Artsy, "artsy", false, "mark gallery to generate thumbnails with original ratio instead of square")
}
return flagsInit, func([]string) error {
return Init(f)
}
}