1// Package png writes opaque SDR captures as PNG files.
2package png
3
4import (
5 "bufio"
6 "context"
7 "errors"
8 "fmt"
9 "image"
10 stdpng "image/png"
11 "io"
12
13 "github.com/bnema/nefercap/internal/adapters/capturefile"
14 "github.com/bnema/nefercap/internal/ports"
15)
16
17// ctxCheckRows bounds how many rows are converted between cancellation checks.
18const ctxCheckRows = 64
19
20// Writer implements ports.ScreenshotWriter. It is stateless and safe for
21// concurrent use; each Save owns its own conversion buffer.
22type Writer struct{}
23
24var _ ports.ScreenshotWriter = (*Writer)(nil)
25
26// New returns a PNG screenshot writer.
27func New() *Writer { return &Writer{} }
28
29// Save encodes f as an opaque RGB PNG at path. The path is created with
30// O_EXCL, so an existing file (or symlink) is never overwritten and yields
31// ports.ErrPathExists. Any file created by a failed Save is removed. All reads
32// of f.Pixels complete before Save returns.
33func (*Writer) Save(ctx context.Context, f ports.Frame, path string) (err error) {
34 if err := ctx.Err(); err != nil {
35 return err
36 }
37 if err := f.Validate(); err != nil {
38 return err
39 }
40 if path == "" {
41 return errors.New("png: empty path")
42 }
43 file, res, err := capturefile.Create(path)
44 if err != nil {
45 return fmt.Errorf("png: %w", err)
46 }
47 // We created the file exclusively, so we own it; a replacement is not ours.
48 defer func() {
49 if err == nil {
50 if cerr := file.Close(); cerr != nil {
51 err = fmt.Errorf("png: close output: %w", cerr)
52 }
53 } else {
54 _ = file.Close()
55 }
56 if err != nil {
57 if rerr := res.Remove(); rerr != nil {
58 err = errors.Join(err, fmt.Errorf("png: %w", rerr))
59 }
60 }
61 }()
62 return New().Encode(ctx, file, f)
63}
64
65// encode writes f to w. The frame is converted once to opaque RGBA because the
66// encoder only has fast paths for the standard image types and stored pixels
67// are BGRA; the copy is exactly one frame and is released on return.
68func (*Writer) Encode(ctx context.Context, w io.Writer, f ports.Frame) error {
69 if err := ctx.Err(); err != nil {
70 return err
71 }
72 if err := f.Validate(); err != nil {
73 return err
74 }
75 img, err := toRGBA(ctx, f)
76 if err != nil {
77 return err
78 }
79 bw := bufio.NewWriterSize(&ctxWriter{ctx: ctx, w: w}, 64<<10)
80 if err := stdpng.Encode(bw, img); err != nil {
81 return fmt.Errorf("png: encode: %w", err)
82 }
83 if err := bw.Flush(); err != nil {
84 return fmt.Errorf("png: write: %w", err)
85 }
86 return nil
87}
88
89// toRGBA converts top-to-bottom, honoring stride, and forces alpha
90// to 255 because output is opaque (XRGB padding and ARGB alpha are ignored).
91func toRGBA(ctx context.Context, f ports.Frame) (*image.RGBA, error) {
92 rowLen := f.Width * ports.BytesPerPixel
93 img := &image.RGBA{
94 Pix: make([]byte, rowLen*f.Height),
95 Stride: rowLen,
96 Rect: image.Rect(0, 0, f.Width, f.Height),
97 }
98 for y := 0; y < f.Height; y++ {
99 if y%ctxCheckRows == 0 {
100 if err := ctx.Err(); err != nil {
101 return nil, err
102 }
103 }
104 src := f.Row(y)
105 dst := img.Pix[y*rowLen : (y+1)*rowLen]
106 for x := 0; x+3 < len(src); x += 4 {
107 dst[x] = src[x+2]
108 dst[x+1] = src[x+1]
109 dst[x+2] = src[x]
110 dst[x+3] = 0xff
111 }
112 }
113 return img, nil
114}
115
116// ctxWriter makes a long encode stop at the next write after cancellation.
117type ctxWriter struct {
118 ctx context.Context
119 w io.Writer
120}
121
122func (c *ctxWriter) Write(p []byte) (int, error) {
123 if err := c.ctx.Err(); err != nil {
124 return 0, err
125 }
126 return c.w.Write(p)
127}