Skip to content

Commit 0359b93

Browse files
authored
Improve PNG decompression implementation (#980)
* Add PNG integration test, ensure jsdom detected as Node * Improve PNG decompression implementation Use fast-png to unify the implementation between Node.js and browser, and unbork Web Worker support (fixes #859) * Temporarily disable test, delete orphaned type file
1 parent 331d20d commit 0359b93

File tree

7 files changed

+107
-114
lines changed

7 files changed

+107
-114
lines changed

package-lock.json

Lines changed: 60 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@
4141
"dependencies": {
4242
"@xmldom/xmldom": "^0.9.8",
4343
"bson": "^7.0.0",
44+
"buffer": "^6.0.3",
4445
"cbor-js": "^0.1.0",
4546
"eventemitter3": "^5.0.1",
46-
"pngparse": "^2.0.0",
47+
"fast-png": "^7.0.1",
4748
"uuid": "^13.0.0",
4849
"ws": "^8.0.0"
4950
},

src/core/SocketAdapter.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "../types/protocol.js";
1212
import { deserialize } from "bson";
1313
import type { WebSocket as WsWebSocket } from "ws";
14+
import decompressPng from "../util/decompressPng.js";
1415

1516
export type RequiredSocketInterface = Pick<
1617
WebSocket | RTCDataChannel | WsWebSocket,
@@ -157,27 +158,11 @@ export default class SocketAdapter {
157158
callback: (message: RosbridgeMessage) => void,
158159
) {
159160
if (isRosbridgePngMessage(message)) {
160-
const pngCallback = (data: unknown) => {
161-
if (isRosbridgeMessage(data)) {
162-
callback(data);
163-
} else {
164-
throw new Error("Decompressed PNG data was invalid!");
165-
}
166-
};
167-
// If in Node.js..
168-
if (typeof window === "undefined") {
169-
import("../util/decompressPng.js")
170-
.then(({ default: decompressPng }) => {
171-
decompressPng(message.data, pngCallback);
172-
})
173-
.catch(console.error);
161+
const decoded = decompressPng(message.data);
162+
if (isRosbridgeMessage(decoded)) {
163+
callback(decoded);
174164
} else {
175-
// if in browser..
176-
import("../util/shim/decompressPng.js")
177-
.then(({ default: decompressPng }) => {
178-
decompressPng(message.data, pngCallback);
179-
})
180-
.catch(console.error);
165+
throw new Error("Received invalid message in PNG data!");
181166
}
182167
} else {
183168
callback(message);

src/types/pngparse.d.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/util/decompressPng.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,34 @@
33
* @author Ramon Wijnands - rayman747@hotmail.com
44
*/
55

6-
import pngparse from "pngparse";
6+
import { decode } from "fast-png";
7+
import { Buffer } from "buffer";
8+
9+
const textDecoder = new TextDecoder();
710

811
/**
912
* If a message was compressed as a PNG image (a compression hack since
1013
* gzipping over WebSockets * is not supported yet), this function decodes
1114
* the "image" as a Base64 string.
1215
*
1316
* @param data - An object containing the PNG data.
14-
* @param callback - Function with the following params:
1517
*/
16-
export default function decompressPng(
17-
data: string,
18-
callback: (data: unknown) => void,
19-
) {
18+
export default function decompressPng(data: string): unknown {
2019
const buffer = Buffer.from(data, "base64");
2120

22-
pngparse.parse(buffer, function (err, data) {
23-
if (err || !(data instanceof Object) || !("data" in data)) {
24-
throw new Error("Cannot process PNG encoded message ");
25-
} else {
26-
const jsonData = String(data.data);
27-
callback(JSON.parse(jsonData));
28-
}
29-
});
21+
const decoded = tryDecodeBuffer(buffer);
22+
23+
try {
24+
return JSON.parse(textDecoder.decode(decoded.data));
25+
} catch (error) {
26+
throw new Error("Error parsing PNG JSON contents", { cause: error });
27+
}
28+
}
29+
30+
function tryDecodeBuffer(buffer: Buffer) {
31+
try {
32+
return decode(buffer);
33+
} catch (error) {
34+
throw new Error("Error decoding buffer", { cause: error });
35+
}
3036
}

src/util/shim/decompressPng.ts

Lines changed: 0 additions & 61 deletions
This file was deleted.

test/examples/topic-listener.example.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect } from "vitest";
1+
import { describe, it, expect, vi } from "vitest";
22
import * as ROSLIB from "../../src/RosLib.js";
33

44
const ros = new ROSLIB.Ros({
@@ -45,4 +45,22 @@ describe("Topics Example", function () {
4545

4646
topic.on("unsubscribe", done);
4747
}));
48-
}, 1000);
48+
49+
// TODO: reenable when rosbridge is fixed in ROS 2
50+
it.skip("Listening to a PNG-compressed topic", async () => {
51+
const topic = ros.Topic<{ data: string }>({
52+
name: "/png_test",
53+
messageType: "std_msgs/String",
54+
compression: "png",
55+
});
56+
const callback = vi.fn();
57+
topic.subscribe(callback);
58+
59+
topic.publish({ data: "some message that will be PNG-compressed" });
60+
await vi.waitFor(() => {
61+
expect(callback).toHaveBeenCalledWith({
62+
data: "some message that will be PNG-compressed",
63+
});
64+
});
65+
});
66+
});

0 commit comments

Comments
 (0)