feat/freeze-key 753c87f

png.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 9

↑↓ moveenter openesc close