package epd7in3e import ( "errors" "fmt" "image" ) // Frame geometry: 800x480, 4 bits per pixel, 2 pixels per byte, left // pixel in the HIGH nibble, row-major, top row first. const ( Width = 800 Height = 480 FrameBytes = Width * Height / 2 // 192000 ) // ErrBadSize is returned when an image is neither 800x480 nor // 480x800. var ErrBadSize = errors.New("epd7in3e: image must be 800x480 or 480x800") // Pack writes the panel frame buffer for img into buf, which must be // exactly FrameBytes long. img must be 800x480, or 480x800, in which // case it is rotated 90 degrees clockwise. Any other size is an error. func Pack(buf []byte, img image.Image) error { if len(buf) != FrameBytes { return fmt.Errorf("epd7in3e: buffer is %d bytes, want %d", len(buf), FrameBytes) } b := img.Bounds() switch { case b.Dx() == Width && b.Dy() == Height: packDirect(buf, img) case b.Dx() == Height && b.Dy() == Width: packRotated(buf, img) default: return fmt.Errorf("epd7in3e: image is %dx%d, want %dx%d or %dx%d: %w", b.Dx(), b.Dy(), Width, Height, Height, Width, ErrBadSize) } return nil } // Rotate180 reverses a packed frame buffer in place: both byte order // and the nibble order within each byte, so the left pixel stays the // high nibble after rotation. func Rotate180(buf []byte) { n := len(buf) for i := 0; i < n/2; i++ { j := n - 1 - i buf[i], buf[j] = swapNibbles(buf[j]), swapNibbles(buf[i]) } } func swapNibbles(b byte) byte { return (b&0x0f)<<4 | b>>4 } // packDirect fills buf from an 800x480 image, with fast paths for the // common concrete types. func packDirect(buf []byte, img image.Image) { switch p := img.(type) { case *image.Paletted: packPaletted(buf, p) case *image.RGBA: packRGBA(buf, p.Pix, p.Stride, p.Rect.Min, false) case *image.NRGBA: packRGBA(buf, p.Pix, p.Stride, p.Rect.Min, true) default: packGeneric(buf, img, 0) } } // packRotated fills buf from a 480x800 image, rotating it 90 degrees // clockwise. func packRotated(buf []byte, img image.Image) { packGeneric(buf, img, 1) } // packGeneric fills buf pixel by pixel through image.At; rot is 1 for // a rotated source. func packGeneric(buf []byte, img image.Image, rot int) { r := img.Bounds() for y := 0; y < Height; y++ { out := buf[y*(Width/2):] for x := 0; x < Width/2; x++ { var c0, c1 Color if rot == 0 { c0 = codeOf(img.At(r.Min.X+2*x, r.Min.Y+y)) c1 = codeOf(img.At(r.Min.X+2*x+1, r.Min.Y+y)) } else { c0 = codeOf(img.At(r.Min.X+y, r.Min.Y+Width-1-2*x)) c1 = codeOf(img.At(r.Min.X+y, r.Min.Y+Width-2-2*x)) } out[x] = byte(c0)<<4 | byte(c1) } } } // packPaletted fills buf from a Paletted image, using a per-palette // code table built once. func packPaletted(buf []byte, p *image.Paletted) { if p.Palette == nil { packGeneric(buf, p, 0) return } var m [256]byte for i, c := range p.Palette { m[i] = byte(codeOf(c)) } minX, minY := p.Rect.Min.X, p.Rect.Min.Y for y := 0; y < Height; y++ { row := p.Pix[(y+minY)*p.Stride+minX:] out := buf[y*(Width/2):] for x := 0; x < Width/2; x++ { out[x] = m[row[2*x]]<<4 | m[row[2*x+1]] } } } // packRGBA fills buf from raw 4-bytes-per-pixel pixels in RGBA (false) // or NRGBA (true, premultiplied alpha) layout. func packRGBA(buf []byte, pix []byte, stride int, min image.Point, premultiplied bool) { for y := 0; y < Height; y++ { i := (y+min.Y)*stride + 4*min.X out := buf[y*(Width/2):] for x := 0; x < Width/2; x++ { var c0, c1 Color if premultiplied { c0 = nrgbaCode(pix[i], pix[i+1], pix[i+2], pix[i+3]) c1 = nrgbaCode(pix[i+4], pix[i+5], pix[i+6], pix[i+7]) } else { c0 = nearestCode(pix[i], pix[i+1], pix[i+2]) c1 = nearestCode(pix[i+4], pix[i+5], pix[i+6]) } out[x] = byte(c0)<<4 | byte(c1) i += 8 } } } // nrgbaCode unpremultiplies one NRGBA pixel and returns its nearest // panel code. func nrgbaCode(r, g, b, a uint8) Color { if a == 0 { return Black } if a == 0xff { return nearestCode(r, g, b) } return nearestCode(uint8(int(r)*255/int(a)), uint8(int(g)*255/int(a)), uint8(int(b)*255/int(a))) }