Skip to content

Commit

Permalink
feat(compiler): 🌐 Add web compilation option
Browse files Browse the repository at this point in the history
Adds the `--web` format option to `compile`, which creates a webp and yaml file that can be displayed with only CSS. Also adds the functionality for `--here`, `--there` and `--outdir`.
  • Loading branch information
jphastings committed Dec 15, 2023
1 parent c0201b8 commit 5b00458
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 53 deletions.
31 changes: 25 additions & 6 deletions cmd/postcards/cmd/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,61 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/dotpostcard/postcards-go/compile"
"github.com/dotpostcard/postcards-go/internal/cmdhelp"
"github.com/spf13/cobra"
)

// compileCmd represents the compile command
var compileCmd = &cobra.Command{
Use: "compile",
Short: "Compiles images & metadata into a postcard file",
Short: "Compiles images & metadata into a postcard file, or web-compatible equivalent",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
path, err := filepath.Abs(args[0])
if err != nil {
return fmt.Errorf("unknown file path: %w", err)
}

outdir, err := cmdhelp.Outdir(cmd, path)
if err != nil {
return err
}

override, err := cmd.Flags().GetBool("override")
if err != nil {
override = false
}

filename, data, err := compile.Files(path, !override)
webFormat, err := cmd.Flags().GetBool("web")
if err != nil {
webFormat = false
}

filenames, datas, err := compile.Files(path, !override, webFormat)
if err != nil {
return err
}

if data == nil {
fmt.Printf("Postcard already exists, skipping: %s\n", filename)
if datas == nil {
fmt.Printf("Postcard files already exist, skipping: %s\n", strings.Join(filenames, ", "))
}

fmt.Printf("Writing postcard file to %s\n", filename)
return os.WriteFile(filename, data, 0600)
fmt.Println("Writing postcard files…")
for i, filename := range filenames {
if err := os.WriteFile(filepath.Join(outdir, filename), datas[i], 0600); err != nil {
return fmt.Errorf("unable to write file %s: %w", filename, err)
}
fmt.Printf("↪ Wrote postcard file to %s\n", filename)
}
return nil
},
}

func init() {
compileCmd.Flags().Bool("override", false, "overrides output files")
compileCmd.Flags().Bool("web", false, "make web-compatible postcard file")
rootCmd.AddCommand(compileCmd)
}
13 changes: 9 additions & 4 deletions cmd/postcards/cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"fmt"
"os"

"github.com/dotpostcard/postcards-go"
Expand All @@ -14,14 +15,18 @@ var rootCmd = &cobra.Command{
Version: postcards.Version.String(),
}

func checkErr(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

func Execute() {
rootCmd.PersistentFlags().Bool("here", false, "Output files in the current working directory")
rootCmd.PersistentFlags().Bool("there", true, "Output files in the same directory as the source data")
rootCmd.PersistentFlags().String("outdir", "", "Output files to the given directory")
rootCmd.MarkFlagsMutuallyExclusive("here", "there", "outdir")

err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
checkErr(rootCmd.Execute())
}
105 changes: 67 additions & 38 deletions compile/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,103 +23,132 @@ import (
var nameRegex = regexp.MustCompile(`(.+)-(?:front|back|meta)+\.[a-z]+`)

// Files accepts a path to one of the three needed files, attempts to find the others, and provides the conventional name and bytes for the file.
func Files(part string, skipIfPresent bool) (string, []byte, error) {
func Files(part string, skipIfPresent bool, webFormat bool) ([]string, [][]byte, error) {
dir := filepath.Dir(part)
parts := nameRegex.FindStringSubmatch(filepath.Base(part))
if len(parts) != 2 {
return "", nil, fmt.Errorf("given filename not of the form *-{front,back,meta}.ext")
return nil, nil, fmt.Errorf("given filename not of the form *-{front,back,meta}.ext")
}
prefix := parts[1]
outputFilename := fmt.Sprintf("%s.postcard", prefix)
var outputFilenames []string
if webFormat {
outputFilenames = []string{
fmt.Sprintf("%s.webp", prefix),
fmt.Sprintf("%s.md", prefix),
}
} else {
outputFilenames = []string{fmt.Sprintf("%s.postcard", prefix)}
}

exists, err := fileExists(outputFilename)
exists, err := anyFilesExist(outputFilenames...)
if err != nil {
return outputFilename, nil, nil
return outputFilenames, nil, nil
}
if skipIfPresent && exists {
return outputFilename, nil, fmt.Errorf("output file already exists: %s", outputFilename)
return outputFilenames, nil, fmt.Errorf("output file already exists: %s", strings.Join(outputFilenames, ", "))
}

metaRaw, metaExt, err := openVagueFilename(dir, prefix, "meta", "json", "yml", "yaml")
if err != nil {
return "", nil, fmt.Errorf("couldn't load metadata: %w", err)
return nil, nil, fmt.Errorf("couldn't load metadata: %w", err)
}
meta, err := metaReader(metaRaw, metaExt)
if err != nil {
return "", nil, fmt.Errorf("couldn't parse metadata: %w", err)
return nil, nil, fmt.Errorf("couldn't parse metadata: %w", err)
}

front, _, err := openVagueFilename(dir, prefix, "front", "png", "jpg", "tif", "tiff")
if err != nil {
return "", nil, fmt.Errorf("couldn't load postcard front: %w", err)
return nil, nil, fmt.Errorf("couldn't load postcard front: %w", err)
}
back, _, err := openVagueFilename(dir, prefix, "back", "png", "jpg", "tif", "tiff")
if err != nil {
return "", nil, fmt.Errorf("couldn't load postcard back: %w", err)
return nil, nil, fmt.Errorf("couldn't load postcard back: %w", err)
}

pc, err := Readers(front, back, meta)
if err != nil {
return "", nil, err
}
if webFormat {
img, md, err := CompileWeb(front, back, meta)
if err != nil {
return nil, nil, err
}

buf := new(bytes.Buffer)
if err := postcards.Write(pc, buf); err != nil {
return "", nil, err
}
return outputFilenames, [][]byte{img, md}, nil
} else {
pc, err := Readers(front, back, meta)
if err != nil {
return nil, nil, err
}

return outputFilename, buf.Bytes(), nil
}
buf := new(bytes.Buffer)
if err := postcards.Write(pc, buf); err != nil {
return nil, nil, err
}

func fileExists(filename string) (bool, error) {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, err
return outputFilenames, [][]byte{buf.Bytes()}, nil
}
if info.IsDir() {
return true, fmt.Errorf("file %s is a directory", filename)
}

func anyFilesExist(filenames ...string) (bool, error) {
for _, filename := range filenames {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
continue
} else if err != nil {
return false, err
}
if info.IsDir() {
return true, fmt.Errorf("file %s is a directory", filename)
}
return true, nil
}
return true, nil
return false, nil
}

// Readers accepts reader objects for each of the components of a postcard file, and creates an in-memory Postcard object.
func Readers(frontReader, backReader io.Reader, mp MetadataProvider) (*types.Postcard, error) {
func processImages(frontReader, backReader io.Reader, mp MetadataProvider) (image.Image, image.Image, types.Size, types.Size, types.Metadata, error) {
meta, err := mp.Metadata()
if err != nil {
return nil, fmt.Errorf("unable to obtain the metadata: %w", err)
return nil, nil, types.Size{}, types.Size{}, types.Metadata{}, fmt.Errorf("unable to obtain the metadata: %w", err)
}

if err := validateMetadata(meta); err != nil {
return nil, fmt.Errorf("metadata invalid: %w", err)
return nil, nil, types.Size{}, types.Size{}, types.Metadata{}, fmt.Errorf("metadata invalid: %w", err)
}

frontRaw, frontDims, err := readerToImage(frontReader)
if err != nil {
return nil, fmt.Errorf("unable to parse image for front image: %w", err)
return nil, nil, types.Size{}, types.Size{}, types.Metadata{}, fmt.Errorf("unable to parse image for front image: %w", err)
}
backRaw, backDims, err := readerToImage(backReader)
if err != nil {
return nil, fmt.Errorf("unable to parse image for back image: %w", err)
return nil, nil, types.Size{}, types.Size{}, types.Metadata{}, fmt.Errorf("unable to parse image for back image: %w", err)
}

meta.FrontDimensions = bestFrontDimensions(meta.FrontDimensions, frontDims, backDims, meta.Flip.Heteroriented())

if err := validate.Dimensions(&meta, frontRaw.Bounds(), backRaw.Bounds(), frontDims, backDims); err != nil {
return nil, err
return nil, nil, types.Size{}, types.Size{}, types.Metadata{}, err
}
if isOversized(frontDims) {
log.Printf("WARNING! This postcard is very large (%s), do the images have the correct ppi/ppcm?\n", frontDims)
}

frontImg, err := hideSecrets(frontRaw, meta.Front.Secrets)
if err != nil {
return nil, fmt.Errorf("unable to hide the secret areas specified on the postcard front: %w", err)
return nil, nil, types.Size{}, types.Size{}, types.Metadata{}, fmt.Errorf("unable to hide the secret areas specified on the postcard front: %w", err)
}
backImg, err := hideSecrets(backRaw, meta.Back.Secrets)
if err != nil {
return nil, fmt.Errorf("unable to hide the secret areas specified on the postcard back: %w", err)
return nil, nil, types.Size{}, types.Size{}, types.Metadata{}, fmt.Errorf("unable to hide the secret areas specified on the postcard back: %w", err)
}

return frontImg, backImg, frontDims, backDims, meta, nil
}

// Readers accepts reader objects for each of the components of a postcard file, and creates an in-memory Postcard object.
func Readers(frontReader, backReader io.Reader, mp MetadataProvider) (*types.Postcard, error) {
frontImg, backImg, frontDims, backDims, meta, err := processImages(frontReader, backReader, mp)
if err != nil {
return nil, err
}

frontWebp, err := encodeWebp(frontImg, frontDims)
Expand Down
22 changes: 18 additions & 4 deletions compile/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,28 @@ func hashOfPostcardInnards(data []byte) [16]byte {
return md5.Sum(data)
}

func ExampleFiles() {
filename, data, err := compile.Files("../fixtures/hello-meta.yaml", false)
func ExampleFiles_postcard() {
filenames, datas, err := compile.Files("../fixtures/hello-meta.yaml", false, false)
if err != nil {
panic(err)
}

fmt.Printf("%s has checksum %x", filename, hashOfPostcardInnards(data))
// Output: hello.postcard has checksum ecb741d69f14bd70aaa3f02436e5ea49
fmt.Printf("(%d file) %s has checksum %x", len(filenames), filenames[0], hashOfPostcardInnards(datas[0]))
// Output: (1 file) hello.postcard has checksum 659a65db02a600f70d3bf0b438d10297
}

func ExampleFiles_web() {
filenames, datas, err := compile.Files("../fixtures/hello-meta.yaml", false, true)
if err != nil {
panic(err)
}

fmt.Printf("(%d files) %s has checksum %x, %s has checksum %x",
len(filenames),
filenames[0], hashOfPostcardInnards(datas[0]),
filenames[1], hashOfPostcardInnards(datas[1]),
)
// Output: (2 files) hello.webp has checksum c085551191a9bf20e65392ed239d990e, hello.md has checksum 47ab2ccda9585222cc0625f5298e12c7
}

func checkBadSetup(t *testing.T, err error) {
Expand Down
81 changes: 81 additions & 0 deletions compile/web.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package compile

import (
"bytes"
_ "embed"
"image"
"image/draw"
"io"
"math/big"
"text/template"

"github.com/dotpostcard/postcards-go/internal/types"
)

//go:embed web.md.tmpl
var webTemplateStr string
var webTemplate = template.Must(template.New("web").Parse(webTemplateStr))

func CompileWeb(front, back io.Reader, mp MetadataProvider) (imgBytes []byte, mdBytes []byte, err error) {
frontImg, backImg, frontDims, _, meta, err := processImages(front, back, mp)
if err != nil {
return nil, nil, err
}

combinedDims := types.Size{
PxWidth: frontDims.PxWidth,
PxHeight: frontDims.PxHeight * 2,
CmWidth: frontDims.CmWidth,
CmHeight: frontDims.CmHeight.Mul(frontDims.CmHeight, big.NewRat(2, 1)),
}

frontBounds := frontImg.Bounds()
backBounds := image.Rectangle{
Min: image.Point{0, frontDims.PxHeight},
Max: image.Point{frontDims.PxWidth, combinedDims.PxHeight},
}

combinedImg := image.NewRGBA(image.Rect(0, 0, combinedDims.PxWidth, combinedDims.PxHeight))
draw.Draw(combinedImg, frontBounds, frontImg, image.Point{}, draw.Src)

if meta.Flip == types.FlipLeftHand || meta.Flip == types.FlipRightHand {
backImg = rotateImage(backImg, meta.Flip)
}
draw.Draw(combinedImg, backBounds, backImg, image.Point{}, draw.Src)

combined, err := encodeWebp(combinedImg, combinedDims)
if err != nil {
return nil, nil, err
}

buf := new(bytes.Buffer)
if err := webTemplate.Execute(buf, meta); err != nil {
return nil, nil, err
}

return combined, buf.Bytes(), nil
}

func rotateImage(img image.Image, flip types.Flip) image.Image {
bounds := img.Bounds()
rotated := image.NewRGBA(image.Rect(0, 0, bounds.Dy(), bounds.Dx()))

switch flip {
case types.FlipLeftHand:
// Top left of the source should be bottom left of the output
for x := bounds.Min.X; x < bounds.Max.X; x++ {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
rotated.Set(y, bounds.Max.X-x, img.At(x, y))
}
}
case types.FlipRightHand:
// Top left of the source should be top right of the output
for x := bounds.Min.X; x < bounds.Max.X; x++ {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
rotated.Set(bounds.Max.Y-y, x, img.At(x, y))
}
}
}

return rotated
}
9 changes: 9 additions & 0 deletions compile/web.md.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: {{ .Location.Name }}
lat: {{ .Location.Latitude }}
long: {{ .Location.Longitude }}
date: {{ .SentOn }}
frontalt: {{ .Front.Description }}
---

{{ .Back.Transcription }}

0 comments on commit 5b00458

Please sign in to comment.