Skip to content

Instantly share code, notes, and snippets.

@djhohnstein
Created July 26, 2022 20:31
Show Gist options
  • Save djhohnstein/df2915155f4eb8e1747870e2ef2fc8b6 to your computer and use it in GitHub Desktop.
Save djhohnstein/df2915155f4eb8e1747870e2ef2fc8b6 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment