Skip to content

Instantly share code, notes, and snippets.

@gasman
Created April 30, 2012 18:08
Show Gist options
  • Select an option

  • Save gasman/2560551 to your computer and use it in GitHub Desktop.

Select an option

Save gasman/2560551 to your computer and use it in GitHub Desktop.
pnginator: pack Javascript into a self-extracting PNG
#!/usr/bin/env ruby -w
# pnginator.rb: pack a .js file into a PNG image with an HTML payload;
# when saved with an .html extension and opened in a browser, the HTML extracts and executes
# the javascript.
# Usage: ruby pnginator.rb input.js output.png.html
# By Gasman <http://matt.west.co.tt/>
# from an original idea by Daeken: http://daeken.com/superpacking-js-demos
MAX_WIDTH = 4096
USE_PNGOUT = true
require 'zlib'
require 'tempfile'
input_filename, output_filename = ARGV
f = File.open(input_filename, 'rb')
js = f.read
f.close
if js.length < MAX_WIDTH
# js fits onto one pixel line
js += "\x00"
scanlines = [js]
width = js.length
height = 1
# Daeken's single-pixel-row bootstrap (requires js string to be reversed)
# (edit by Gasman: change eval to (1,eval) to force global evaluation and avoid massive slowdown)
# html = "<canvas id=q><img onload=for(p=q.width=#{width},(c=q.getContext('2d')).drawImage(this,0,e='');p;)e+=String.fromCharCode(c.getImageData(--p,0,1,1).data[0]);(1,eval)(e) src=#>"
# p01's single-pixel-row bootstrap (requires an 0x00 end marker on the js string)
# (edit by Gasman: move drawImage out of getImageData params (it returns undef, which is invalid) and change eval to (1,eval) to force global evaluation)
html = "<canvas id=c><img onload=with(c.getContext('2d'))for(p=e='';drawImage(this,p--,0),t=getImageData(0,0,1,1).data[0];)e+=String.fromCharCode(t);(1,eval)(e) src=#>"
else
js = "\x00" + js
width = MAX_WIDTH
# split js into scanlines of 'width' pixels; pad the last one with whitespace
scanlines = js.scan(/.{1,#{width}}/m).collect{|line| line.ljust(width, "\x00")}
height = scanlines.length
# p01's multiple-pixel-row bootstrap (requires a dummy first byte on the js string)
# (edit by Gasman: set explicit canvas width to support widths above 300; move drawImage out of getImageData params; change eval to (1,eval) to force global evaluation)
html = "<canvas id=c><img onload=for(w=c.width=#{width},a=c.getContext('2d'),a.drawImage(this,p=0,0),e='',d=a.getImageData(0,0,w,#{height}).data;t=d[p+=4];)e+=String.fromCharCode(t);(1,eval)(e) src=#>"
end
# prepend each scanline with 0x00 to indicate 'no filtering', then concat into one string
image_data = scanlines.collect{|line| "\x00" + line}.join
idat_chunk = Zlib::Deflate.deflate(image_data, 9) # 9 = maximum compression
def png_chunk(signature, data)
[data.length, signature, data, Zlib::crc32(signature + data)].pack("NA4A*N")
end
if USE_PNGOUT
# Create a valid (no format hacks) .png file to pass to pngout
f = Tempfile.open(['pnginator', '.png'])
begin
f.write("\x89PNG\x0d\x0a\x1a\x0a") # PNG file header
f.write(png_chunk("IHDR", [width, height, 8, 0, 0, 0, 0].pack("NNccccc")))
f.write(png_chunk("IDAT", idat_chunk))
f.write(png_chunk("IEND", ''))
f.close
system("pngout", f.path, "-c0", "-y")
# read file back and extract the IDAT chunk
f.open
f.read(8)
while !f.eof?
length, signature = f.read(8).unpack("NA4")
data = f.read(length)
crc = f.read(4)
if signature == "IDAT"
idat_chunk = data
break
end
end
ensure
f.close
f.unlink
end
end
File.open(output_filename, 'wb') do |f|
f.write("\x89PNG\x0d\x0a\x1a\x0a") # PNG file header
f.write(png_chunk("IHDR", [width, height, 8, 0, 0, 0, 0].pack("NNccccc")))
# a custom chunk containing the HTML payload; stated chunk length is 4 less than the actual length,
# leaving the final 4 bytes to take the place of the checksum
f.write([html.length - 4, "jawh", html].pack("NA4A*"))
# can safely omit the checksum of the IDAT chunk
# f.write([idat_chunk.length, "IDAT", idat_chunk, Zlib::crc32("IDAT" + idat_chunk)].pack("NA4A*N"))
f.write([idat_chunk.length, "IDAT", idat_chunk].pack("NA4A*"))
# can safely omit the IEND chunk
# f.write([0, "IEND", "", Zlib::crc32("IEND")].pack("NA4A*N"))
end
@simonwhitaker

Copy link
Copy Markdown

My dream of having my jQuery file smaller than my favicon just came a step closed. :-)

@matbeedotcom

Copy link
Copy Markdown

Wow, how is this even used? in an tag?

@matbeedotcom

Copy link
Copy Markdown

IMG Tag.

@NyonCat

NyonCat commented Feb 15, 2014

Copy link
Copy Markdown

why is canvas not created from within onload?

@hckr

hckr commented Dec 11, 2017

Copy link
Copy Markdown

@NyonCat It would take more bytes.

@shu1

shu1 commented Oct 23, 2018

Copy link
Copy Markdown

This works on Chrome and Firefox for me, and it's great! But on Safari or Edge, it just shows the png header text on the page, so I assumed it doesn't work on those browsers.

However, a friend said it works on Edge on his PC, and showed me a screenshot. Does anyone know if it's supposed to work or not on Edge, and Safari?

@LinuxRocks2000

Copy link
Copy Markdown

It didn't work with alert('hello world') in the input.js. Is there a specific way I have to write the js file?

@gasman

gasman commented Jul 10, 2019

Copy link
Copy Markdown
Author

@LinuxRocks2000 What browser did you try it in? It works for me in Firefox, but not Chrome - it seems that Chrome has tightened up its security restrictions since I created this, and now it will only work if you serve it over HTTP rather than opening the file locally.

@AmitSivaLevi

AmitSivaLevi commented Sep 19, 2019

Copy link
Copy Markdown

@gasman Thanks for sharing your code,Is it still working? Tried with an alert message but not working.

@gasman

gasman commented Sep 19, 2019

Copy link
Copy Markdown
Author

@AmitSivaLevi See my comment above... have just retested, and the same thing applies on current versions. Firefox works completely, Chrome only works if you serve the file over HTTP (or, possibly, if you launch it with the command-line option --allow-file-access-from-files).

I've now put up a test copy, so you can see what's supposed to happen: https://gasman.github.io/pnginator/test.png.html . Hopefully this should work on both Chrome and Firefox; if you save the page and launch it locally, you'll probably find it only works on Firefox. If this copy works but the one you built doesn't, then something has gone wrong in the build process...

@DavidBuchanan314

Copy link
Copy Markdown

Fun fact, you can use the same trick with .webp for lower header overhead and better compression ratios

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment