xorg: refuse a window that is not on the screen, and keep an inline screenshot within the bus (review of #154)
mesh/merge-gate pass: builds xorg → g14, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
mesh/merge-gate pass: builds xorg → g14, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
A window that is unmapped or on a hidden workspace was cut from the screen anyway, which shows what is there instead of it. An inline image of up to 4 MiB could not cross the bus, whose largest message is 1 MiB; it is now held to 700 KiB and halved in size until it fits, saying so.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"os"
|
||||
@@ -495,8 +496,28 @@ func (x xorg) keyboard(ctx context.Context, a desktop.Args) (any, error) {
|
||||
|
||||
var windowID = regexp.MustCompile(`^(0x[0-9a-fA-F]{1,8}|[0-9]{1,10})$`)
|
||||
|
||||
// mostInline is the largest PNG a screenshot carries in its answer.
|
||||
const mostInline = 4 << 20
|
||||
// mostInline is the largest PNG a screenshot carries in its answer. A tool's answer crosses the bus,
|
||||
// whose largest message is 1 MiB; 700 KiB is about 935 KiB as base64, which leaves room for the rest.
|
||||
const mostInline = 700 << 10
|
||||
|
||||
// shrink averages each by×by block of an image into one pixel.
|
||||
func shrink(img image.Image, by int) *image.RGBA {
|
||||
b := img.Bounds()
|
||||
out := image.NewRGBA(image.Rect(0, 0, b.Dx()/by, b.Dy()/by))
|
||||
for y := 0; y < out.Bounds().Dy(); y++ {
|
||||
for x := 0; x < out.Bounds().Dx(); x++ {
|
||||
var r, g, bl, n uint32
|
||||
for dy := 0; dy < by; dy++ {
|
||||
for dx := 0; dx < by; dx++ {
|
||||
cr, cg, cb, _ := img.At(b.Min.X+x*by+dx, b.Min.Y+y*by+dy).RGBA()
|
||||
r, g, bl, n = r+cr, g+cg, bl+cb, n+1
|
||||
}
|
||||
}
|
||||
out.SetRGBA(x, y, color.RGBA{uint8(r / n >> 8), uint8(g / n >> 8), uint8(bl / n >> 8), 0xff})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
|
||||
inline, _, err := a.Bool("inline")
|
||||
@@ -535,6 +556,12 @@ func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// A window that is not viewable is not on the screen: its rectangle would show what is there
|
||||
// instead, which is a picture of the wrong thing.
|
||||
if g.Mapped != "" && g.Mapped != "IsViewable" {
|
||||
return nil, fmt.Errorf("window %s is %s, not on the screen (IsUnMapped: not shown; IsUnviewable: "+
|
||||
"its parent is not shown, e.g. on another workspace): bring it to the screen first", w, g.Mapped)
|
||||
}
|
||||
cut = &g
|
||||
}
|
||||
args := []string{"-silent", "-out", dump.Name(), "-root"}
|
||||
@@ -593,11 +620,25 @@ func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
|
||||
b := shot.Bounds()
|
||||
answer := map[string]any{"session": s.Display, "path": path, "width": b.Dx(), "height": b.Dy(), "bytes": buf.Len()}
|
||||
if inline {
|
||||
// For a session that cannot read the machine's files: the image itself, within a bound.
|
||||
if buf.Len() > mostInline {
|
||||
answer["inline"] = fmt.Sprintf("not included: %d bytes is more than %d; take one output or one window", buf.Len(), mostInline)
|
||||
// For a session that cannot read the machine's files: the image itself, within the bus's bound.
|
||||
// An image larger than mostInline is halved until it fits, and the answer says by how much; the
|
||||
// file keeps the full size.
|
||||
small, scale := buf.Bytes(), 1
|
||||
for len(small) > mostInline && shot.Bounds().Dx()/(scale*2) >= 64 {
|
||||
scale *= 2
|
||||
var b bytes.Buffer
|
||||
if err := png.Encode(&b, shrink(shot, scale)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
small = b.Bytes()
|
||||
}
|
||||
if len(small) > mostInline {
|
||||
answer["inline"] = fmt.Sprintf("not included: %d bytes even at 1/%d; take one output or one window", len(small), scale)
|
||||
} else {
|
||||
answer["png_base64"] = base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
answer["png_base64"] = base64.StdEncoding.EncodeToString(small)
|
||||
if scale > 1 {
|
||||
answer["inline_scale"] = fmt.Sprintf("1/%d of the file's size, to fit the bus", scale)
|
||||
}
|
||||
}
|
||||
}
|
||||
return answer, nil
|
||||
|
||||
Reference in New Issue
Block a user