Skip to content

Commit

Permalink
feat(photos): add blurhash for thumbnails
Browse files Browse the repository at this point in the history
  • Loading branch information
chuhlomin committed Oct 28, 2023
1 parent e9f2514 commit 7826904
Show file tree
Hide file tree
Showing 8 changed files with 1,458 additions and 24 deletions.
18 changes: 18 additions & 0 deletions content/photos.css
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,30 @@ a:hover {
background: var(--color-background-higher);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
position: relative; /* for :before element with blurhash image */
}

.photos .photo.lazy {
background: var(--color-background-higher) !important;
}

.photos .photo::before {
display: block;
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: var(--background-blurhash); /* set by JS per element */
opacity: 0;
transition: opacity 0.33s ease-out;
}

.photos .photo.lazy::before {
opacity: 1;
}

.photos a:focus {
outline: 2px solid var(--color-link);
border-radius: 1px;
Expand Down
1,304 changes: 1,304 additions & 0 deletions content/photos.yml

Large diffs are not rendered by default.

22 changes: 12 additions & 10 deletions generator/photos.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ import (

// Photo is a struct for items in photos.yml file
type Photo struct {
Path string
Width int `yaml:"width,omitempty"`
Height int `yaml:"height,omitempty"`
ThumbPath string `yaml:"thumb,omitempty"`
ThumbXOffset int `yaml:"thumb_x,omitempty"`
ThumbYOffset int `yaml:"thumb_y,omitempty"`
ThumbWidth int `yaml:"thumb_width,omitempty"`
ThumbHeight int `yaml:"thumb_height,omitempty"`
ThumbTotalWidth int `yaml:"thumb_total_width,omitempty"`
ThumbTotalHeight int `yaml:"thumb_total_height,omitempty"`
Path string
Width int `yaml:"width,omitempty"`
Height int `yaml:"height,omitempty"`
ThumbPath string `yaml:"thumb,omitempty"`
ThumbXOffset int `yaml:"thumb_x,omitempty"`
ThumbYOffset int `yaml:"thumb_y,omitempty"`
ThumbWidth int `yaml:"thumb_width,omitempty"`
ThumbHeight int `yaml:"thumb_height,omitempty"`
ThumbTotalWidth int `yaml:"thumb_total_width,omitempty"`
ThumbTotalHeight int `yaml:"thumb_total_height,omitempty"`
Blurhash string `yaml:"blurhash,omitempty"`
BlurhashImageBase64 string `yaml:"blurhash_image_base64,omitempty"`
}

func (g *Generator) processPhotos(fileContent []byte) (interface{}, error) {
Expand Down
8 changes: 8 additions & 0 deletions generator/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ var fm = template.FuncMap{
"ts": func() string { return ts.Format(time.RFC3339) },
"jsonify": jsonify,
"divide": func(a, b int) int { return a / b },
"cleanPhotos": func(photos []Photo) []Photo {
var result []Photo
for _, photo := range photos {
photo.BlurhashImageBase64 = ""
result = append(result, photo)
}
return result
},
}

func config(key string) string {
Expand Down
1 change: 1 addition & 0 deletions photographer/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 // indirect
github.com/aws/smithy-go v1.15.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bbrks/go-blurhash v1.1.1 // indirect
github.com/charmbracelet/lipgloss v0.8.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions photographer/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ github.com/aws/smithy-go v1.15.0 h1:PS/durmlzvAFpQHDs4wi4sNNP9ExsqZh6IlfdHXgKK8=
github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/bbrks/go-blurhash v1.1.1 h1:uoXOxRPDca9zHYabUTwvS4KnY++KKUbwFo+Yxb8ME4M=
github.com/bbrks/go-blurhash v1.1.1/go.mod h1:lkAsdyXp+EhARcUo85yS2G1o+Sh43I2ebF5togC4bAY=
github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdyCQk84RU=
github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU=
github.com/charmbracelet/log v0.2.5 h1:1yVvyKCKVV639RR4LIq1iy1Cs1AKxuNO+Hx2LJtk7Wc=
Expand All @@ -53,6 +55,7 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
Expand Down
122 changes: 110 additions & 12 deletions photographer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@
package main

import (
"bytes"
"context"
"encoding/base64"
"fmt"
"image"
"io"
"os"
"path/filepath"
"sort"
"strconv"
"strings"

"github.com/bbrks/go-blurhash"
"github.com/charmbracelet/log"
flags "github.com/jessevdk/go-flags"
"github.com/nfnt/resize"
Expand All @@ -29,16 +33,18 @@ import (
// Photo struct for items in photos.yml file
// Must be in sync with generator/photos.go
type Photo struct {
Path string
Width int `yaml:"width,omitempty"`
Height int `yaml:"height,omitempty"`
ThumbPath string `yaml:"thumb,omitempty"`
ThumbXOffset int `yaml:"thumb_x,omitempty"`
ThumbYOffset int `yaml:"thumb_y,omitempty"`
ThumbWidth int `yaml:"thumb_width,omitempty"`
ThumbHeight int `yaml:"thumb_height,omitempty"`
ThumbTotalWidth int `yaml:"thumb_total_width,omitempty"`
ThumbTotalHeight int `yaml:"thumb_total_height,omitempty"`
Path string
Width int `yaml:"width,omitempty"`
Height int `yaml:"height,omitempty"`
ThumbPath string `yaml:"thumb,omitempty"`
ThumbXOffset int `yaml:"thumb_x,omitempty"`
ThumbYOffset int `yaml:"thumb_y,omitempty"`
ThumbWidth int `yaml:"thumb_width,omitempty"`
ThumbHeight int `yaml:"thumb_height,omitempty"`
ThumbTotalWidth int `yaml:"thumb_total_width,omitempty"`
ThumbTotalHeight int `yaml:"thumb_total_height,omitempty"`
Blurhash string `yaml:"blurhash,omitempty"`
BlurhashImageBase64 string `yaml:"blurhash_image_base64,omitempty"`

// Temporary image.Image field used to generate thumbnails
image image.Image `yaml:"-"`
Expand Down Expand Up @@ -71,7 +77,12 @@ type appConfig struct {
R2AccessKeySecret string `env:"R2_ACCESS_KEY_SECRET" long:"r2-access-key-secret" description:"r2 access key secret"`
R2Bucket string `env:"R2_BUCKET" long:"r2-bucket" description:"r2 bucket"`

Force bool `long:"force" description:"force thumbnail generation"`
// Force thumbnail generation
ForceThumbnails bool `long:"force-thumbnails" description:"force thumbnail generation"`

// Blurhash
ForceBlurhash bool `long:"force-blurhash" description:"force blurhash generation"`
ForceBlurhashImages bool `long:"force-blurhash-images" description:"force blurhash images generation"`
}

func main() {
Expand Down Expand Up @@ -123,11 +134,21 @@ func run() error {
return fmt.Errorf("error uploading new photos: %v", err)
}

photos, err = generateThumbnails(ctx, r2, photos, dir, cfg.Force)
photos, err = generateThumbnails(ctx, r2, photos, dir, cfg.ForceThumbnails)
if err != nil {
return fmt.Errorf("error generating thumbnails: %v", err)
}

photos, err = generateBlurhashes(photos, dir, cfg.ForceBlurhash)
if err != nil {
return fmt.Errorf("error generating blurhashes: %v", err)
}

photos, err = generateBlurhashImages(photos, cfg.ForceBlurhashImages)
if err != nil {
return fmt.Errorf("error generating blurhash images: %v", err)
}

// save photos.yml file
if err = savePhotosFile(cfg.YamlFile, photos); err != nil {
return fmt.Errorf("error saving photos: %v", err)
Expand Down Expand Up @@ -488,3 +509,80 @@ func readImage(dir string, path string) (image.Image, error) {

return img, nil
}

func generateBlurhashes(photos []*Photo, dir string, force bool) ([]*Photo, error) {
var err error
for _, photo := range photos {
if photo.Blurhash != "" && !force {
continue
}

log.Infof("Generating blurhash for %s", photo.Path)
photo.Blurhash, err = generateBlurhash(photo.Path, dir)
if err != nil {
return nil, fmt.Errorf("error generating blurhash: %v", err)
}
}

return photos, nil
}

func generateBlurhash(path, dir string) (string, error) {
file, err := os.Open(filepath.Join(dir, path))
if err != nil {
return "", fmt.Errorf("error opening file: %v", err)
}
defer file.Close()

return generateBlurhashForReader(file)
}

func generateBlurhashForReader(reader io.Reader) (string, error) {
m, _, err := image.Decode(reader)
if err != nil {
return "", err
}

return blurhash.Encode(4, 4, m)
}

func generateBlurhashImages(photos []*Photo, force bool) ([]*Photo, error) {
var err error
for _, photo := range photos {
if photo.Blurhash == "" {
continue
}

if photo.BlurhashImageBase64 != "" && !force {
continue
}

log.Infof("Generating blurhash image for %s", photo.Path)
photo.BlurhashImageBase64, err = generateBlurhashImage(photo)
if err != nil {
return nil, fmt.Errorf("error generating blurhash image: %v", err)
}
}

return photos, nil
}

func generateBlurhashImage(photo *Photo) (string, error) {
m, err := blurhash.Decode(
photo.Blurhash,
photo.ThumbWidth/2,
photo.ThumbHeight/2,
1,
)
if err != nil {
return "", fmt.Errorf("error decoding blurhash: %v", err)
}

buf := new(bytes.Buffer)
if err := jpeg.Encode(buf, m, &jpeg.Options{Quality: 90}); err != nil {
return "", fmt.Errorf("error encoding blurhash image: %v", err)
}

b64 := base64.StdEncoding.EncodeToString(buf.Bytes())
return b64, nil
}
4 changes: 2 additions & 2 deletions templates/photos.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

<div class="photos" id="photos">
{{ range . }}
<div class="container"><a href="#{{ .Path }}" class="photo lazy" style="background: url('{{ config "PhotosDomain" }}{{ .ThumbPath }}?ts={{ ts }}') no-repeat center center; background-position: -{{ divide .ThumbXOffset 2 }}px -{{ divide .ThumbYOffset 2 }}px; background-size: {{ divide .ThumbTotalWidth 2 }}px {{ divide .ThumbTotalHeight 2 }}px; aspect-ratio: {{ .ThumbWidth }} / {{ .ThumbHeight }};" draggable="true"></a></div>
<div class="container"><a href="#{{ .Path }}" class="photo lazy" style="background: url('{{ config "PhotosDomain" }}{{ .ThumbPath }}?ts={{ ts }}') no-repeat center center; background-position: -{{ divide .ThumbXOffset 2 }}px -{{ divide .ThumbYOffset 2 }}px; background-size: {{ divide .ThumbTotalWidth 2 }}px {{ divide .ThumbTotalHeight 2 }}px; aspect-ratio: {{ .ThumbWidth }} / {{ .ThumbHeight }}; {{ with .BlurhashImageBase64 }}--background-blurhash: url('data:image/jpeg;base64,{{ . }}');{{ end }}" draggable="true"></a></div>
{{- end }}

</div>
Expand All @@ -42,7 +42,7 @@

<script>
var showingViewer = false;
var photos = {{ . | jsonify }};
var photos = {{ . | cleanPhotos | jsonify }};

var view = function(path) {
if (path === "close") {
Expand Down

0 comments on commit 7826904

Please sign in to comment.