diff --git a/core/cmd/dms/commands_screenshot.go b/core/cmd/dms/commands_screenshot.go index f8660b7a..1e16c8f5 100644 --- a/core/cmd/dms/commands_screenshot.go +++ b/core/cmd/dms/commands_screenshot.go @@ -295,7 +295,14 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui data := buf.Data() rgb := make([]byte, dstW*dstH*3) - swapRB := pixelFormat == uint32(screenshot.FormatARGB8888) || pixelFormat == uint32(screenshot.FormatXRGB8888) || pixelFormat == 0 + + var swapRB bool + switch pixelFormat { + case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888): + swapRB = false + default: + swapRB = true + } for y := 0; y < dstH; y++ { srcY := int(float64(y) / scale) @@ -309,16 +316,17 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui } si := srcY*buf.Stride + srcX*4 di := (y*dstW + x) * 3 - if si+2 < len(data) { - if swapRB { - rgb[di+0] = data[si+2] - rgb[di+1] = data[si+1] - rgb[di+2] = data[si+0] - } else { - rgb[di+0] = data[si+0] - rgb[di+1] = data[si+1] - rgb[di+2] = data[si+2] - } + if si+3 >= len(data) { + continue + } + if swapRB { + rgb[di+0] = data[si+2] + rgb[di+1] = data[si+1] + rgb[di+2] = data[si+0] + } else { + rgb[di+0] = data[si+0] + rgb[di+1] = data[si+1] + rgb[di+2] = data[si+2] } } } @@ -370,7 +378,37 @@ func runScreenshotList(cmd *cobra.Command, args []string) { } for _, o := range outputs { - fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n", - o.Name, o.Width, o.Height, o.X, o.Y, o.Scale) + scaleStr := fmt.Sprintf("%.2f", o.FractionalScale) + if o.FractionalScale == float64(int(o.FractionalScale)) { + scaleStr = fmt.Sprintf("%d", int(o.FractionalScale)) + } + + transformStr := transformName(o.Transform) + + fmt.Printf("%s: %dx%d+%d+%d scale=%s transform=%s\n", + o.Name, o.Width, o.Height, o.X, o.Y, scaleStr, transformStr) + } +} + +func transformName(t int32) string { + switch t { + case 0: + return "normal" + case 1: + return "90" + case 2: + return "180" + case 3: + return "270" + case 4: + return "flipped" + case 5: + return "flipped-90" + case 6: + return "flipped-180" + case 7: + return "flipped-270" + default: + return fmt.Sprintf("%d", t) } } diff --git a/core/internal/screenshot/compositor.go b/core/internal/screenshot/compositor.go index d74207c0..64b0a8d3 100644 --- a/core/internal/screenshot/compositor.go +++ b/core/internal/screenshot/compositor.go @@ -90,14 +90,15 @@ func SetCompositorDWL() { } type WindowGeometry struct { - X int32 - Y int32 - Width int32 - Height int32 - Output string - Scale float64 - OutputX int32 - OutputY int32 + X int32 + Y int32 + Width int32 + Height int32 + Output string + Scale float64 + OutputX int32 + OutputY int32 + OutputTransform int32 } func GetActiveWindow() (*WindowGeometry, error) { @@ -385,17 +386,22 @@ func GetFocusedMonitor() string { return "" } -func getOutputPosition(outputName string) (x, y int32, ok bool) { +type outputInfo struct { + x, y int32 + transform int32 +} + +func getOutputInfo(outputName string) (*outputInfo, bool) { display, err := client.Connect("") if err != nil { - return 0, 0, false + return nil, false } ctx := display.Context() defer ctx.Close() registry, err := display.GetRegistry() if err != nil { - return 0, 0, false + return nil, false } var outputManager *wlr_output_management.ZwlrOutputManagerV1 @@ -414,16 +420,17 @@ func getOutputPosition(outputName string) (x, y int32, ok bool) { }) if err := wlhelpers.Roundtrip(display, ctx); err != nil { - return 0, 0, false + return nil, false } if outputManager == nil { - return 0, 0, false + return nil, false } type headState struct { - name string - x, y int32 + name string + x, y int32 + transform int32 } heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState) done := false @@ -438,6 +445,9 @@ func getOutputPosition(outputName string) (x, y int32, ok bool) { state.x = pe.X state.y = pe.Y }) + e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) { + state.transform = te.Transform + }) }) outputManager.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) { done = true @@ -445,17 +455,21 @@ func getOutputPosition(outputName string) (x, y int32, ok bool) { for !done { if err := ctx.Dispatch(); err != nil { - return 0, 0, false + return nil, false } } for _, state := range heads { if state.name == outputName { - return state.x, state.y, true + return &outputInfo{ + x: state.x, + y: state.y, + transform: state.transform, + }, true } } - return 0, 0, false + return nil, false } func getDWLActiveWindow() (*WindowGeometry, error) { @@ -586,21 +600,22 @@ func getDWLActiveWindow() (*WindowGeometry, error) { scale = 1.0 } - var outputX, outputY int32 - if ox, oy, ok := getOutputPosition(state.name); ok { - outputX, outputY = ox, oy + geom := &WindowGeometry{ + X: state.x, + Y: state.y, + Width: state.w, + Height: state.h, + Output: state.name, + Scale: scale, } - return &WindowGeometry{ - X: state.x, - Y: state.y, - Width: state.w, - Height: state.h, - Output: state.name, - Scale: scale, - OutputX: outputX, - OutputY: outputY, - }, nil + if info, ok := getOutputInfo(state.name); ok { + geom.OutputX = info.x + geom.OutputY = info.y + geom.OutputTransform = info.transform + } + + return geom, nil } return nil, fmt.Errorf("no active output found") diff --git a/core/internal/screenshot/encode.go b/core/internal/screenshot/encode.go index 1535320c..174f2b6c 100644 --- a/core/internal/screenshot/encode.go +++ b/core/internal/screenshot/encode.go @@ -20,7 +20,13 @@ func BufferToImageWithFormat(buf *ShmBuffer, format uint32) *image.RGBA { img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height)) data := buf.Data() - swapRB := format == uint32(FormatARGB8888) || format == uint32(FormatXRGB8888) || format == 0 + var swapRB bool + switch format { + case uint32(FormatABGR8888), uint32(FormatXBGR8888): + swapRB = false + default: + swapRB = true + } for y := 0; y < buf.Height; y++ { srcOff := y * buf.Stride diff --git a/core/internal/screenshot/region.go b/core/internal/screenshot/region.go index 239270fb..dc0066b6 100644 --- a/core/internal/screenshot/region.go +++ b/core/internal/screenshot/region.go @@ -380,19 +380,21 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture, return } + var capturedBuf *ShmBuffer + frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) { + if int(e.Stride) < int(e.Width)*4 { + log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width) + return + } buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride)) if err != nil { log.Error("create screen buffer failed", "err", err) return } - if withCursor { - pc.screenBuf = buf - pc.format = e.Format - } else { - pc.screenBufNoCursor = buf - } + capturedBuf = buf + pc.format = e.Format pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size())) if err != nil { @@ -421,6 +423,34 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture, frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) { frame.Destroy() + + if capturedBuf == nil { + onReady() + return + } + + if pc.yInverted { + capturedBuf.FlipVertical() + pc.yInverted = false + } + + if output.transform != TransformNormal { + invTransform := InverseTransform(output.transform) + transformed, err := capturedBuf.ApplyTransform(invTransform) + if err != nil { + log.Error("apply transform failed", "err", err) + } else if transformed != capturedBuf { + capturedBuf.Close() + capturedBuf = transformed + } + } + + if withCursor { + pc.screenBuf = capturedBuf + } else { + pc.screenBufNoCursor = capturedBuf + } + onReady() }) diff --git a/core/internal/screenshot/screenshot.go b/core/internal/screenshot/screenshot.go index a5bdb959..1308607f 100644 --- a/core/internal/screenshot/screenshot.go +++ b/core/internal/screenshot/screenshot.go @@ -324,13 +324,18 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) { outX, outY := output.x, output.y scale := float64(output.scale) - if DetectCompositor() == CompositorHyprland { + switch DetectCompositor() { + case CompositorHyprland: if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok { outX, outY = hx, hy } if s := GetHyprlandMonitorScale(output.name); s > 0 { scale = s } + case CompositorDWL: + if info, ok := getOutputInfo(output.name); ok { + outX, outY = info.x, info.y + } } if scale <= 0 { scale = 1.0 @@ -458,13 +463,42 @@ func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult return nil, fmt.Errorf("capture output: %w", err) } - return s.processFrame(frame, Region{ + result, err := s.processFrame(frame, Region{ X: output.x, Y: output.y, Width: output.width, Height: output.height, Output: output.name, }) + if err != nil { + return nil, err + } + + if result.YInverted { + result.Buffer.FlipVertical() + result.YInverted = false + } + + if output.transform == TransformNormal { + return result, nil + } + + invTransform := InverseTransform(output.transform) + transformed, err := result.Buffer.ApplyTransform(invTransform) + if err != nil { + result.Buffer.Close() + return nil, fmt.Errorf("apply transform: %w", err) + } + + if transformed != result.Buffer { + result.Buffer.Close() + result.Buffer = transformed + } + + result.Region.Width = int32(transformed.Width) + result.Region.Height = int32(transformed.Height) + + return result, nil } func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*CaptureResult, error) { @@ -545,6 +579,10 @@ func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*Ca } func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) { + if output.transform != TransformNormal { + return s.captureRegionOnTransformedOutput(output, region) + } + scale := output.fractionalScale if scale <= 0 && DetectCompositor() == CompositorHyprland { scale = GetHyprlandMonitorScale(output.name) @@ -599,6 +637,76 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio return s.processFrame(frame, region) } +func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, region Region) (*CaptureResult, error) { + result, err := s.captureWholeOutput(output) + if err != nil { + return nil, err + } + + scale := output.fractionalScale + if scale <= 0 && DetectCompositor() == CompositorHyprland { + scale = GetHyprlandMonitorScale(output.name) + } + if scale <= 0 { + scale = float64(output.scale) + } + if scale <= 0 { + scale = 1.0 + } + + localX := int(float64(region.X-output.x) * scale) + localY := int(float64(region.Y-output.y) * scale) + w := int(float64(region.Width) * scale) + h := int(float64(region.Height) * scale) + + if localX < 0 { + w += localX + localX = 0 + } + if localY < 0 { + h += localY + localY = 0 + } + if localX+w > result.Buffer.Width { + w = result.Buffer.Width - localX + } + if localY+h > result.Buffer.Height { + h = result.Buffer.Height - localY + } + + if w <= 0 || h <= 0 { + result.Buffer.Close() + return nil, fmt.Errorf("region not visible on output") + } + + cropped, err := CreateShmBuffer(w, h, w*4) + if err != nil { + result.Buffer.Close() + return nil, fmt.Errorf("create crop buffer: %w", err) + } + + srcData := result.Buffer.Data() + dstData := cropped.Data() + + for y := 0; y < h; y++ { + srcOff := (localY+y)*result.Buffer.Stride + localX*4 + dstOff := y * cropped.Stride + if srcOff+w*4 <= len(srcData) && dstOff+w*4 <= len(dstData) { + copy(dstData[dstOff:dstOff+w*4], srcData[srcOff:srcOff+w*4]) + } + } + + result.Buffer.Close() + cropped.Format = PixelFormat(result.Format) + + return &CaptureResult{ + Buffer: cropped, + Region: region, + YInverted: false, + Format: result.Format, + }, nil +} + func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) { var buf *ShmBuffer var pool *client.ShmPool @@ -609,6 +717,10 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, failed := false frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) { + if int(e.Stride) < int(e.Width)*4 { + log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width) + return + } var err error buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride)) if err != nil { @@ -906,16 +1018,32 @@ func ListOutputs() ([]Output, error) { sc.outputsMu.Lock() defer sc.outputsMu.Unlock() + compositor := DetectCompositor() result := make([]Output, 0, len(sc.outputs)) for _, o := range sc.outputs { - result = append(result, Output{ - Name: o.name, - X: o.x, - Y: o.y, - Width: o.width, - Height: o.height, - Scale: o.scale, - }) + out := Output{ + Name: o.name, + X: o.x, + Y: o.y, + Width: o.width, + Height: o.height, + Scale: o.scale, + FractionalScale: o.fractionalScale, + Transform: o.transform, + } + + switch compositor { + case CompositorHyprland: + if hx, hy, hw, hh, ok := GetHyprlandMonitorGeometry(o.name); ok { + out.X, out.Y = hx, hy + out.Width, out.Height = hw, hh + } + if s := GetHyprlandMonitorScale(o.name); s > 0 { + out.FractionalScale = s + } + } + + result = append(result, out) } return result, nil } diff --git a/core/internal/screenshot/shm.go b/core/internal/screenshot/shm.go index 758c828a..64784d70 100644 --- a/core/internal/screenshot/shm.go +++ b/core/internal/screenshot/shm.go @@ -11,8 +11,23 @@ const ( FormatXBGR8888 = shm.FormatXBGR8888 ) +const ( + TransformNormal = shm.TransformNormal + Transform90 = shm.Transform90 + Transform180 = shm.Transform180 + Transform270 = shm.Transform270 + TransformFlipped = shm.TransformFlipped + TransformFlipped90 = shm.TransformFlipped90 + TransformFlipped180 = shm.TransformFlipped180 + TransformFlipped270 = shm.TransformFlipped270 +) + type ShmBuffer = shm.Buffer func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) { return shm.CreateBuffer(width, height, stride) } + +func InverseTransform(transform int32) int32 { + return shm.InverseTransform(transform) +} diff --git a/core/internal/screenshot/types.go b/core/internal/screenshot/types.go index 339c5747..e7d8507d 100644 --- a/core/internal/screenshot/types.go +++ b/core/internal/screenshot/types.go @@ -32,11 +32,13 @@ func (r Region) IsEmpty() bool { } type Output struct { - Name string - X, Y int32 - Width int32 - Height int32 - Scale int32 + Name string + X, Y int32 + Width int32 + Height int32 + Scale int32 + FractionalScale float64 + Transform int32 } type Config struct { diff --git a/core/internal/wayland/shm/buffer.go b/core/internal/wayland/shm/buffer.go index d41fe6ad..294bd055 100644 --- a/core/internal/wayland/shm/buffer.go +++ b/core/internal/wayland/shm/buffer.go @@ -137,3 +137,96 @@ func (b *Buffer) Clear() { func (b *Buffer) CopyFrom(src *Buffer) { copy(b.data, src.data) } + +const ( + TransformNormal = 0 + Transform90 = 1 + Transform180 = 2 + Transform270 = 3 + TransformFlipped = 4 + TransformFlipped90 = 5 + TransformFlipped180 = 6 + TransformFlipped270 = 7 +) + +func (b *Buffer) ApplyTransform(transform int32) (*Buffer, error) { + if transform == TransformNormal { + return b, nil + } + + var newW, newH int + switch transform { + case Transform90, Transform270, TransformFlipped90, TransformFlipped270: + newW, newH = b.Height, b.Width + default: + newW, newH = b.Width, b.Height + } + + newBuf, err := CreateBuffer(newW, newH, newW*4) + if err != nil { + return nil, err + } + newBuf.Format = b.Format + + srcData := b.data + dstData := newBuf.data + + for sy := 0; sy < b.Height; sy++ { + for sx := 0; sx < b.Width; sx++ { + var dx, dy int + + switch transform { + case Transform90: // 90° CCW + dx = sy + dy = b.Width - 1 - sx + case Transform180: + dx = b.Width - 1 - sx + dy = b.Height - 1 - sy + case Transform270: // 270° CCW = 90° CW + dx = b.Height - 1 - sy + dy = sx + case TransformFlipped: + dx = b.Width - 1 - sx + dy = sy + case TransformFlipped90: + dx = sy + dy = sx + case TransformFlipped180: + dx = sx + dy = b.Height - 1 - sy + case TransformFlipped270: + dx = b.Height - 1 - sy + dy = b.Width - 1 - sx + default: + dx, dy = sx, sy + } + + si := sy*b.Stride + sx*4 + di := dy*newBuf.Stride + dx*4 + + if si+3 < len(srcData) && di+3 < len(dstData) { + dstData[di+0] = srcData[si+0] + dstData[di+1] = srcData[si+1] + dstData[di+2] = srcData[si+2] + dstData[di+3] = srcData[si+3] + } + } + } + + return newBuf, nil +} + +func InverseTransform(transform int32) int32 { + switch transform { + case Transform90: + return Transform270 + case Transform270: + return Transform90 + case TransformFlipped90: + return TransformFlipped270 + case TransformFlipped270: + return TransformFlipped90 + default: + return transform + } +}