pngjs
pngjs copied to clipboard
Make png js browserified work with fetch streams in the browser
I'm attempting to use PNGJS to read and write raw pixel data for a browser-based game. I'm using the browserified version of the library (const PNG = require('pngjs/browser').PNG;
), and I'm simply attempting to populate a PNG object from a served .png file. As far as I can tell, the library intends me to do this from a stream, and thus I'm using the fetch
method built into the browser, which returns a ReadableStream.
The appropriate excerpt from my code:
let levelStream = new PNG({ filterType: 4 });
let fetchLevelProm = fetch('./levels/TestLevel.png').then(
(response) => {
if (response.status !== 200) {
console.log('Issue loading level file from server. Status Code: ' + response.status);
return;
}
return response;
}
)
.then(response => response.body)
.then(body => body.pipeTo(levelStream))
.then(promise => GP.loadLevel(levelStream))
.catch(function(err) {
console.log('Fetch Error: ', err);
});
This looks like it should work; pipeTo
requires a WriteableStream
, which the PNG object seems to be(?). Unfortunately, practice does not support theory, as testing this in Chrome throws an error at the pipeTo
statement:
TypeError: Failed to execute 'pipeTo' on 'ReadableStream': Illegal invocation
Which isn't very explicit, and leaves me rather flummoxed.
Given my state of confusion, I just wanted to check that I'm not completely misunderstanding something, and I'm not busily using two different non-interactive APIs with exactly the same name or whatever. This is something which should work, right?
Answer: No, you can't, fetch/browser streams are a different API/system to the Node/fs ones.
There's probably a clever solution to this, and it would also nice if someone added a good solution to the library.
Shite Workaround: Don't bother with streams, just parse it:
For posterity; You can actually use the async parse
functionality perfectly effectively for this. Here's a bit of sample code where I use it to read a fetch
result:
let levelStream;
let fetchLevelProm = fetch('./levels/TestLevel.png').then(
(response) => {
if (response.status !== 200) {
console.log('Issue loading level file from server. Status Code: ' + response.status);
return;
}
return response;
}
)
.then(response => response.arrayBuffer())
.then(buffer => {
levelStream = new PNG({ filterType:4 }).parse( buffer, function(error, data) {
if (error != null) { console.log('ERROR: In level image read; ' + error); }
else { GP.loadLevel(levelStream); }
});
})
.catch(function(err) {
console.log('Fetch Error: ', err);
});
Don't be confused by it being called 'levelData', I'm just storing data in a perfectly normal PNG.
PS: Not currently closing, as it still seems a major shortcoming that should probably be addressed. If any contributors see this, feel free to close as a 'won't fix' if you think it's not worth the effort to build in support.
Sorry no idea, I’ve never needed to use this in the browser. I’ve renamed and will keep it open.
Hi,
Is there any update on this, I am facing a similar issue. I want to pass a fetch
api readable stream to node.js writeable stream.
Any help on how to workaround this would be really great, Thanks.
same error ~
@sniggyfigbat Your solution works great. How do you write to a file?
Example:
let writable = await file_handle.createWritable();
let png = new PNG({})
I parse the PNG with the arraybuffer, all works
png.pipeTo(writable)
Then I get error png.pipeTo is not a function
FYI:
If I try await response.body.pipeTo(writable);
it works and you can see that response.body is a readable stream in the debug console. However, the png file above shows the data but it is not a readable stream. Do I just convert it to a readable stream. I thought pngjs produced read and write streams but maybe the streams are different in the browser. If I need to convert it to a browser readable stream how do I do that?
Cheers!
@delebash I've no idea, I'm afraid. I haven't touched the project I needed this for in two years, and I don't remember much in the way of details. Sorry!
TY. I have made some progress. I am now using writable.write(data)
instead of pipeTo
and it works sort of. Without passing response.arrayBuffer()
to PNG I get the correct but unmodified file. When I parse response.arrayBuffer
using PNG it writes a 1kb corrupt file. May guess is that I need PNG to return an arrayBuffer but I am not sure how to do that.
I came to the same conclusion as @sniggyfigbat , but using async/await. Obviously if streams worked, that could potentially be more efficient since it could leverage streams to parse large files. Oh well.
Here's my async/await version to decode a fetched image:
async function decode(response) {
const buffer = await response.arrayBuffer();
const png = await new Promise((resolve, reject) =>
new PNG().parse(buffer, (err, data) =>
err ? reject(err) : resolve(data)
)
);
}
// ...
const fetchedImage = await fetch('./a.png');
const decodedImage = await decodeImage(fetchedImage);
console.log('decoded', decodedImage);
No, you can't, fetch/browser streams are a different API/system to the Node/fs ones.
Interestingly node.js has supported web streams since version 16.
The fix in the library probably revolves around replacing the streams it uses with web streams. 🤔