diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-09 20:44:58 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-09 20:44:58 +0300 |
| commit | 3e61d09873065f5342efc414ee3ea0d5fdc4c767 (patch) | |
| tree | 7d0ac51cfb41b4774db6292deeb0cc3dce93cf07 /internal/processor/image.go | |
| parent | 51f95f88ca78471a50b3fc62dbcea8edb609dc80 (diff) | |
add snonux static microblog generator
Full Go implementation with:
- txt/md/image/audio input processing, URL auto-linking in .txt files
- Paginated HTML output with Atom feed
- 11 visual themes: neon, terminal, synthwave, minimal, brutalist, paper,
aurora, matrix, ocean, retro, glass (selectable via --theme flag)
- Keyboard navigation (j/k/arrows, Enter modal, h/l page nav)
- Shared nav templates (navhints, navmodal, navscript) across all themes
- Magefile build automation; integration test suite covering all themes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/processor/image.go')
| -rw-r--r-- | internal/processor/image.go | 116 |
1 files changed, 116 insertions, 0 deletions
diff --git a/internal/processor/image.go b/internal/processor/image.go new file mode 100644 index 0000000..9a7d769 --- /dev/null +++ b/internal/processor/image.go @@ -0,0 +1,116 @@ +package processor + +import ( + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + "os" + "path/filepath" + + "golang.org/x/image/draw" +) + +const ( + maxImageWidth = 1024 + jpegQuality = 80 +) + +// processImage reads the source image, resizes it if wider than maxImageWidth, +// encodes it as JPEG at jpegQuality, and writes the result to destDir. +// Returns the output filename (always a .jpg) and an HTML <img> snippet. +func processImage(srcPath, destDir, postID string) (filename, htmlContent string, err error) { + img, err := decodeImage(srcPath) + if err != nil { + return "", "", err + } + + img = resizeIfNeeded(img) + + outName := "image.jpg" + outPath := filepath.Join(destDir, outName) + + if err := writeJPEG(img, outPath); err != nil { + return "", "", err + } + + // The <img> src is relative to the site root, pointing into the posts dir. + src := fmt.Sprintf("posts/%s/%s", postID, outName) + html := fmt.Sprintf(`<img src="%s" alt="" class="post-image">`, src) + + return outName, html, nil +} + +// decodeImage decodes a JPEG, PNG, or GIF (first frame) from srcPath. +func decodeImage(srcPath string) (image.Image, error) { + f, err := os.Open(srcPath) + if err != nil { + return nil, fmt.Errorf("open image %s: %w", srcPath, err) + } + defer f.Close() + + ext := filepath.Ext(srcPath) + switch ext { + case ".jpg", ".jpeg": + img, err := jpeg.Decode(f) + if err != nil { + return nil, fmt.Errorf("decode JPEG %s: %w", srcPath, err) + } + return img, nil + + case ".png": + img, err := png.Decode(f) + if err != nil { + return nil, fmt.Errorf("decode PNG %s: %w", srcPath, err) + } + return img, nil + + case ".gif": + // Use only the first frame of animated GIFs. + g, err := gif.Decode(f) + if err != nil { + return nil, fmt.Errorf("decode GIF %s: %w", srcPath, err) + } + return g, nil + + default: + return nil, fmt.Errorf("unsupported image format: %s", ext) + } +} + +// resizeIfNeeded returns a resized copy of img if its width exceeds maxImageWidth, +// preserving aspect ratio. Otherwise the original is returned unchanged. +func resizeIfNeeded(img image.Image) image.Image { + bounds := img.Bounds() + w := bounds.Dx() + + if w <= maxImageWidth { + return img + } + + h := bounds.Dy() + newW := maxImageWidth + newH := (h * newW) / w + + dst := image.NewRGBA(image.Rect(0, 0, newW, newH)) + draw.BiLinear.Scale(dst, dst.Bounds(), img, bounds, draw.Over, nil) + + return dst +} + +// writeJPEG encodes img as JPEG at the configured quality level and writes to path. +func writeJPEG(img image.Image, path string) error { + f, err := os.Create(path) + if err != nil { + return fmt.Errorf("create JPEG %s: %w", path, err) + } + defer f.Close() + + opts := &jpeg.Options{Quality: jpegQuality} + if err := jpeg.Encode(f, img, opts); err != nil { + return fmt.Errorf("encode JPEG %s: %w", path, err) + } + + return nil +} |
