shutterbug 0.2.5 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +8 -8
  2. data/.travis.yml +3 -3
  3. data/README.md +6 -0
  4. data/bower.json +1 -1
  5. data/demo/The_Scream.jpg +0 -0
  6. data/demo/canvas_example.html +36 -0
  7. data/demo/index.html +1 -0
  8. data/lib/shutterbug.rb +1 -1
  9. data/lib/shutterbug/cache_manager.rb +2 -2
  10. data/lib/shutterbug/configuration.rb +4 -8
  11. data/lib/shutterbug/handlers.rb +5 -4
  12. data/lib/shutterbug/handlers/convert_handler.rb +38 -16
  13. data/lib/shutterbug/handlers/direct_upload_handler.rb +30 -0
  14. data/lib/shutterbug/handlers/file_handler.rb +30 -0
  15. data/lib/shutterbug/handlers/js_file_handler.rb +13 -10
  16. data/lib/shutterbug/handlers/shutterbug.js +123 -32
  17. data/lib/shutterbug/phantom_job.rb +11 -10
  18. data/lib/shutterbug/rackapp.rb +15 -18
  19. data/lib/shutterbug/rasterize.js +4 -4
  20. data/lib/shutterbug/storage.rb +1 -1
  21. data/lib/shutterbug/storage/file_storage.rb +21 -8
  22. data/lib/shutterbug/storage/s3_storage.rb +26 -31
  23. data/shutterbug.gemspec +0 -1
  24. data/spec/shared_examples_for_handlers.rb +13 -9
  25. data/spec/shared_examples_for_storage.rb +14 -10
  26. data/spec/shutterbug/configuration_spec.rb +1 -11
  27. data/spec/shutterbug/convert_handler_spec.rb +5 -4
  28. data/spec/shutterbug/direct_upload_handler_spec.rb +7 -0
  29. data/spec/shutterbug/file_handler_spec.rb +7 -0
  30. data/spec/shutterbug/file_storage_spec.rb +2 -1
  31. data/spec/shutterbug/js_file_handler_spec.rb +2 -1
  32. data/spec/shutterbug/rackapp_spec.rb +14 -28
  33. data/spec/shutterbug/s3_storage_spec.rb +1 -1
  34. metadata +10 -12
  35. data/lib/shutterbug/handlers/file_handlers.rb +0 -9
  36. data/lib/shutterbug/handlers/file_handlers/base.rb +0 -39
  37. data/lib/shutterbug/handlers/file_handlers/html_file.rb +0 -22
  38. data/lib/shutterbug/handlers/file_handlers/png_file.rb +0 -23
  39. data/spec/shared_examples_for_file_handlers.rb +0 -16
  40. data/spec/shutterbug/html_file_handler_spec.rb +0 -10
  41. data/spec/shutterbug/png_file_handler_spec.rb +0 -10
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NjVmZDdiYjY0NmUzYzc1YzNlN2QwMGI5MmEwOTk1MzNjNWZkNjM2NA==
4
+ MjY4NTQ1OTA1ZjFjZmFjZGUzNjVhMWZmNjc4ODM4N2VjZTVkMDFiYQ==
5
5
  data.tar.gz: !binary |-
6
- N2I1N2ZlYzM5ZjAzOGNkMTA2ZGY4ZTE0N2VlYTQ3NmUyNjUzMzk5MQ==
6
+ NzhmMTc5NGEzNzIzYjNjM2M1ZmIzM2U3NGJlMDhjYTgyYzRjNDZhNg==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- MmNjZWJlMmI1ZTI1OWJkMzc5MWVhZjhiZWQxYTk0MjNlMTIzNWMzYjk5YzI2
10
- ZDdkOTExNmQ0NzBkYzFkN2UxOWY4MTAxNTJmMjY3YTc0OWZkNGUzOWYwMDY5
11
- NTY5Zjc4YmUzNTEzZjllZmE2NDE1YTFmZjllNTllMTc0YmYzY2I=
9
+ NmJkMjRiYjMyYjZhZjAyZmUyYTUxOTc3ZThiNjZlMWVkNWUwMjFmYTNlNTBi
10
+ MjcyM2EyZTE5MWZmNjgxMDdlM2UxZDRjOWRjYWI1NjFlMmUzNzExMWM4ZjRk
11
+ YmU5YWI5Y2YxY2MyYTkyOTE2YzdhMDAwZDBjYWE4M2RkZDliY2U=
12
12
  data.tar.gz: !binary |-
13
- M2E0OTFkZTlkN2ZmNmU1NjdiZjlmYzY5ZjE4YmI0NjRmYzQ2ZDU4MjgyMWQ4
14
- ZDhjY2QzNmMxNjM0YjVjMzc1M2MzMWYzNDA1NjUwODdhZGQyNGE2NzM4YTg5
15
- YmZkZmI3MWQwYTQyODgxOTYyNmRiMGU5Yzc0YTVjNTQwYTMzMTQ=
13
+ ZDFmZmM5ZjZiZjVjYjI4ODk0OTdlZmQxZmRiZmY5OTVlNjhmYTAzZjE4MzYz
14
+ ZWU1OTk4YjhkMmMwNjcxNmNhOWVhYzNjMzZkZGMyMWNhMTA2YjMzMjJiZDVh
15
+ ZWRlMzE0YzcyZmRjNTUyZGZlNzA5NTFlM2VhYzYwMzU2NmYxMmM=
data/.travis.yml CHANGED
@@ -1,9 +1,9 @@
1
1
  language: ruby
2
2
  rvm:
3
- - "1.9.2"
3
+ # - "1.9.2"
4
4
  - "1.9.3"
5
5
  - "2.0.0"
6
- - jruby-19mode # JRuby in 1.9 mode
7
- - rbx-19mode
6
+ # - jruby-19mode # JRuby in 1.9 mode
7
+ # - rbx-19mode
8
8
  # uncomment this line if your project needs to run something other than `rake`:
9
9
  # script: bundle exec rspec spec
data/README.md CHANGED
@@ -179,6 +179,12 @@ And a Procfile which looks like this:
179
179
 
180
180
  ## Changes ##
181
181
 
182
+ * December 4, 2014 – v 0.4.3
183
+ * Added support of various image formats and quality settings.
184
+
185
+ * December 2, 2014 – v 0.3.0
186
+ * Improved canvas snapshot - data is uploaded directly to S3 from the browser (no PhantomJS rendering).
187
+
182
188
  * November 12, 2014 – v 0.2.5
183
189
  * Added setFailureCallback to shutterbug.js for gracefully handling ajax failures in some custom way.
184
190
  * Updated CORS configuration in config.ru to allow [:options] requests.
data/bower.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shutterbug",
3
- "version": "0.2.5",
3
+ "version": "0.4.3",
4
4
  "homepage": "https://github.com/concord-consortium/shutterbug",
5
5
  "authors": [
6
6
  "Noah Paessel <npaessel@concord.org>"
Binary file
@@ -0,0 +1,36 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>demo</title>
5
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
6
+ <script type="text/javascript" src="/shutterbug/shutterbug.js"></script>
7
+ <link rel="stylesheet" type="text/css" href="main.css">
8
+ </head>
9
+
10
+ <body>
11
+ <div>
12
+ <canvas id="src" width="350" height="350"></canvas>
13
+ </div>
14
+ <button class="shutterbug" data-dst="#dst">Snapshot</button>
15
+ <div id="dst"></div>
16
+ </body>
17
+ <script type="text/javascript">
18
+ $(document).ready(function(e) {
19
+ var img = new Image();
20
+ img.onload = function () {
21
+ var ctx = $("canvas")[0].getContext("2d");
22
+ ctx.fillStyle = "green";
23
+ ctx.fillRect(0, 0, 350, 350);
24
+ ctx.fillStyle = "orange";
25
+ ctx.fillRect(10, 10, 330, 330);
26
+ ctx.drawImage(img, 20, 20, 310, 310);
27
+ };
28
+ img.src = "The_Scream.jpg";
29
+
30
+ var bug = new Shutterbug("#src", "#dst", null, null, null, {format: "jpeg", quality: 0.75});
31
+ $("button.shutterbug").click(function(e) {
32
+ bug.getDomSnapshot();
33
+ });
34
+ });
35
+ </script>
36
+ </html>
data/demo/index.html CHANGED
@@ -14,6 +14,7 @@
14
14
  <li>Very <a href="simple_example.html">simple</a> example.</li>
15
15
  <li><a href="iframe_example.html">IFrame</a> example.</li>
16
16
  <li><a href="nested_iframe_example.html">Nested IFrames</a> example.</li>
17
+ <li><a href="canvas_example.html">Canvas</a> example.</li>
17
18
  </ul>
18
19
  </body>
19
20
  </html>
data/lib/shutterbug.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Shutterbug
2
- VERSION = "0.2.5"
2
+ VERSION = "0.4.3"
3
3
  autoload :Rackapp, "shutterbug/rackapp"
4
4
  autoload :Configuration, "shutterbug/configuration"
5
5
  autoload :Storage, "shutterbug/storage"
@@ -1,6 +1,6 @@
1
1
  module Shutterbug
2
2
  module CacheManager
3
- autoload :NoCache, "shutterbug/cache_manager/no_cache"
4
- autoload :CacheEntry, "shutterbug/cache_manager/cache_entry"
3
+ autoload :NoCache, "shutterbug/cache_manager/no_cache"
4
+ autoload :CacheEntry, "shutterbug/cache_manager/cache_entry"
5
5
  end
6
6
  end
@@ -1,7 +1,7 @@
1
1
  require 'tmpdir'
2
+
2
3
  module Shutterbug
3
4
  class Configuration
4
-
5
5
  attr_accessor :uri_prefix
6
6
  attr_accessor :path_prefix
7
7
  attr_accessor :resource_dir
@@ -27,19 +27,15 @@ module Shutterbug
27
27
  end
28
28
 
29
29
  def fs_path_for(filename)
30
- File.join(resource_dir,"phantom_#{filename}")
30
+ File.join(resource_dir, "phantom_#{filename}")
31
31
  end
32
32
 
33
33
  def url_prefix
34
34
  "#{uri_prefix}#{path_prefix}"
35
35
  end
36
36
 
37
- def convert_path
38
- "#{url_prefix}/make_snapshot"
39
- end
40
-
41
37
  def base_url(req)
42
- req.POST()['base_url'] || req.referrer || "#{req.scheme}://#{req.host_with_port}"
38
+ req.POST()['base_url'] || req.referrer || "#{req.scheme}://#{req.host_with_port}"
43
39
  end
44
40
 
45
41
  def storage
@@ -50,4 +46,4 @@ module Shutterbug
50
46
  return (self.s3_bin && self.s3_key && self.s3_secret)
51
47
  end
52
48
  end
53
- end
49
+ end
@@ -1,7 +1,8 @@
1
- require "shutterbug/handlers/file_handlers"
2
1
  module Shutterbug
3
2
  module Handlers
4
- autoload :ConvertHandler, "shutterbug/handlers/convert_handler"
5
- autoload :JsFileHandler, "shutterbug/handlers/js_file_handler"
3
+ autoload :ConvertHandler, "shutterbug/handlers/convert_handler"
4
+ autoload :DirectUploadHandler, "shutterbug/handlers/direct_upload_handler"
5
+ autoload :JsFileHandler, "shutterbug/handlers/js_file_handler"
6
+ autoload :FileHandler, "shutterbug/handlers/file_handler"
6
7
  end
7
- end
8
+ end
@@ -1,38 +1,60 @@
1
1
  require 'stringio'
2
+
2
3
  module Shutterbug
3
4
  module Handlers
4
5
  class ConvertHandler
5
6
 
6
- def initialize(_config = Configuration.instance)
7
- @config = _config
7
+ def self.config
8
+ Configuration.instance
8
9
  end
9
10
 
10
- def regex
11
- /#{@config.path_prefix}\/make_snapshot/
11
+ def self.regex
12
+ /#{self.config.path_prefix}\/make_snapshot/
12
13
  end
13
14
 
14
15
  def handle(helper, req, env)
15
16
  response_text = convert(req).image_tag
16
- helper.good_response(response_text,'text/plain')
17
+ helper.response(response_text, 'text/plain')
18
+ end
19
+
20
+ def convert_quality(val, format)
21
+ # Client sends quality between 0 and 1 (similar to .toDataURL() second argument).
22
+ # This conversion tries to ensure that the size of the final image is similar to
23
+ # .toDataURL() output with given quality settings.
24
+ val = val.to_f
25
+ case format
26
+ when "png"
27
+ val *= 10
28
+ when "jpeg"
29
+ val *= 100
30
+ else
31
+ val *= 100
32
+ end
33
+ # PhantomJS expects integer.
34
+ val.to_i
17
35
  end
18
36
 
19
37
  def convert(req)
20
- html = req.POST()['content'] || ""
21
- width = req.POST()['width'] || 1000
22
- height = req.POST()['height'] || 700
23
- css = req.POST()['css'] || ""
24
- job = PhantomJob.new(@config.base_url(req), html, css, width, height)
25
- unless (cache_entry = @config.cache_manager.find(job.cache_key))
38
+ html = req.POST()['content'] || ""
39
+ width = req.POST()['width'] || 1000
40
+ height = req.POST()['height'] || 700
41
+ css = req.POST()['css'] || ""
42
+ format = req.POST()['format'] || "png"
43
+ quality = req.POST()['quality'] || 1
44
+ quality = convert_quality(quality, format)
45
+ config = self.class.config
46
+ job = PhantomJob.new(config.base_url(req), html, css, width, height, format, quality)
47
+ unless (cache_entry = config.cache_manager.find(job.cache_key))
26
48
  job.rasterize
27
49
  html_entry = Shutterbug::CacheManager::CacheEntry.new(job.html_file)
28
- png_entry = Shutterbug::CacheManager::CacheEntry.new(job.png_file)
29
- html_entry.preview_url = png_entry.preview_url
30
- @config.cache_manager.add_entry(html_entry)
31
- cache_entry = @config.cache_manager.add_entry(png_entry)
50
+ image_entry = Shutterbug::CacheManager::CacheEntry.new(job.image_file)
51
+ html_entry.preview_url = image_entry.preview_url
52
+ config.cache_manager.add_entry(html_entry)
53
+ cache_entry = config.cache_manager.add_entry(image_entry)
32
54
  end
33
55
  # return the image tag
34
56
  return cache_entry
35
57
  end
36
58
  end
37
59
  end
38
- end
60
+ end
@@ -0,0 +1,30 @@
1
+ require 'securerandom'
2
+
3
+ # This handler can be used for fast, client-side upload of images (or canvas) directly to storage.
4
+ # When client is taking snapshot of image or canvas, we don't need to use PhantomJS.
5
+ module Shutterbug
6
+ module Handlers
7
+ class DirectUploadHandler
8
+
9
+ def self.regex
10
+ /#{Configuration.instance.path_prefix}\/img_upload_url/
11
+ end
12
+
13
+ # Returns put_url and get_url for a new file that should be uploaded by the client.
14
+ # Of course get_url will work after file is uploaded.
15
+ def handle(helper, req, env)
16
+ format = req.GET()['format'] || 'png'
17
+
18
+ object_name = "img-#{SecureRandom.uuid}.#{format}"
19
+ storage = Configuration.instance.storage
20
+ unless storage.respond_to? :put_url
21
+ return helper.response('direct upload not available', 'text/plain', 400)
22
+ end
23
+ helper.response({
24
+ put_url: storage.put_url(object_name),
25
+ get_url: storage.get_url(object_name),
26
+ }.to_json, 'application/json')
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ module Shutterbug
2
+ module Handlers
3
+ class FileHandler
4
+
5
+ # relative url
6
+ def self.path_prefix
7
+ "#{Configuration.instance.path_prefix}/get_file"
8
+ end
9
+
10
+ # absolute url
11
+ def self.uri_prefix
12
+ "#{Configuration.instance.uri_prefix}#{self.path_prefix}"
13
+ end
14
+
15
+ def self.regex
16
+ filename_matcher = "(([^\/|\.]+)\.?([^\/]+))?"
17
+ /#{self.path_prefix}\/#{filename_matcher}/
18
+ end
19
+
20
+ def handle(helper, req, env)
21
+ filename = self.class.regex.match(req.path)[1]
22
+ if File.extname(filename) == ''
23
+ filename += '.html'
24
+ end
25
+ file = Configuration.instance.storage.new(filename)
26
+ helper.response(file.get_content, file.mime_type)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -2,26 +2,29 @@ module Shutterbug
2
2
  module Handlers
3
3
  class JsFileHandler
4
4
 
5
+ def self.config
6
+ Configuration.instance
7
+ end
8
+
5
9
  def self.js_path
6
- "#{Configuration.instance.url_prefix}/shutterbug.js"
10
+ "#{self.config.url_prefix}/shutterbug.js"
7
11
  end
8
12
 
9
- def regex
10
- /#{@config.path_prefix}\/shutterbug.js/
13
+ def self.regex
14
+ /#{self.config.path_prefix}\/shutterbug.js/
11
15
  end
12
16
 
13
- def js_file
14
- File.join(File.dirname(__FILE__),"shutterbug.js")
17
+ def initialize
18
+ @javascript = File.read(js_file).gsub(/URL_PREFIX/, self.class.config.url_prefix)
15
19
  end
16
20
 
17
- def initialize(_config=Configuration.instance())
18
- @config = _config
19
- @javascript = File.read(js_file).gsub(/CONVERT_PATH/,@config.convert_path)
21
+ def js_file
22
+ File.join(File.dirname(__FILE__), "shutterbug.js")
20
23
  end
21
24
 
22
25
  def handle(helper, req, env)
23
- helper.good_response(@javascript, 'application/javascript')
26
+ helper.response(@javascript, 'application/javascript')
24
27
  end
25
28
  end
26
29
  end
27
- end
30
+ end
@@ -1,9 +1,12 @@
1
1
  /*global $ */
2
- (function(){
3
- var $ = window.$;
2
+ (function() {
3
+ var $ = window.jQuery;
4
4
 
5
5
  var MAX_TIMEOUT = 1500;
6
6
 
7
+ // IE9 doesn't implement this.
8
+ var BIN_DATA_SUPPORTED = typeof(window.Blob) === "function" && typeof(window.Uint8Array) === "function";
9
+
7
10
  var getBaseUrl = function() {
8
11
  var base = window.location.href;
9
12
  return base;
@@ -50,7 +53,7 @@
50
53
  // - element descentands are iframes - .find('iframe')
51
54
  var $iframes = $element.find('iframe').addBack("iframe");
52
55
  this._iframeContentRequests = [];
53
-
56
+
54
57
  $iframes.each(function(i, iframeElem) {
55
58
  var message = {
56
59
  type: 'htmlFragRequest',
@@ -65,7 +68,7 @@
65
68
  var requestDeffered = new $.Deferred();
66
69
  self._iframeContentRequests[i] = requestDeffered;
67
70
  setTimeout(function() {
68
- // It handles a situation in which iframe doesn't support Shutterbug.
71
+ // It handles a situation in which iframe doesn't support Shutterbug.
69
72
  // When we doesn't receive answer for some time, assume that we can't
70
73
  // render this particular iframe (provide null as iframe description).
71
74
  if (requestDeffered.state() !== "resolved") {
@@ -101,19 +104,22 @@
101
104
  });
102
105
  }
103
106
 
104
- var replacementImgs = $element.find('canvas').map( function(count,elem) {
107
+ // .addBack('canvas') handles case when the element itself is a canvas.
108
+ var replacementImgs = $element.find('canvas').addBack('canvas').map(function(i, elem) {
109
+ // Use png here, as it supports transparency and canvas can be layered on top of other elements.
105
110
  var dataUrl = elem.toDataURL('image/png');
106
- var img = cloneDomItem($(elem),"<img>");
111
+ var img = cloneDomItem($(elem), "<img>");
107
112
  img.attr('src', dataUrl);
108
113
  return img;
109
114
  });
110
115
 
111
- element.find('canvas').each(function(i,elm) {
112
- var backgroundDiv = cloneDomItem($(elm),"<div>");
113
- // Add a backing (background) dom element for BG canvas property
114
- $(elm).replaceWith(replacementImgs[i]);
115
- backgroundDiv.insertBefore($(elm));
116
- });
116
+ if (element.is('canvas')) {
117
+ element = replacementImgs[0];
118
+ } else {
119
+ element.find('canvas').each(function(i, elem) {
120
+ $(elem).replaceWith(replacementImgs[i]);
121
+ });
122
+ }
117
123
 
118
124
  element.css({
119
125
  'top':0,
@@ -150,34 +156,110 @@
150
156
  var counter = $("<span>");
151
157
  counter.html(time);
152
158
  $(self.imgDst).html("creating snapshot: ").append(counter);
153
- var timer = setInterval(function(t) {
159
+ this.timer = setInterval(function(t) {
154
160
  time = time + 1;
155
161
  counter.html(time);
156
162
  }, 1000);
163
+ var tagName = $(this.element).prop("tagName");
164
+ switch(tagName) {
165
+ case "CANVAS":
166
+ this.canvasSnapshot();
167
+ break;
168
+ default:
169
+ this.basicSnapshot();
170
+ break;
171
+ }
172
+ };
173
+
174
+ var canvasSnapshot = function() {
175
+ if (!BIN_DATA_SUPPORTED) {
176
+ return this.basicSnapshot();
177
+ }
178
+ var self = this;
179
+ $.ajax({
180
+ type: 'GET',
181
+ url: 'URL_PREFIX/img_upload_url?format=' + this.imageFormat
182
+ }).done(function(data) {
183
+ self.directUpload(data);
184
+ }).fail(function() {
185
+ // Use basic snapshot as a fallback.
186
+ // Direct upload is not supported on server side (e.g. due to used storage).
187
+ self.basicSnapshot();
188
+ });
189
+ };
190
+
191
+ function directUpload(options) {
192
+ var $canvas = $(this.element);
193
+ var dataURL = $canvas[0].toDataURL('image/' + this.imageFormat, this.imageQuality)
194
+ var blob = dataURLtoBlob(dataURL);
195
+ var self = this;
196
+ $.ajax({
197
+ type: 'PUT',
198
+ url: options.put_url,
199
+ data: blob,
200
+ processData: false,
201
+ contentType: false
202
+ }).done(function(data) {
203
+ self.success('<img src=' + options.get_url + '>');
204
+ }).fail(function(jqXHR, textStatus, errorThrown) {
205
+ self.fail(jqXHR, textStatus, errorThrown)
206
+ });
207
+ }
208
+
209
+ function dataURLtoBlob(dataURL) {
210
+ // Convert base64/URLEncoded data component to raw binary data held in a string.
211
+ if (dataURL.split(',')[0].indexOf('base64') === -1) {
212
+ throw new Error('expected base64 data');
213
+ }
214
+ var byteString = atob(dataURL.split(',')[1]);
215
+ // Separate out the mime component.
216
+ var mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0];
217
+ // Write the bytes of the string to a typed array.
218
+ var ia = new Uint8Array(byteString.length);
219
+ for (var i = 0; i < byteString.length; i++) {
220
+ ia[i] = byteString.charCodeAt(i);
221
+ }
222
+ return new Blob([ia], {type: mimeString});
223
+ }
224
+
225
+ var basicSnapshot = function() {
226
+ var self = this;
157
227
  // Ask for HTML fragment and render it on server.
158
- this.getHtmlFragment(function(html) {
228
+ this.getHtmlFragment(function(html_data) {
229
+ html_data.format = self.imageFormat;
230
+ html_data.quality = self.imageQuality;
159
231
  $.ajax({
160
- url: "CONVERT_PATH",
232
+ url: "URL_PREFIX/make_snapshot",
161
233
  type: "POST",
162
- data: html
234
+ data: html_data
163
235
  }).success(function(msg) {
164
- if(self.imgDst) {
165
- $(self.imgDst).html(msg);
166
- }
167
- if (self.callback) {
168
- self.callback(msg);
169
- }
170
- clearInterval(timer);
236
+ self.success(msg)
171
237
  }).fail(function(jqXHR, textStatus, errorThrown) {
172
- $(self.imgDst).html("snapshot failed");
173
- if (self.failCallback) {
174
- self.failCallback(jqXHR, textStatus, errorThrown);
175
- }
176
- clearInterval(timer);
238
+ self.fail(jqXHR, textStatus, errorThrown);
177
239
  });
178
240
  });
179
241
  };
180
242
 
243
+ var success = function(imageTag) {
244
+ if (this.imgDst) {
245
+ $(this.imgDst).html(imageTag);
246
+ }
247
+ if (this.callback) {
248
+ this.callback(imageTag);
249
+ }
250
+ clearInterval(this.timer);
251
+ }
252
+
253
+ var fail = function(jqXHR, textStatus, errorThrown) {
254
+ if (this.imgDst) {
255
+ $(this.imgDst).html("snapshot failed");
256
+ }
257
+ if (this.failCallback) {
258
+ this.failCallback(jqXHR, textStatus, errorThrown);
259
+ }
260
+ clearInterval(this.timer);
261
+ }
262
+
181
263
  var requestHtmlFrag = function() {
182
264
  var destination = $(this.element)[0].contentWindow;
183
265
  var message = {
@@ -188,7 +270,7 @@
188
270
  };
189
271
 
190
272
  var htmlSnap = function() {
191
- this.getHtmlFragment(function callback(fragment){
273
+ this.getHtmlFragment(function callback(fragment) {
192
274
  // FIXME btoa is not intended to encode text it is for for 8bit per char strings
193
275
  // so if you send it a UTF8 string with a special char in it, it will fail
194
276
  // this SO has a note about handling this:
@@ -219,7 +301,7 @@
219
301
  };
220
302
 
221
303
  // TODO: Construct using opts instead of positional arguments.
222
- window.Shutterbug = function(selector, imgDst, callback, id, jQuery) {
304
+ window.Shutterbug = function(selector, imgDst, callback, id, jQuery, opt) {
223
305
  if (typeof(jQuery) != "undefined" && jQuery != null) {
224
306
  $ = jQuery;
225
307
  }
@@ -229,12 +311,21 @@
229
311
  $ = window.$;
230
312
  }
231
313
 
314
+ opt = opt || {};
315
+
232
316
  var shutterbugInstance = {
233
317
  element: selector,
234
318
  imgDst: imgDst,
235
319
  callback: callback,
236
320
  id: id,
321
+ imageFormat: opt.format || "png",
322
+ imageQuality: opt.quality || 1,
237
323
  getDomSnapshot: getDomSnapshot,
324
+ basicSnapshot: basicSnapshot,
325
+ canvasSnapshot: canvasSnapshot,
326
+ directUpload: directUpload,
327
+ success: success,
328
+ fail: fail,
238
329
  getHtmlFragment: getHtmlFragment,
239
330
  requestHtmlFrag: requestHtmlFrag,
240
331
  htmlSnap: htmlSnap,
@@ -260,8 +351,8 @@
260
351
 
261
352
  var htmlFragRequestListen = function(message) {
262
353
  var send_response = function(data) {
263
- // Update timeout. When we receive a request from parent, we have to finish nested iframes
264
- // rendering in that time. Otherwise parent rendering will timeout.
354
+ // Update timeout. When we receive a request from parent, we have to finish nested iframes
355
+ // rendering in that time. Otherwise parent rendering will timeout.
265
356
  // Backward compatibility: Shutterbug v0.1.x don't send iframeReqTimeout.
266
357
  shutterbugInstance.iframeReqTimeout = data.iframeReqTimeout != null ? data.iframeReqTimeout : MAX_TIMEOUT;
267
358
  shutterbugInstance.getHtmlFragment(function(html) {