pngjs icon indicating copy to clipboard operation
pngjs copied to clipboard

Make png js browserified work with fetch streams in the browser

Open sniggyfigbat opened this issue 5 years ago • 10 comments

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 fetchmethod 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?

sniggyfigbat avatar Mar 31 '19 16:03 sniggyfigbat

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.

sniggyfigbat avatar Apr 02 '19 19:04 sniggyfigbat

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.

sniggyfigbat avatar Apr 02 '19 19:04 sniggyfigbat

Sorry no idea, I’ve never needed to use this in the browser. I’ve renamed and will keep it open.

lukeapage avatar Apr 02 '19 23:04 lukeapage

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.

aksharj avatar Jun 15 '20 20:06 aksharj

same error ~

schacker avatar Aug 18 '20 03:08 schacker

@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 avatar Sep 23 '21 05:09 delebash

@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!

sniggyfigbat avatar Sep 23 '21 09:09 sniggyfigbat

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.

delebash avatar Sep 23 '21 11:09 delebash

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);

codyzu avatar Mar 20 '23 09:03 codyzu

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. 🤔

codyzu avatar Mar 20 '23 09:03 codyzu