fix: render RDP sessions at native pixel density (#1059)

This commit is contained in:
ZacharyZcR
2026-07-16 11:52:37 +08:00
committed by GitHub
parent 84d4e9522a
commit 39d40db8c6
5 changed files with 123 additions and 16 deletions
+1 -1
View File
@@ -102,7 +102,7 @@ const clientOptions = {
},
},
allowedUnencryptedConnectionSettings: {
rdp: ["width", "height"],
rdp: ["width", "height", "dpi"],
vnc: ["width", "height"],
telnet: ["width", "height"],
},
+6 -1
View File
@@ -96,7 +96,7 @@ const GuacamoleApp = React.forwardRef<GuacamoleAppHandle, GuacamoleAppProps>(
interface GuacamoleAppInnerProps {
hostId: number;
hostConfig: Pick<SSHHost, "connectionType">;
hostConfig: Pick<SSHHost, "connectionType" | "guacamoleConfig">;
hostName: string;
tabId?: string;
protocol?: "rdp" | "vnc" | "telnet";
@@ -213,6 +213,7 @@ const GuacamoleAppInner = React.forwardRef<
| "rdp"
| "vnc"
| "telnet";
const configuredDpi = Number(hostConfig.guacamoleConfig?.dpi);
return (
<div className="relative w-full h-full">
@@ -250,6 +251,10 @@ const GuacamoleAppInner = React.forwardRef<
token,
protocol: resolvedProtocol,
type: resolvedProtocol,
dpi:
Number.isFinite(configuredDpi) && configuredDpi > 0
? configuredDpi
: undefined,
}}
isVisible={true}
onError={(err) => setConnectionError(err)}
+38 -14
View File
@@ -17,6 +17,7 @@ import {
isPasteShortcut,
pasteTextToRemote,
} from "./guacamole-clipboard.ts";
import { getGuacamoleDisplaySize } from "./guacamole-display-size.ts";
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
@@ -129,13 +130,14 @@ export const GuacamoleDisplay = forwardRef<
): Promise<string | null> => {
try {
let token: string;
const protocol = connectionConfig.protocol ?? connectionConfig.type;
const connectionProtocol =
connectionConfig.protocol ?? connectionConfig.type;
if (connectionConfig.token) {
token = connectionConfig.token;
} else {
const data = await getGuacamoleToken({
protocol: protocol ?? "rdp",
protocol: connectionProtocol ?? "rdp",
hostname: String(connectionConfig.hostname ?? ""),
port: connectionConfig.port,
username: connectionConfig.username,
@@ -156,8 +158,13 @@ export const GuacamoleDisplay = forwardRef<
token = data.token;
}
const width = connectionConfig.width ?? containerWidth ?? 1280;
const height = connectionConfig.height ?? containerHeight ?? 720;
const displaySize = getGuacamoleDisplaySize(
connectionConfig.width ?? containerWidth ?? 1280,
connectionConfig.height ?? containerHeight ?? 720,
connectionProtocol,
window.devicePixelRatio,
connectionConfig.dpi,
);
const wsBase = buildGuacamoleWebSocketBaseUrl({
isDev,
@@ -171,9 +178,10 @@ export const GuacamoleDisplay = forwardRef<
const params = new URLSearchParams({
token,
width: String(width),
height: String(height),
width: String(displaySize.width),
height: String(displaySize.height),
});
if (displaySize.dpi) params.set("dpi", String(displaySize.dpi));
return `${wsBase}?${params.toString()}`;
} catch (error) {
const errorMessage =
@@ -434,9 +442,14 @@ export const GuacamoleDisplay = forwardRef<
onConnect?.();
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const w = Math.round(rect.width);
const h = Math.round(rect.height);
if (w > 0 && h > 0) client.sendSize(w, h);
const size = getGuacamoleDisplaySize(
rect.width,
rect.height,
protocol,
window.devicePixelRatio,
connectionConfig.dpi,
);
client.sendSize(size.width, size.height);
}
rescaleDisplay(false);
break;
@@ -515,6 +528,7 @@ export const GuacamoleDisplay = forwardRef<
rescaleDisplay,
connectionConfig.protocol,
connectionConfig.type,
connectionConfig.dpi,
t,
]);
@@ -588,10 +602,15 @@ export const GuacamoleDisplay = forwardRef<
resizeTimeoutRef.current = setTimeout(() => {
if (clientRef.current && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const w = Math.round(rect.width);
const h = Math.round(rect.height);
if (w > 0 && h > 0) {
clientRef.current.sendSize(w, h);
const size = getGuacamoleDisplaySize(
rect.width,
rect.height,
connectionConfig.protocol ?? connectionConfig.type,
window.devicePixelRatio,
connectionConfig.dpi,
);
if (rect.width > 0 && rect.height > 0) {
clientRef.current.sendSize(size.width, size.height);
rescaleDisplay(true);
}
}
@@ -603,7 +622,12 @@ export const GuacamoleDisplay = forwardRef<
return () => {
resizeObserver.disconnect();
};
}, [rescaleDisplay]);
}, [
connectionConfig.dpi,
connectionConfig.protocol,
connectionConfig.type,
rescaleDisplay,
]);
const syncClipboard = useCallback(() => {
const client = clientRef.current;
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { getGuacamoleDisplaySize } from "./guacamole-display-size";
describe("getGuacamoleDisplaySize", () => {
it("requests native pixels and matching DPI for HiDPI RDP", () => {
expect(getGuacamoleDisplaySize(1280, 720, "rdp", 2)).toEqual({
width: 2560,
height: 1440,
dpi: 192,
pixelRatio: 2,
});
});
it("scales a configured RDP DPI with the device pixel ratio", () => {
expect(getGuacamoleDisplaySize(1000, 600, "rdp", 1.5, 120)).toEqual({
width: 1500,
height: 900,
dpi: 180,
pixelRatio: 1.5,
});
});
it("leaves non-RDP protocols at CSS-pixel dimensions", () => {
expect(getGuacamoleDisplaySize(1280, 720, "vnc", 2)).toEqual({
width: 1280,
height: 720,
pixelRatio: 1,
});
});
it("caps pathological pixel ratios to protect remote session size", () => {
expect(getGuacamoleDisplaySize(400, 800, "rdp", 4)).toEqual({
width: 1200,
height: 2400,
dpi: 288,
pixelRatio: 3,
});
});
});
@@ -0,0 +1,39 @@
const DEFAULT_RDP_DPI = 96;
const MAX_DEVICE_PIXEL_RATIO = 3;
export interface GuacamoleDisplaySize {
width: number;
height: number;
dpi?: number;
pixelRatio: number;
}
export function getGuacamoleDisplaySize(
cssWidth: number,
cssHeight: number,
protocol: string | undefined,
devicePixelRatio: number,
configuredDpi?: number,
): GuacamoleDisplaySize {
const isRdp = protocol === "rdp";
const pixelRatio = isRdp
? Math.min(
MAX_DEVICE_PIXEL_RATIO,
Math.max(1, Number.isFinite(devicePixelRatio) ? devicePixelRatio : 1),
)
: 1;
const size = {
width: Math.max(1, Math.round(cssWidth * pixelRatio)),
height: Math.max(1, Math.round(cssHeight * pixelRatio)),
pixelRatio,
};
if (!isRdp) return size;
const baseDpi =
configuredDpi && Number.isFinite(configuredDpi) && configuredDpi > 0
? configuredDpi
: DEFAULT_RDP_DPI;
return { ...size, dpi: Math.round(baseDpi * pixelRatio) };
}