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: { allowedUnencryptedConnectionSettings: {
rdp: ["width", "height"], rdp: ["width", "height", "dpi"],
vnc: ["width", "height"], vnc: ["width", "height"],
telnet: ["width", "height"], telnet: ["width", "height"],
}, },
+6 -1
View File
@@ -96,7 +96,7 @@ const GuacamoleApp = React.forwardRef<GuacamoleAppHandle, GuacamoleAppProps>(
interface GuacamoleAppInnerProps { interface GuacamoleAppInnerProps {
hostId: number; hostId: number;
hostConfig: Pick<SSHHost, "connectionType">; hostConfig: Pick<SSHHost, "connectionType" | "guacamoleConfig">;
hostName: string; hostName: string;
tabId?: string; tabId?: string;
protocol?: "rdp" | "vnc" | "telnet"; protocol?: "rdp" | "vnc" | "telnet";
@@ -213,6 +213,7 @@ const GuacamoleAppInner = React.forwardRef<
| "rdp" | "rdp"
| "vnc" | "vnc"
| "telnet"; | "telnet";
const configuredDpi = Number(hostConfig.guacamoleConfig?.dpi);
return ( return (
<div className="relative w-full h-full"> <div className="relative w-full h-full">
@@ -250,6 +251,10 @@ const GuacamoleAppInner = React.forwardRef<
token, token,
protocol: resolvedProtocol, protocol: resolvedProtocol,
type: resolvedProtocol, type: resolvedProtocol,
dpi:
Number.isFinite(configuredDpi) && configuredDpi > 0
? configuredDpi
: undefined,
}} }}
isVisible={true} isVisible={true}
onError={(err) => setConnectionError(err)} onError={(err) => setConnectionError(err)}
+38 -14
View File
@@ -17,6 +17,7 @@ import {
isPasteShortcut, isPasteShortcut,
pasteTextToRemote, pasteTextToRemote,
} from "./guacamole-clipboard.ts"; } from "./guacamole-clipboard.ts";
import { getGuacamoleDisplaySize } from "./guacamole-display-size.ts";
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet"; export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
@@ -129,13 +130,14 @@ export const GuacamoleDisplay = forwardRef<
): Promise<string | null> => { ): Promise<string | null> => {
try { try {
let token: string; let token: string;
const protocol = connectionConfig.protocol ?? connectionConfig.type; const connectionProtocol =
connectionConfig.protocol ?? connectionConfig.type;
if (connectionConfig.token) { if (connectionConfig.token) {
token = connectionConfig.token; token = connectionConfig.token;
} else { } else {
const data = await getGuacamoleToken({ const data = await getGuacamoleToken({
protocol: protocol ?? "rdp", protocol: connectionProtocol ?? "rdp",
hostname: String(connectionConfig.hostname ?? ""), hostname: String(connectionConfig.hostname ?? ""),
port: connectionConfig.port, port: connectionConfig.port,
username: connectionConfig.username, username: connectionConfig.username,
@@ -156,8 +158,13 @@ export const GuacamoleDisplay = forwardRef<
token = data.token; token = data.token;
} }
const width = connectionConfig.width ?? containerWidth ?? 1280; const displaySize = getGuacamoleDisplaySize(
const height = connectionConfig.height ?? containerHeight ?? 720; connectionConfig.width ?? containerWidth ?? 1280,
connectionConfig.height ?? containerHeight ?? 720,
connectionProtocol,
window.devicePixelRatio,
connectionConfig.dpi,
);
const wsBase = buildGuacamoleWebSocketBaseUrl({ const wsBase = buildGuacamoleWebSocketBaseUrl({
isDev, isDev,
@@ -171,9 +178,10 @@ export const GuacamoleDisplay = forwardRef<
const params = new URLSearchParams({ const params = new URLSearchParams({
token, token,
width: String(width), width: String(displaySize.width),
height: String(height), height: String(displaySize.height),
}); });
if (displaySize.dpi) params.set("dpi", String(displaySize.dpi));
return `${wsBase}?${params.toString()}`; return `${wsBase}?${params.toString()}`;
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
@@ -434,9 +442,14 @@ export const GuacamoleDisplay = forwardRef<
onConnect?.(); onConnect?.();
if (containerRef.current) { if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect(); const rect = containerRef.current.getBoundingClientRect();
const w = Math.round(rect.width); const size = getGuacamoleDisplaySize(
const h = Math.round(rect.height); rect.width,
if (w > 0 && h > 0) client.sendSize(w, h); rect.height,
protocol,
window.devicePixelRatio,
connectionConfig.dpi,
);
client.sendSize(size.width, size.height);
} }
rescaleDisplay(false); rescaleDisplay(false);
break; break;
@@ -515,6 +528,7 @@ export const GuacamoleDisplay = forwardRef<
rescaleDisplay, rescaleDisplay,
connectionConfig.protocol, connectionConfig.protocol,
connectionConfig.type, connectionConfig.type,
connectionConfig.dpi,
t, t,
]); ]);
@@ -588,10 +602,15 @@ export const GuacamoleDisplay = forwardRef<
resizeTimeoutRef.current = setTimeout(() => { resizeTimeoutRef.current = setTimeout(() => {
if (clientRef.current && containerRef.current) { if (clientRef.current && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect(); const rect = containerRef.current.getBoundingClientRect();
const w = Math.round(rect.width); const size = getGuacamoleDisplaySize(
const h = Math.round(rect.height); rect.width,
if (w > 0 && h > 0) { rect.height,
clientRef.current.sendSize(w, h); connectionConfig.protocol ?? connectionConfig.type,
window.devicePixelRatio,
connectionConfig.dpi,
);
if (rect.width > 0 && rect.height > 0) {
clientRef.current.sendSize(size.width, size.height);
rescaleDisplay(true); rescaleDisplay(true);
} }
} }
@@ -603,7 +622,12 @@ export const GuacamoleDisplay = forwardRef<
return () => { return () => {
resizeObserver.disconnect(); resizeObserver.disconnect();
}; };
}, [rescaleDisplay]); }, [
connectionConfig.dpi,
connectionConfig.protocol,
connectionConfig.type,
rescaleDisplay,
]);
const syncClipboard = useCallback(() => { const syncClipboard = useCallback(() => {
const client = clientRef.current; 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) };
}