cmd/handsum: new tool for the Handsum file format

This commit is contained in:
Nigel Tao
2025-02-23 12:15:59 +11:00
parent 9a7e2a4d06
commit 12b404f58d
6 changed files with 536 additions and 0 deletions

126
cmd/handsum/main.go Normal file
View File

@@ -0,0 +1,126 @@
// Copyright 2025 The Wuffs Authors.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//
// SPDX-License-Identifier: Apache-2.0 OR MIT
// ----------------
// handsum decodes and encodes the Handsum lossy image file format.
package main
import (
"bytes"
"errors"
"flag"
"image"
"image/png"
"os"
"github.com/google/wuffs/lib/handsum"
_ "image/gif"
_ "image/jpeg"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
)
var (
decodeFlag = flag.Bool("decode", false, "whether to decode the input")
encodeFlag = flag.Bool("encode", false, "whether to encode the input")
roundtripFlag = flag.Bool("roundtrip", false, "whether to encode-and-decode the input")
)
const usageStr = `handsum decodes and encodes the Handsum lossy image file format.
Usage: choose one of
handsum -decode [path]
handsum -encode [path]
handsum -roundtrip [path]
The path to the input image file is optional. If omitted, stdin is read.
The output image (in Handsum or PNG format) is written to stdout.
Decode inputs Handsum and outputs PNG.
Encode inputs BMP, GIF, JPEG, PNG, TIFF or WEBP and outputs Handsum.
Roundtrip is equivalent to encode (to an ephemeral file) and then decode.
`
func main() {
if err := main1(); err != nil {
os.Stderr.WriteString(err.Error() + "\n")
os.Exit(1)
}
}
func main1() error {
flag.Usage = func() { os.Stderr.WriteString(usageStr) }
flag.Parse()
inFile := os.Stdin
switch flag.NArg() {
case 0:
// No-op.
case 1:
f, err := os.Open(flag.Arg(0))
if err != nil {
return err
}
defer f.Close()
inFile = f
default:
return errors.New("too many filenames; the maximum is one")
}
if *decodeFlag && !*encodeFlag && !*roundtripFlag {
return decode(inFile)
}
if !*decodeFlag && *encodeFlag && !*roundtripFlag {
return encode(inFile)
}
if !*decodeFlag && !*encodeFlag && *roundtripFlag {
return roundtrip(inFile)
}
return errors.New("must specify exactly one of -decode, -encode, -roundtrip or -help")
}
func decode(inFile *os.File) error {
src, err := handsum.Decode(inFile)
if err != nil {
return err
}
return png.Encode(os.Stdout, src)
}
func encode(inFile *os.File) error {
src, _, err := image.Decode(inFile)
if err != nil {
return err
}
return handsum.Encode(os.Stdout, src)
}
func roundtrip(inFile *os.File) error {
src, _, err := image.Decode(inFile)
if err != nil {
return err
}
buf := &bytes.Buffer{}
err = handsum.Encode(buf, src)
if err != nil {
return err
}
dst, err := handsum.Decode(buf)
if err != nil {
return err
}
return png.Encode(os.Stdout, dst)
}

2
go.mod
View File

@@ -1,3 +1,5 @@
module github.com/google/wuffs
go 1.16
require golang.org/x/image v0.24.0 // indirect

63
go.sum
View File

@@ -0,0 +1,63 @@
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

345
lib/handsum/handsum.go Normal file
View File

@@ -0,0 +1,345 @@
// Copyright 2025 The Wuffs Authors.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//
// SPDX-License-Identifier: Apache-2.0 OR MIT
// ----------------
// Package handsum implements the Handsum image file format.
//
// This is a very lossy format for very small thumbnails. Very small in terms
// of image dimensions, up to 32×32 pixels, but also in terms of file size.
// Every Handsum image file is exactly 48 bytes (384 bits) long. This can imply
// using as little as 0.046875 bytes (0.375 bits) per pixel.
//
// Each Handsum image is essentially a scaled 16×16 pixel YCbCr 4:2:0 JPEG MCU
// (Minimum Coded Unit; 4 Luma and 2 Chroma blocks), keeping only the 15 lowest
// frequency DCT (Discrete Cosine Transform) coefficients. Each of the (6 × 15)
// = 90 coefficients are encoded as one nibble (4 bits) with fixed quantization
// factors, totalling 45 bytes. The initial 3 bytes holds a 16-bit magic
// signature, 2-bit version number and 6-bit aspect ratio.
//
// As of February 2025, the latest version is Version 0. All Version 0 files
// use the sRGB color profile.
package handsum
import (
"errors"
"image"
"image/color"
"io"
"github.com/google/wuffs/lib/lowleveljpeg"
"golang.org/x/image/draw"
)
// FileSize is the size (in bytes) of every Handsum image file.
const FileSize = 48
// MaxDimension is the maximum (inclusive) width or height of every Handsum
// image file.
//
// Every image is either (W × 32) or (32 × H) or both, for some positive W or H
// that is no greater than 32.
const MaxDimension = 32
// Magic is the byte string prefix of every Handsum image file.
//
// It's like how every JPEG image file starts with "\xFF\xD8".
const Magic = "\xFE\xD7"
func init() {
image.RegisterFormat("handsum", Magic, Decode, DecodeConfig)
}
var (
ErrBadArgument = errors.New("handsum: bad argument")
ErrNotAHandsumFile = errors.New("handsum: not a handsum file")
ErrUnsupportedFileVersion = errors.New("handsum: unsupported file version")
)
// Encode writes src to w in the Handsum format.
func Encode(w io.Writer, src image.Image) error {
if (w == nil) || (src == nil) {
return ErrBadArgument
}
srcB := src.Bounds()
srcW, srcH := srcB.Dx(), srcB.Dy()
if (srcW <= 0) || (srcH <= 0) {
return ErrBadArgument
}
aspectRatio := byte(0x1F)
if srcW > srcH { // Landscape.
a := ((int64(srcH) * 64) + int64(srcW)) / (2 * int64(srcW))
if a <= 0 {
a = 1
}
aspectRatio = byte(a-1) | 0x00
} else if srcW < srcH { // Portrait.
a := ((int64(srcW) * 64) + int64(srcH)) / (2 * int64(srcH))
if a <= 0 {
a = 1
}
aspectRatio = byte(a-1) | 0x20
}
dst := image.NewRGBA(image.Rectangle{Max: image.Point{X: 16, Y: 16}})
draw.BiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Src, nil)
dstU8s := lowleveljpeg.Array6BlockU8{}
dstU8s.ExtractFrom(dst, 0, 0)
dstI16s := lowleveljpeg.Array6BlockI16{}
dstI16s.ForwardDCTFrom(&dstU8s)
buf := [FileSize]byte{}
buf[0] = Magic[0]
buf[1] = Magic[1]
buf[2] = aspectRatio
bitOffset := 3 * 8
for i := range dstI16s {
bitOffset = encodeBlock(&buf, bitOffset, &dstI16s[i])
}
_, err := w.Write(buf[:])
return err
}
func encodeBlock(buf *[FileSize]byte, bitOffset int, b *lowleveljpeg.BlockI16) int {
for i := 0; i < nCoeffs; i++ {
e := uint8(0)
if i == 0 {
e = encodeDC(b[0])
} else {
e = encodeAC(b[zigzag[i]])
}
buf[bitOffset>>3] |= e << (bitOffset & 4)
bitOffset += 4
}
return bitOffset
}
func encodeDC(value int16) uint8 {
const w = dcBucketWidth
v := int32(value) + ((w * 8) + (w / 2))
v /= w
if v < 0x0 {
return 0x0
} else if v > 0xF {
return 0xF
}
return uint8(v)
}
func encodeAC(value int16) uint8 {
// Dividing by 2 is an arbitrary adjustment that's not mirrored on the
// decode side, but the results seem a little more vibrant.
const w = acBucketWidth / 2
v := int32(value) + ((w * 8) + (w / 2))
v /= w
if v < 0x0 {
return 0x0
} else if v > 0xF {
return 0xF
}
return uint8(v)
}
// Both DC and AC coefficients are quantized into 16 buckets (4 bits), but they
// use different bucket widths:
//
// Bucket DC AC
// 0x0 -1024 -128
// 0x1 -896 -112
// 0x2 -768 -96
// 0x3 -640 -80
// ... ... ...
// 0x7 -128 -16
// 0x8 0 0
// 0x9 +128 +16
// ... ... ...
// 0xE +768 +96
// 0xF +896 +112
const (
dcBucketWidth = 128
acBucketWidth = 16
)
// DecodeConfig reads a Handsum image configuration from r.
func DecodeConfig(r io.Reader) (image.Config, error) {
buf := [3]byte{}
if _, err := io.ReadFull(r, buf[:]); err != nil {
return image.Config{}, err
} else if (buf[0] != Magic[0]) || (buf[1] != Magic[1]) {
return image.Config{}, ErrNotAHandsumFile
} else if (buf[2] & 0xC0) != 0 {
return image.Config{}, ErrUnsupportedFileVersion
}
w, h := decodeWidthAndHeight(buf[2])
return image.Config{
ColorModel: color.RGBAModel,
Width: w,
Height: h,
}, nil
}
// Decode reads a Handsum image from r.
func Decode(r io.Reader) (image.Image, error) {
buf := [FileSize]byte{}
if _, err := io.ReadFull(r, buf[:]); err != nil {
return nil, err
} else if (buf[0] != Magic[0]) || (buf[1] != Magic[1]) {
return nil, ErrNotAHandsumFile
} else if (buf[2] & 0xC0) != 0 {
return nil, ErrUnsupportedFileVersion
}
bitOffset := 3 * 8
lumaQuadBlockU8 := lowleveljpeg.QuadBlockU8{}
bitOffset = decodeBlock(lumaQuadBlockU8[0x00:], 16, &buf, bitOffset)
bitOffset = decodeBlock(lumaQuadBlockU8[0x08:], 16, &buf, bitOffset)
bitOffset = decodeBlock(lumaQuadBlockU8[0x80:], 16, &buf, bitOffset)
bitOffset = decodeBlock(lumaQuadBlockU8[0x88:], 16, &buf, bitOffset)
smoothLumaBlockSeams(&lumaQuadBlockU8)
cbBlockU8 := lowleveljpeg.BlockU8{}
bitOffset = decodeBlock(cbBlockU8[:], 8, &buf, bitOffset)
cbQuadBlockU8 := lowleveljpeg.QuadBlockU8{}
cbQuadBlockU8.UpsampleFrom(&cbBlockU8)
crBlockU8 := lowleveljpeg.BlockU8{}
bitOffset = decodeBlock(crBlockU8[:], 8, &buf, bitOffset)
crQuadBlockU8 := lowleveljpeg.QuadBlockU8{}
crQuadBlockU8.UpsampleFrom(&crBlockU8)
src := &image.YCbCr{
Y: lumaQuadBlockU8[:],
Cb: cbQuadBlockU8[:],
Cr: crQuadBlockU8[:],
YStride: 16,
CStride: 16,
SubsampleRatio: image.YCbCrSubsampleRatio444,
Rect: image.Rectangle{Max: image.Point{X: 16, Y: 16}},
}
dstW, dstH := decodeWidthAndHeight(buf[2])
dst := image.NewRGBA(image.Rectangle{Max: image.Point{X: dstW, Y: dstH}})
draw.BiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Src, nil)
return dst, nil
}
func decodeWidthAndHeight(buf2 byte) (w int, h int) {
if (buf2 & 0x20) == 0 { // Landscape.
w = 32
h = 1 + int(buf2&0x1F)
} else { // Portrait.
w = 1 + int(buf2&0x1F)
h = 32
}
return w, h
}
func decodeBlock(dst []byte, stride int, src *[FileSize]byte, bitOffset int) int {
a := lowleveljpeg.BlockI16{}
{
nibble := (src[bitOffset>>3] >> (bitOffset & 4)) & 15
a[0] = (int16(nibble) - 8) * dcBucketWidth
bitOffset += 4
}
for i := 1; i < nCoeffs; i++ {
nibble := (src[bitOffset>>3] >> (bitOffset & 4)) & 15
a[zigzag[i]] = (int16(nibble) - 8) * acBucketWidth
bitOffset += 4
}
b := lowleveljpeg.BlockU8{}
b.InverseDCTFrom(&a)
for i := 0; i < 8; i++ {
di := i * stride
bi := i * 8
copy(dst[di:di+8], b[bi:bi+8])
}
return bitOffset
}
const nCoeffs = 15
// zigzag represents JPEG's zig-zag order for visiting coefficients. Handsum
// only uses the first (1 + 2 + 3 + 4 + 5) = 15 of JPEG's 64 coefficients.
//
// https://en.wikipedia.org/wiki/File:JPEG_ZigZag.svg
var zigzag = [nCoeffs]uint8{
0o00, 0o01, 0o10, 0o20, 0o11, 0o02, 0o03, 0o12, // 0, 1, 8, 16, 9, 2, 3, 10,
0o21, 0o30, 0o40, 0o31, 0o22, 0o13, 0o04, // 17, 24, 32, 25, 18, 11, 4,
}
func smoothLumaBlockSeams(b *lowleveljpeg.QuadBlockU8) {
for _, pair := range smoothingPairs {
v0 := uint32(b[pair[0]])
v1 := uint32(b[pair[1]])
b[pair[0]] = uint8(((3 * v0) + v1 + 2) / 4)
b[pair[1]] = uint8(((3 * v1) + v0 + 2) / 4)
}
v77 := uint32(b[0x77])
v78 := uint32(b[0x78])
v88 := uint32(b[0x88])
v87 := uint32(b[0x87])
b[0x77] = uint8(((9 * v77) + (3 * v78) + v88 + (3 * v87) + 8) / 16)
b[0x78] = uint8(((9 * v78) + (3 * v88) + v87 + (3 * v77) + 8) / 16)
b[0x88] = uint8(((9 * v88) + (3 * v87) + v77 + (3 * v78) + 8) / 16)
b[0x87] = uint8(((9 * v87) + (3 * v77) + v78 + (3 * v88) + 8) / 16)
}
// smoothingPairs are the seams of the four 8×8 Luma blocks in a 16×16 MCU. The
// central 4 pixels are handled separately.
var smoothingPairs = [28][2]uint8{
{0x07, 0x08},
{0x17, 0x18},
{0x27, 0x28},
{0x37, 0x38},
{0x47, 0x48},
{0x57, 0x58},
{0x67, 0x68},
{0x70, 0x80},
{0x71, 0x81},
{0x72, 0x82},
{0x73, 0x83},
{0x74, 0x84},
{0x75, 0x85},
{0x76, 0x86},
{0x79, 0x89},
{0x7A, 0x8A},
{0x7B, 0x8B},
{0x7C, 0x8C},
{0x7D, 0x8D},
{0x7E, 0x8E},
{0x7F, 0x8F},
{0x97, 0x98},
{0xA7, 0xA8},
{0xB7, 0xB8},
{0xC7, 0xC8},
{0xD7, 0xD8},
{0xE7, 0xE8},
{0xF7, 0xF8},
}

Binary file not shown.

Binary file not shown.