A tool for archiving & converting scans of postcards, and information about them.

feat: :construction: Support WASM as compilation target

Moves ccgo & unsupported features from other packages out into files with buildtags, and replacements in neighbouring files. Starts providing WASIp1 binaries with Goreleaser.

+215 -85
+3
.goreleaser.yaml
··· 14 14 15 15 builds: 16 16 - main: ./cmd/postcards 17 + id: postcards 17 18 binary: postcards 18 19 env: 19 20 - CGO_ENABLED=0 ··· 21 22 - linux 22 23 - windows 23 24 - darwin 25 + - wasip1 24 26 goarch: 25 27 - amd64 26 28 - arm64 29 + - wasm 27 30 ldflags: 28 31 - -s -w -X "main.Date={{.CommitDate}}" 29 32 universal_binaries:
+7 -3
README.md
··· 8 8 ## Install 9 9 10 10 ```sh 11 - $ go install github.com/jphastings/dotpostcard/cmd/postcards@latest 11 + go install github.com/jphastings/dotpostcard/cmd/postcards@latest 12 12 ``` 13 13 14 - Or, if you have [Homebrew](https://brew.sh) installed: 14 + If you have [Homebrew](https://brew.sh) installed: 15 15 16 16 ```sh 17 - $ brew install jphastings/tools/postcards 17 + brew install jphastings/tools/postcards 18 18 ``` 19 + 20 + You can also download compiled binaries from [Github releases](https://github.com/jphastings/dotpostcard/releases/) for Windows, Linux, macOS — each of which is provided for arm64 and amd64 architectures. 21 + 22 + WASIp1 compatible WASM executables are also provided with a reduced feature set (notably more efficient image encoders, WebP and JPEGli, are absent). 19 23 20 24 ## Usage 21 25
+1 -1
TODO.md
··· 38 38 - [x] Decode USD & USDZ #usd 39 39 - [x] Creating a USD(Z) from an image that doesn't have resolution data (eg front/back portrait fixtures) seems to nil pointer fail. #bug 40 40 - [x] Get this CLI tool building automatically 41 - - [x] Web (webp) with transparency not convertable (eg. to USD) #bug 41 + - [x] Web (webp) with transparency not convertible (eg. to USD) #bug
+3
cmd/postcards/format.go
··· 1 + //go:build !wasm 2 + // +build !wasm 3 + 1 4 package main 2 5 3 6 import (
+2 -2
formats/html/postcard.html.tmpl
··· 3 3 4 4 <input type="checkbox" id="postcard-{{.Name}}"> 5 5 <label for="postcard-{{.Name}}"> 6 - <div class="postcard flip-{{ .Flip }} {{ if gt .Physical.FrontDimensions.PxHeight .Physical.FrontDimensions.PxWidth }}portrait{{ else }}landscape{{ end }}" style="--postcard: url('{{ .Name }}.postcard.jpg'); --aspect-ratio: {{ .Physical.FrontDimensions.PxWidth }} / {{ .Physical.FrontDimensions.PxHeight }}"> 7 - <img src="{{ .Name }}.postcard.jpg" loading="lazy" alt="{{ .Front.Description }}" width="500px"> 6 + <div class="postcard flip-{{ .Flip }} {{ if gt .Physical.FrontDimensions.PxHeight .Physical.FrontDimensions.PxWidth }}portrait{{ else }}landscape{{ end }}" style="--postcard: url('{{ .Name }}.postcard.jpeg'); --aspect-ratio: {{ .Physical.FrontDimensions.PxWidth }} / {{ .Physical.FrontDimensions.PxHeight }}"> 7 + <img src="{{ .Name }}.postcard.jpeg" loading="lazy" alt="{{ .Front.Description }}" width="500px"> 8 8 <div class="shadow"></div> 9 9 </div> 10 10 </label>
+1 -1
formats/usd/codec.go
··· 151 151 usdFilename := pc.Name + extension 152 152 153 153 // Grab the filename of the texture image, as it might be JPG or PNG 154 - webImg, _ := web.Codec("jpg", "png") 154 + webImg, _ := web.Codec("jpeg", "png") 155 155 fws, err := webImg.Encode(pc, opts) 156 156 if err != nil { 157 157 return nil, err
+3 -3
formats/web/codec.go
··· 23 23 24 24 type codec struct { 25 25 // This holds a list of formats, the first will be tried, and if it's unsuitable, the next etc. 26 - // This is particularly useful for transparency. Eg. A `web.Codec("jpg", "webp")` would save as jpg 26 + // This is particularly useful for transparency. Eg. A `web.Codec("jpeg", "webp")` would save as jpg 27 27 // if there is no transparency, and WebP if there is transparency to encode, or if "archival" was 28 28 // specified (as JPEG can't encode images losslessly). 29 29 formats []string ··· 54 54 } 55 55 56 56 var formatCapabilities = map[string]capabilities{ 57 - "jpg": {}, 57 + "jpeg": {}, 58 58 "webp": {lossless: true, transparency: true}, 59 59 "png": {lossless: true, transparency: true}, 60 60 } ··· 77 77 return codec{formats: fmts}, nil 78 78 } 79 79 80 - var DefaultCodec, _ = Codec("jpg", "webp") 80 + var DefaultCodec, _ = Codec("jpeg", "webp") 81 81 82 82 func (c codec) Name() string { return codecName }
+5 -17
formats/web/decode.go
··· 8 8 "image/jpeg" 9 9 "io" 10 10 11 - "git.sr.ht/~jackmordaunt/go-libwebp/webp" 12 11 "github.com/jphastings/dotpostcard/formats" 13 12 "github.com/jphastings/dotpostcard/formats/xmp" 13 + "github.com/jphastings/dotpostcard/internal/images" 14 14 "github.com/jphastings/dotpostcard/pkg/xmpinject" 15 15 "github.com/jphastings/dotpostcard/types" 16 16 ) 17 17 18 18 func (b bundle) Decode(decOpts formats.DecodeOptions) (types.Postcard, error) { 19 + defer b.Close() 20 + 19 21 var dataCopy bytes.Buffer 20 22 t := io.TeeReader(b, &dataCopy) 21 23 ··· 28 30 var xmpDecoder func([]byte) ([]byte, error) 29 31 switch format { 30 32 case "webp": 31 - imgDecoder = webp.Decode 33 + // This function is defined in multiple files so we can keep the WebP package out of WASM builds. 34 + imgDecoder = images.ReadWebP 32 35 xmpDecoder = xmpinject.XMPfromWebP 33 36 case "jpeg": 34 37 imgDecoder = jpeg.Decode ··· 90 93 91 94 return pc, nil 92 95 } 93 - 94 - // the goalang.org/x/image/webp decoder bugs out on Alpha layers; it gets Registered with the image package 95 - // when jackmordaunt's webp parser is loaded, but sits at the top of the pack — so using image.Decode to 96 - // decode automatically won't work (it uses golang.org version, which breaks) and using image.DecodeConfig 97 - // to get the format also fails (as that also uses the golang.org version, which breaks) 98 - // This slightly hacky approach means we can manually use only jackmordaunt's version 99 - func determineFormat(r io.Reader) (string, error) { 100 - _, err := webp.DecodeConfig(r) 101 - if err == nil { 102 - return "webp", nil 103 - } 104 - 105 - _, format, err := image.Decode(r) 106 - return format, err 107 - }
+5 -4
formats/web/encode.go
··· 11 11 12 12 "github.com/jphastings/dotpostcard/formats" 13 13 "github.com/jphastings/dotpostcard/formats/xmp" 14 + "github.com/jphastings/dotpostcard/internal/images" 14 15 "github.com/jphastings/dotpostcard/types" 15 16 ) 16 17 ··· 78 79 79 80 switch format { 80 81 case "webp": 81 - err = writeWebP(w, combinedImg, xmpData, opts.Archival, pc.Meta.HasTransparency) 82 + err = images.WriteWebP(w, combinedImg, xmpData, opts.Archival, pc.Meta.HasTransparency) 82 83 case "png": 83 - err = writePNG(w, combinedImg, xmpData, opts.Archival) 84 - case "jpg": 85 - err = writeJPG(w, combinedImg, xmpData) 84 + err = images.WritePNG(w, combinedImg, xmpData, opts.Archival) 85 + case "jpeg": 86 + err = images.WriteJPEG(w, combinedImg, xmpData) 86 87 default: 87 88 err = fmt.Errorf("unsupported output image format: %s", format) 88 89 }
-52
formats/web/imageformats.go
··· 1 - package web 2 - 3 - import ( 4 - "bytes" 5 - "image" 6 - "image/png" 7 - "io" 8 - 9 - "git.sr.ht/~jackmordaunt/go-libwebp/webp" 10 - "github.com/gen2brain/jpegli" 11 - "github.com/jphastings/dotpostcard/pkg/xmpinject" 12 - ) 13 - 14 - func writeWebP(w io.Writer, combinedImg image.Image, xmpData []byte, archival, hasAlpha bool) error { 15 - var webpOpts []webp.EncodeOption 16 - if archival { 17 - webpOpts = []webp.EncodeOption{webp.Lossless()} 18 - } else { 19 - webpOpts = []webp.EncodeOption{webp.Quality(70)} 20 - } 21 - 22 - webpData := new(bytes.Buffer) 23 - if err := webp.Encode(webpData, combinedImg, webpOpts...); err != nil { 24 - return err 25 - } 26 - 27 - return xmpinject.XMPintoWebP(w, webpData.Bytes(), xmpData, combinedImg.Bounds(), hasAlpha) 28 - } 29 - 30 - func writePNG(w io.Writer, combinedImg image.Image, xmpData []byte, _ bool) error { 31 - pngData := new(bytes.Buffer) 32 - if err := png.Encode(pngData, combinedImg); err != nil { 33 - return err 34 - } 35 - 36 - return xmpinject.XMPintoPNG(w, pngData.Bytes(), xmpData) 37 - } 38 - 39 - func writeJPG(w io.Writer, combinedImg image.Image, xmpData []byte) error { 40 - jpegliOpts := &jpegli.EncodingOptions{ 41 - Quality: 70, 42 - ProgressiveLevel: 2, 43 - FancyDownsampling: true, 44 - } 45 - 46 - jpgData := new(bytes.Buffer) 47 - if err := jpegli.Encode(jpgData, combinedImg, jpegliOpts); err != nil { 48 - return err 49 - } 50 - 51 - return xmpinject.XMPintoJPEG(w, jpgData.Bytes(), xmpData) 52 - }
+15
formats/web/webp-wasm.go
··· 1 + //go:build wasm 2 + // +build wasm 3 + 4 + package web 5 + 6 + import ( 7 + "image" 8 + "io" 9 + ) 10 + 11 + // This is a trivial substitute for the webp enabled version that requires hackery. 12 + func determineFormat(r io.Reader) (string, error) { 13 + _, format, err := image.Decode(r) 14 + return format, err 15 + }
+26
formats/web/webp.go
··· 1 + //go:build !wasm 2 + // +build !wasm 3 + 4 + package web 5 + 6 + import ( 7 + "image" 8 + "io" 9 + 10 + "git.sr.ht/~jackmordaunt/go-libwebp/webp" 11 + ) 12 + 13 + // the goalang.org/x/image/webp decoder bugs out on Alpha layers; it gets Registered with the image package 14 + // when jackmordaunt's webp parser is loaded, but sits at the top of the pack — so using image.Decode to 15 + // decode automatically won't work (it uses golang.org version, which breaks) and using image.DecodeConfig 16 + // to get the format also fails (as that also uses the golang.org version, which breaks) 17 + // This slightly hacky approach means we can manually use only jackmordaunt's version 18 + func determineFormat(r io.Reader) (string, error) { 19 + _, err := webp.DecodeConfig(r) 20 + if err == nil { 21 + return "webp", nil 22 + } 23 + 24 + _, format, err := image.Decode(r) 25 + return format, err 26 + }
+11
internal/images/jpeg-read.go
··· 1 + package images 2 + 3 + import ( 4 + "image" 5 + "image/jpeg" 6 + "io" 7 + ) 8 + 9 + func ReadJPEG(r io.Reader) (image.Image, error) { 10 + return jpeg.Decode(r) 11 + }
+27
internal/images/jpeg-wasm.go
··· 1 + //go:build wasm 2 + // +build wasm 3 + 4 + package images 5 + 6 + import ( 7 + "bytes" 8 + "image" 9 + "io" 10 + 11 + "image/jpeg" 12 + 13 + "github.com/jphastings/dotpostcard/pkg/xmpinject" 14 + ) 15 + 16 + func WriteJPEG(w io.Writer, combinedImg image.Image, xmpData []byte) error { 17 + jpegOpts := &jpeg.Options{ 18 + Quality: 75, 19 + } 20 + 21 + jpgData := new(bytes.Buffer) 22 + if err := jpeg.Encode(jpgData, combinedImg, jpegOpts); err != nil { 23 + return err 24 + } 25 + 26 + return xmpinject.XMPintoJPEG(w, jpgData.Bytes(), xmpData) 27 + }
+28
internal/images/jpeg-write.go
··· 1 + //go:build !wasm 2 + // +build !wasm 3 + 4 + package images 5 + 6 + import ( 7 + "bytes" 8 + "image" 9 + "io" 10 + 11 + "github.com/gen2brain/jpegli" 12 + "github.com/jphastings/dotpostcard/pkg/xmpinject" 13 + ) 14 + 15 + func WriteJPEG(w io.Writer, combinedImg image.Image, xmpData []byte) error { 16 + jpegliOpts := &jpegli.EncodingOptions{ 17 + Quality: 70, 18 + ProgressiveLevel: 2, 19 + FancyDownsampling: true, 20 + } 21 + 22 + jpgData := new(bytes.Buffer) 23 + if err := jpegli.Encode(jpgData, combinedImg, jpegliOpts); err != nil { 24 + return err 25 + } 26 + 27 + return xmpinject.XMPintoJPEG(w, jpgData.Bytes(), xmpData) 28 + }
+19
internal/images/png.go
··· 1 + package images 2 + 3 + import ( 4 + "bytes" 5 + "image" 6 + "image/png" 7 + "io" 8 + 9 + "github.com/jphastings/dotpostcard/pkg/xmpinject" 10 + ) 11 + 12 + func WritePNG(w io.Writer, combinedImg image.Image, xmpData []byte, _ bool) error { 13 + pngData := new(bytes.Buffer) 14 + if err := png.Encode(pngData, combinedImg); err != nil { 15 + return err 16 + } 17 + 18 + return xmpinject.XMPintoPNG(w, pngData.Bytes(), xmpData) 19 + }
+20
internal/images/webp-wasm.go
··· 1 + //go:build wasm 2 + // +build wasm 3 + 4 + package images 5 + 6 + import ( 7 + "fmt" 8 + "image" 9 + "io" 10 + 11 + "golang.org/x/image/webp" 12 + ) 13 + 14 + func WriteWebP(w io.Writer, img image.Image, xmpData []byte, archival, hasAlpha bool) error { 15 + return fmt.Errorf("writing webP images is not available on this platform") 16 + } 17 + 18 + func ReadWebP(r io.Reader) (image.Image, error) { 19 + return webp.Decode(r) 20 + }
+33
internal/images/webp.go
··· 1 + //go:build !wasm 2 + // +build !wasm 3 + 4 + package images 5 + 6 + import ( 7 + "bytes" 8 + "image" 9 + "io" 10 + 11 + "git.sr.ht/~jackmordaunt/go-libwebp/webp" 12 + "github.com/jphastings/dotpostcard/pkg/xmpinject" 13 + ) 14 + 15 + func WriteWebP(w io.Writer, img image.Image, xmpData []byte, archival, hasAlpha bool) error { 16 + var webpOpts []webp.EncodeOption 17 + if archival { 18 + webpOpts = []webp.EncodeOption{webp.Lossless()} 19 + } else { 20 + webpOpts = []webp.EncodeOption{webp.Quality(70)} 21 + } 22 + 23 + webpData := new(bytes.Buffer) 24 + if err := webp.Encode(webpData, img, webpOpts...); err != nil { 25 + return err 26 + } 27 + 28 + return xmpinject.XMPintoWebP(w, webpData.Bytes(), xmpData, img.Bounds(), hasAlpha) 29 + } 30 + 31 + func ReadWebP(r io.Reader) (image.Image, error) { 32 + return webp.Decode(r) 33 + }
+6 -2
types/postcard.go
··· 13 13 } 14 14 15 15 func (pc Postcard) Sides() int { 16 - if pc.Back == nil { 16 + switch { 17 + case pc.Front == nil: 18 + return 0 19 + case pc.Back == nil: 17 20 return 1 21 + default: 22 + return 2 18 23 } 19 - return 2 20 24 } 21 25 22 26 type Location struct {