3

In my application I am trying to save an arbitrary part of a rendered HTML canvas to an image file. In my Javascript I call ctx.getImageData(x, y, w, h) and pass the resulting object to my macruby code (though if you know a solution in objc I am also very interested).

There I'm trying to create a NSBitmapImageRep object so that I can then save to an image format the user desires.

This is my code so far (the function gets a WebScriptObject as it's argument):

def setimagedata(d)
    w =  d.valueForKey("width").to_i
    h =  d.valueForKey("height").to_i
    data = Pointer.new(:char, d.valueForKey("data").valueForKey("length").to_i)
    d.valueForKey("data").valueForKey("length").to_i.times do |i|
        data[i] = d.valueForKey("data").webScriptValueAtIndex(i).to_i
    end
    puts "data complete" # get's called
    @exported_image = NSBitmapImageRep.alloc.initWithBitmapDataPlanes(data,
        pixelsWide: w, pixelsHigh:h, bitsPerSample: 32,
        samplesPerPixel: 4, hasAlpha: true, isPlanar: false, 
        colorSpaceName: NSCalibratedRGBColorSpace, 
        bitmapFormat: NSAlphaNonpremultipliedBitmapFormat, 
        bytesPerRow: 0, bitsPerPixel: 0)
    puts "done" # doesn't get called
end

The code doesn't seem to get through the initWithBitmapDataPlanes function but gives no error.

My question is: what am I doing wrong? Is this approach reasonable (if not, what would be better?).

Edit

Using Phrogz' answer below I got an intermediate solution: I use another canvas, getImageData, putImageData and toDataURL to get a data url of the region needed. In my setimagedata I simply save the data url and my dataOfType: error: method looks like this:

def dataOfType(type, error:outError)
    workspace = NSWorkspace.sharedWorkspace
    if workspace.type(type, conformsToType: "public.image")
        @data_url[ /(?<=,).+/ ].unpack("m").first
    end
end

The missing secrete sauce it this ugly hax:

class NSString
    def writeToURL(url, options: opts, error: error)
        File.open(url.path, "w") {|f| f << self }
    end
end

It leverages duck typing in Cocoa and defines a selector normally on NSData to write itself to a file.

This seems to work so far and I'm happy I reached a solution. However I would still like to see a solution using NSBitmapImageRep. The next feature I'm implementing is exporting to video and I believe I will need the finer control provided by this class.

Jakub Hampl
  • 39,863
  • 10
  • 77
  • 106

1 Answers1

3

Instead of using getImageData, I would suggest using Canvas.toDataURL. This will give you a binary PNG (or JPEG) that happens to be base64 encoded. Decode the base64 and you'll have a file you can save for the user, or process to transcode to another format.

Edit: I originally deleted my answer because I realized that you wanted to serialize a sub-region of the canvas. Then I realized that if this helps you could instead do this:

  1. Create a new canvas of the size of the subregion.
  2. Use context.drawImage() to copy the sub-region from the original to the new canvas.
  3. Call Canvas.toDataURL on the new canvas to get the base64-serialized data.

To decode the base64 data, you can use the base64 ruby library's decode64 method. Note that the implementation of this method is exceedingly simple, however, and you could just inline it:

def decode64(str)
  str.unpack("m").first
end

Edit 2: For example, given the results of a toDataURL call placed in a Ruby string, simply:

require 'base64'
data_only = data_url[ /(?<=,).+/ ] # Find everything after the first comma
File.open( 'foo.png', 'w' ){ |f| f << Base64.decode64( data_only ) }
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • How would I go about decoding? – Jakub Hampl Jan 09 '11 at 15:46
  • I realized all this, but the part where I got stuck was getting from this unpacked String the NSData that I need for saving (currently I'm using the NSDocument's `dataOfType(type, error:outError)`. For me the problem isn't necessarily the transport of the data from javascript but turning it into an image file. – Jakub Hampl Jan 09 '11 at 21:05
  • @JakubHampl I'm not sure what part you're getting stuck on, but I've updated the answer again with the pure Ruby way to write the data URL to a valid file. I've validated locally that this works. – Phrogz Jan 10 '11 at 00:53
  • The problem is that I need it as `NSData` or I need a way how to deal with the NSDocument's save process - the documentation seems to favour returning appropriate NSData from `dataOfType:error:`. – Jakub Hampl Jan 10 '11 at 20:13
  • @JakubHampl So you're asking how to take a binary 'string' in MacRuby and convert it to NSData? You might get better results by asking that question specifically, on its own. Although your goal is still the same, we seem to be venturing far from your original question here. – Phrogz Jan 10 '11 at 20:45