Release 5.2
diff --git a/src/js/account.js b/src/js/account.js
new file mode 100644
index 0000000..d8ddd0d
--- /dev/null
+++ b/src/js/account.js
@@ -0,0 +1,75 @@
+/**

+ * Create a user.

+ * @param {Object} user

+ *   properties: id, name, accessToken, expires, accessTokenSecret, albumId

+ */

+var User = function(user) {

+  for (var prop in user) {

+    this[prop] = user[prop];

+  }

+};

+

+var Account = {

+

+  getUsers: function(siteId) {

+    var users = localStorage.getItem(siteId + '_userInfo');

+    if (users) {

+      users = JSON.parse(users);

+      for (var id in users) {

+        // Remove expired user.

+        if (Account.isExpires(users[id])) {

+          delete users[id];

+        }

+      }

+      localStorage.setItem(siteId + '_userInfo', JSON.stringify(users));

+      return users;

+    }

+    return {};

+  },

+

+  getUser: function(siteId, userId) {

+    var users = Account.getUsers(siteId);

+    var user = users[userId];

+    if (user && Account.isExpires(user)) {

+      Account.removeUser(siteId, userId);

+      return null;

+    } else {

+      return user;

+    }

+  },

+

+  addUser: function(siteId, user) {

+    var users = Account.getUsers(siteId);

+    var userId = user.id;

+    if (!users[userId]) {

+      users[userId] = user;

+      users = JSON.stringify(users);

+      localStorage.setItem(siteId + '_userInfo', users);

+    }

+  },

+

+  updateUser: function(siteId, user) {

+    var users = Account.getUsers(siteId);

+    var userId = user.id;

+    if (users && users[userId]) {

+      users[userId] = user;

+      users = JSON.stringify(users);

+      localStorage.setItem(siteId + '_userInfo', users);

+    }

+  },

+  

+  removeUser: function(siteId, userId) {

+    var users = Account.getUsers(siteId);

+    delete users[userId];

+    users = JSON.stringify(users);

+    localStorage.setItem(siteId + '_userInfo', users);

+  },

+

+  isExpires: function(user) {

+    var expires = user.expires;

+    if (expires) {

+      return new Date().getTime() >= expires;

+    }

+    return false;

+  }

+};
\ No newline at end of file
diff --git a/src/js/ajax.js b/src/js/ajax.js
new file mode 100644
index 0000000..2f3d4a5
--- /dev/null
+++ b/src/js/ajax.js
@@ -0,0 +1,185 @@
+(function(){

+  /**

+   * ajax is a encapsulated function that used to send data to server

+   * asynchronously. It uses XMLHttpRequest object to send textual or binary

+   * data through HTTP method GET, POST etc. It can custom request method,

+   * request header. Response can be parsed automatically by MIME type of

+   * response's Content-type, and it can handle success, error or progress event

+   * in course of sending request and retrieving response.

+   * @param {Object} option

+   */

+  function ajax(option) {

+    if (arguments.length < 1 || option.constructor != Object)

+      throw new Error('Bad parameter.');

+    var url = option.url;

+    var success = option.success;

+    var complete = option.complete;

+    if (!url || !(success || complete))

+      throw new Error('Parameter url and success or complete are required.');

+

+    var parameters = option.parameters || {};

+    var method = option.method || 'GET';

+    var status = option.status;

+    var headers = option.headers || {};

+    var data = option.data || null;

+    var multipartData = option.multipartData;

+    var queryString = constructQueryString(parameters);

+

+    if (multipartData) {

+      var boundary = multipartData.boundary || 'XMLHttpRequest2';

+      method = 'POST';

+      var multipartDataString;

+      var contentType = headers['Content-Type'] || 'multipart/form-data';

+      if (contentType.indexOf('multipart/form-data') == 0) {

+        headers['Content-Type'] = 'multipart/form-data; boundary=' + boundary;

+        multipartDataString = constructMultipartFormData(multipartData, boundary,

+          parameters);

+      } else if (contentType.indexOf('multipart/related') == 0) {

+        headers['Content-Type'] = 'multipart/related; boundary=' + boundary;

+        multipartDataString = constructMultipartRelatedData(boundary,

+          multipartData.dataList);

+      }

+

+      data = constructBufferData(multipartDataString);

+    } else {

+      if (queryString)

+        url += '?' + queryString;

+    }

+

+    var xhr = new XMLHttpRequest();

+    xhr.open(method, url, true);

+    xhr.onreadystatechange = function() {

+      if (xhr.readyState == 4) {

+        var statusCode = xhr.status;

+        var parsedResponse = parseResponse(xhr);

+        if (complete)

+          complete(statusCode, parsedResponse);

+        if (success && (statusCode == 200 || statusCode == 304)) {

+          success(parsedResponse);

+        } else if (status) {

+          if (status[statusCode]) {

+            // Call specified status code handler

+            status[statusCode](parsedResponse);

+          } else if (status['others']) {

+            // Call others status code handler

+            status['others'](parsedResponse, statusCode);

+          }

+        }

+      }

+    };

+

+    // Handle request progress

+    var progress = option.progress;

+    if (progress) {

+      xhr.upload.addEventListener('progress', function(e) {

+        // lengthComputable return true when the length of the progress is known

+        if (e.lengthComputable) {

+          progress(e.loaded, e.total);

+        }

+      }, false);

+    }

+    // Set request header

+    for (var headerKey in headers) {

+      xhr.setRequestHeader(headerKey, headers[headerKey]);

+    }

+

+    xhr.send(data);

+  }

+

+  function constructQueryString(parameters) {

+    var tmpParameter = [];

+    for(var name in parameters) {

+      var value = parameters[name];

+      if (value.constructor == Array) {

+        value.forEach(function(val) {

+          tmpParameter.push(name + '=' + val);

+        });

+      } else {

+        tmpParameter.push(name + '=' + value);

+      }

+    }

+    return tmpParameter.join('&');

+  }

+

+  // Parse response data according to content type of response

+  function parseResponse(xhr) {

+    var ct = xhr.getResponseHeader("content-type");

+    if (typeof ct == 'string') {

+      if (ct.indexOf('xml') >= 0)

+        return xhr.responseXML;

+      else if (ct.indexOf('json') >= 0)

+        return JSON.parse(xhr.responseText);

+    }

+    return xhr.responseText;

+  }

+

+  /**

+   * Construct multipart/form-data formatted data string.

+   * @param {Object} binaryData binary data

+   * @param {String} boundary boundary of parts

+   * @param {Object} otherParameters other text parameters

+   */

+  function constructMultipartFormData(binaryData, boundary, otherParameters) {

+    var commonHeader = 'Content-Disposition: form-data; ';

+    var data = [];

+    for (var key in otherParameters) {

+

+      // Add boundary of one header part

+      data.push('--' + boundary + '\r\n');

+

+      // Add same Content-Disposition information

+      data.push(commonHeader);

+      data.push('name="' + key + '"\r\n\r\n' + otherParameters[key] + '\r\n');

+    }

+

+    // Construct file data header

+    data.push('--' + boundary + '\r\n');

+    data.push(commonHeader);

+

+    data.push('name="' + (binaryData.name || 'binaryfilename') + '"; ');

+    data.push('filename=\"' + binaryData.value + '\"\r\n');

+    data.push('Content-type: ' + binaryData.type + '\r\n\r\n');

+    data.push(binaryData.data + '\r\n');

+

+    data.push('--' + boundary + '--\r\n');

+    return data.join('');

+  }

+

+  function constructBufferData(dataString, contentType) {

+      var len = dataString.length;

+

+      // Create a 8-bit unsigned integer ArrayBuffer view

+      var data = new Uint8Array(len);

+      for (var i = 0; i < len; i++) {

+        data[i] = dataString.charCodeAt(i);

+      }

+

+      return data.buffer

+  }

+

+  function constructMultipartRelatedData(boundary, dataList) {

+    var result = [];

+    dataList.forEach(function(data) {

+      result.push('--' + boundary + '\r\n');

+      result.push('Content-Type: ' + data.contentType + '\r\n\r\n');

+      result.push(data.data + '\r\n');

+    });

+    result.push('--' + boundary + '--\r\n');

+    return result.join('');

+  }

+

+  ajax.encodeForBinary = function(string) {

+    string = encodeURI(string).replace(/%([A-Z0-9]{2})/g, '%u00$1');

+    return unescape(string);

+  };

+

+  ajax.convertEntityString = function(string) {

+    var entitychars = ['<', '>', '&', '"', '\''];

+    var entities = ['&lt;', '&gt;', '&amp;', '&quot;', '&apos;'];

+    entitychars.forEach(function(character, index) {

+      string = string.replace(character, entities[index]);

+    });

+    return string;

+  };

+  window.ajax = ajax;

+})();
\ No newline at end of file
diff --git a/src/js/background.js b/src/js/background.js
new file mode 100644
index 0000000..e3c030e
--- /dev/null
+++ b/src/js/background.js
@@ -0,0 +1,323 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.  Use of this
+// source code is governed by a BSD-style license that can be found in the
+// LICENSE file.
+
+var screenshot = {
+  tab: 0,
+  canvas: document.createElement("canvas"),
+  startX: 0,
+  startY: 0,
+  scrollX: 0,
+  scrollY: 0,
+  docHeight: 0,
+  docWidth: 0,
+  visibleWidth: 0,
+  visibleHeight: 0,
+  scrollXCount: 0,
+  scrollYCount: 0,
+  scrollBarX: 17,
+  scrollBarY: 17,
+  captureStatus: true,
+  screenshotName: null,
+
+  handleHotKey: function(keyCode) {
+    if (HotKey.isEnabled()) {
+      switch (keyCode) {
+        case HotKey.getCharCode('area'):
+          screenshot.showSelectionArea();
+          break;
+        case HotKey.getCharCode('viewport'):
+          screenshot.captureWindow();
+          break;
+        case HotKey.getCharCode('fullpage'):
+          screenshot.captureWebpage();
+          break;
+        case HotKey.getCharCode('screen'):
+          screenshot.captureScreen();
+          break;
+      }
+    }
+  },
+
+  /**
+  * Receive messages from content_script, and then decide what to do next
+  */
+  addMessageListener: function() {
+    chrome.extension.onMessage.addListener(function(request, sender, response) {
+      var obj = request;
+      var hotKeyEnabled = HotKey.isEnabled();
+      switch (obj.msg) {
+        case 'capture_hot_key':
+          screenshot.handleHotKey(obj.keyCode);
+          break;
+        case 'capture_selected':
+          screenshot.captureSelected();
+          break;
+        case 'capture_window':
+          if (hotKeyEnabled) {
+            screenshot.captureWindow();
+          }
+          break;
+        case 'capture_area':
+          if (hotKeyEnabled) {
+            screenshot.showSelectionArea();
+          }
+          break;
+        case 'capture_webpage':
+          if (hotKeyEnabled) {
+            screenshot.captureWebpage();
+          }
+          break;
+      }
+    });
+  },
+
+  /**
+  * Send the Message to content-script
+  */
+  sendMessage: function(message, callback) {
+    chrome.tabs.getSelected(null, function(tab) {
+      chrome.tabs.sendMessage(tab.id, message, callback);
+    });
+  },
+
+  showSelectionArea: function() {
+    screenshot.sendMessage({msg: 'show_selection_area'}, null);
+  },
+
+  captureWindow: function() {
+    screenshot.sendMessage({msg: 'capture_window'},
+        screenshot.onResponseVisibleSize);
+  },
+
+  captureSelected: function() {
+    screenshot.sendMessage({msg: 'capture_selected'},
+        screenshot.onResponseVisibleSize);
+  },
+
+  captureWebpage: function() {
+    screenshot.sendMessage({msg: 'scroll_init'},
+        screenshot.onResponseVisibleSize);
+  },
+
+  onResponseVisibleSize: function(response) {
+    switch (response.msg) {
+      case 'capture_window':
+        screenshot.captureVisible(response.docWidth, response.docHeight);
+        break;
+      case 'scroll_init_done':
+        screenshot.startX = response.startX,
+        screenshot.startY = response.startY,
+        screenshot.scrollX = response.scrollX,
+        screenshot.scrollY = response.scrollY,
+        screenshot.canvas.width = response.canvasWidth;
+        screenshot.canvas.height = response.canvasHeight;
+        screenshot.visibleHeight = response.visibleHeight,
+        screenshot.visibleWidth = response.visibleWidth,
+        screenshot.scrollXCount = response.scrollXCount;
+        screenshot.scrollYCount = response.scrollYCount;
+        screenshot.docWidth = response.docWidth;
+        screenshot.docHeight = response.docHeight;
+        screenshot.zoom = response.zoom;
+        setTimeout("screenshot.captureAndScroll()", 100);
+        break;
+      case 'scroll_next_done':
+        screenshot.scrollXCount = response.scrollXCount;
+        screenshot.scrollYCount = response.scrollYCount;
+        setTimeout("screenshot.captureAndScroll()", 100);
+        break;
+      case 'scroll_finished':
+        screenshot.captureAndScrollDone();
+        break;
+    }
+  },
+
+  captureSpecialPage: function() {
+    var formatParam = localStorage.screenshootQuality || 'png';
+    chrome.tabs.captureVisibleTab(
+        null, {format: formatParam, quality: 50}, function(data) {
+      var image = new Image();
+      image.onload = function() {
+        screenshot.canvas.width = image.width;
+        screenshot.canvas.height = image.height;
+        var context = screenshot.canvas.getContext("2d");
+        context.drawImage(image, 0, 0);
+        screenshot.postImage();
+      };
+      image.src = data;
+    });
+  },
+
+  captureScreenCallback: function(data) {
+    var image = new Image();
+    image.onload = function() {
+      screenshot.canvas.width = image.width;
+      screenshot.canvas.height = image.height;
+      var context = screenshot.canvas.getContext("2d");
+      context.drawImage(image, 0, 0);
+      screenshot.postImage();
+    };
+    image.src = "data:image/bmp;base64," + data;
+  },
+
+  /**
+  * Use drawImage method to slice parts of a source image and draw them to
+  * the canvas
+  */
+  capturePortion: function(x, y, width, height,
+                           visibleWidth, visibleHeight, docWidth, docHeight) {
+    var formatParam = localStorage.screenshootQuality || 'png';
+    chrome.tabs.captureVisibleTab(
+        null, {format: formatParam, quality: 50}, function(data) {
+      var image = new Image();
+      image.onload = function() {
+        var curHeight = image.width < docWidth ?
+            image.height - screenshot.scrollBarY : image.height;
+        var curWidth = image.height < docHeight ?
+            image.width - screenshot.scrollBarX : image.width;
+        var zoomX = curWidth / visibleWidth;
+        var zoomY = curHeight / visibleHeight;
+        screenshot.canvas.width = width * zoomX;
+        screenshot.canvas.height = height * zoomY;
+        var context = screenshot.canvas.getContext("2d");
+        context.drawImage(image, x * zoomX, y * zoomY, width * zoomX,
+            height * zoomY, 0, 0, width * zoomX, height * zoomY);
+        screenshot.postImage();
+      };
+      image.src = data;
+    });
+  },
+
+  captureVisible: function(docWidth, docHeight) {
+    var formatParam = localStorage.screenshootQuality || 'png';
+    chrome.tabs.captureVisibleTab(
+        null, {format: formatParam, quality: 50}, function(data) {
+      var image = new Image();
+      image.onload = function() {
+        var width = image.height < docHeight ?
+            image.width - 17 : image.width;
+        var height = image.width < docWidth ?
+            image.height - 17 : image.height;
+        screenshot.canvas.width = width;
+        screenshot.canvas.height = height;
+        var context = screenshot.canvas.getContext("2d");
+        context.drawImage(image, 0, 0, width, height, 0, 0, width, height);
+        screenshot.postImage();
+      };
+      image.src = data;
+    });
+  },
+
+  /**
+  * Use the drawImage method to stitching images, and render to canvas
+  */
+  captureAndScroll: function() {
+    var formatParam = localStorage.screenshootQuality || 'png';
+    chrome.tabs.captureVisibleTab(
+        null, {format: formatParam, quality: 50}, function(data) {
+      var image = new Image();
+      image.onload = function() {
+        var context = screenshot.canvas.getContext('2d');
+        var width = 0;
+        var height = 0;
+
+        // Get scroll bar's width.
+        screenshot.scrollBarY =
+            screenshot.visibleHeight < screenshot.docHeight ? 17 : 0;
+        screenshot.scrollBarX =
+            screenshot.visibleWidth < screenshot.docWidth ? 17 : 0;
+
+        // Get visible width and height of capture result.
+        var visibleWidth =
+            (image.width - screenshot.scrollBarY < screenshot.canvas.width ?
+            image.width - screenshot.scrollBarY : screenshot.canvas.width);
+        var visibleHeight =
+            (image.height - screenshot.scrollBarX < screenshot.canvas.height ?
+            image.height - screenshot.scrollBarX : screenshot.canvas.height);
+
+        // Get region capture start x coordinate.
+        var zoom = screenshot.zoom;
+        var x1 = screenshot.startX - Math.round(screenshot.scrollX * zoom);
+        var x2 = 0;
+        var y1 = screenshot.startY - Math.round(screenshot.scrollY * zoom);
+        var y2 = 0;
+
+        if ((screenshot.scrollYCount + 1) * visibleWidth >
+            screenshot.canvas.width) {
+          width = screenshot.canvas.width % visibleWidth;
+          x1 = (screenshot.scrollYCount + 1) * visibleWidth -
+              screenshot.canvas.width + screenshot.startX - screenshot.scrollX;
+        } else {
+          width = visibleWidth;
+        }
+
+        if ((screenshot.scrollXCount + 1) * visibleHeight >
+            screenshot.canvas.height) {
+          height = screenshot.canvas.height % visibleHeight;
+          if ((screenshot.scrollXCount + 1) * visibleHeight +
+              screenshot.scrollY < screenshot.docHeight) {
+            y1 = 0;
+          } else {
+            y1 = (screenshot.scrollXCount + 1) * visibleHeight +
+                screenshot.scrollY - screenshot.docHeight;
+          }
+
+        } else {
+          height = visibleHeight;
+        }
+        x2 = screenshot.scrollYCount * visibleWidth;
+        y2 = screenshot.scrollXCount * visibleHeight;
+        context.drawImage(image, x1, y1, width, height, x2, y2, width, height);
+        screenshot.sendMessage({msg: 'scroll_next', visibleWidth: visibleWidth,
+            visibleHeight: visibleHeight}, screenshot.onResponseVisibleSize);
+      };
+      image.src = data;
+    });
+  },
+
+  captureAndScrollDone: function() {
+    screenshot.postImage();
+  },
+
+  /**
+  * Post the image to 'showimage.html'
+  */
+  postImage: function() {
+    chrome.tabs.getSelected(null, function(tab) {
+      screenshot.tab = tab;
+    });
+    var date = new Date();
+    screenshot.screenshotName = "Screenshot "+dateFormat(date, 'Y-m-d H.i.s');
+    chrome.tabs.create({'url': 'showimage.html'});
+    var popup = chrome.extension.getViews({type: 'popup'})[0];
+    if (popup)
+      popup.close();
+  },
+
+  isThisPlatform: function(operationSystem) {
+    return navigator.userAgent.toLowerCase().indexOf(operationSystem) > -1;
+  },
+
+  executeScriptsInExistingTabs: function() {
+    chrome.windows.getAll(null, function(wins) {
+      for (var j = 0; j < wins.length; ++j) {
+        chrome.tabs.getAllInWindow(wins[j].id, function(tabs) {
+          for (var i = 0; i < tabs.length; ++i) {
+            if (tabs[i].url.indexOf("chrome://") != 0) {
+              chrome.tabs.executeScript(tabs[i].id, { file: 'js/page.js' });
+              chrome.tabs.executeScript(tabs[i].id, { file: 'js/shortcut.js' });
+            }
+          }
+        });
+      }
+    });
+  },
+
+  init: function() {
+    localStorage.screenshootQuality = localStorage.screenshootQuality || 'png';
+    screenshot.executeScriptsInExistingTabs();
+    screenshot.addMessageListener();
+  }
+};
+
+screenshot.init();
diff --git a/src/js/editor.js b/src/js/editor.js
new file mode 100644
index 0000000..0d284ff
--- /dev/null
+++ b/src/js/editor.js
@@ -0,0 +1,240 @@
+function Canvas() {}

+

+Canvas.prototype.drawStrokeRect = function(

+    ctx, color, x, y, width, height, lineWidth) {

+  ctx.strokeStyle = color;

+  ctx.lineWidth = lineWidth;

+  ctx.strokeRect(x, y, width, height);

+}

+

+Canvas.prototype.drawFillRect = function(ctx, color, x, y, width, height) {

+  ctx.fillStyle = color;

+  ctx.fillRect(x, y, width, height);

+}

+

+Canvas.prototype.drawEllipse = function(

+    ctx, color, x, y, xAxis, yAxis, lineWidth, type) {

+  var startX = x + xAxis;

+  var startY = y;

+  ctx.beginPath();

+  ctx.lineWidth = lineWidth;

+  ctx.moveTo(startX, startY);

+  for (var i = 0; i <= 360; i++) {

+    var degree = i * Math.PI / 180;

+    startX = x + (xAxis - 2) * Math.cos(degree);

+    startY = y - (yAxis - 2) * Math.sin(degree);

+    ctx.lineTo(startX, startY);

+  }

+  if (type == 'rect') {

+    ctx.fillStyle = changeColorToRgba(color, 0.5);

+    ctx.fill();

+  } else if (type == 'border') {

+    ctx.strokeStyle = color;

+    ctx.stroke();

+  }

+  ctx.closePath();

+}

+

+// Divide an entire phrase in an array of phrases, all with the max pixel

+// length given.

+Canvas.prototype.getLines = function(ctx, text, width, font) {

+  var words = text.split(" ");

+  var lines = [];

+  var lastLine = "";

+  var measure = 0;

+  ctx.font = font;

+  for (var i = 0; i < words.length; i++) {

+    var word = words[i];

+    measure = ctx.measureText(lastLine + word).width;

+    if (measure <= width || word == "") {

+      lastLine += word + " ";

+    } else {

+      if (lastLine != "")

+        lines.push(lastLine);

+

+      // break the word if necessary 

+      measure = ctx.measureText(word).width;

+      if (measure <= width) {

+        lastLine = word + " ";

+      } else {

+        lastLine = word[0];

+        for (var j = 1; j < word.length; j++) {

+          measure = ctx.measureText(lastLine + word[j]).width;

+          if (measure <= width) {

+            lastLine += word[j];

+          } else {

+            lines.push(lastLine);

+            lastLine = word[j];

+          } 

+        }

+        lastLine += " ";

+      }

+    }

+  }

+  if (lastLine != "")

+    lines.push(lastLine);

+  return lines;

+}

+

+Canvas.prototype.setText = function(

+    ctx, text, color, fontSize, fontFamily, lineHeight, x, y, width) {

+  ctx.textBaseline = 'top';

+  ctx.fillStyle = color;

+  ctx.font = fontSize + ' ' + fontFamily;

+  ctx.lineHeight = lineHeight;

+  var lines = Canvas.prototype.getLines(ctx, text, width - 2, ctx.font);

+  for (var i = 0; i < lines.length; i++)

+    ctx.fillText(lines[i], x, y + lineHeight * i, width);

+}

+

+Canvas.prototype.drawLine = function(

+    ctx, color, lineCap, lineWidth, startX, startY, endX, endY) {

+  ctx.beginPath();

+  ctx.moveTo(startX, startY);

+  ctx.strokeStyle = color;

+  ctx.lineWidth = lineWidth;

+  ctx.lineCap = lineCap;

+  ctx.lineTo(endX, endY);

+  ctx.closePath();

+  ctx.stroke();

+}

+

+Canvas.prototype.drawArrow = function(

+    ctx, color, lineWidth, arrowWidth, arrowHeight, lineCap,

+    startX, startY, endX, endY) {

+  var arrowCoordinates = calculateArrowCoordinates(

+      arrowWidth, arrowHeight,startX, startY, endX, endY);

+  ctx.beginPath();

+  ctx.strokeStyle = color;

+  ctx.lineWidth = lineWidth;

+  ctx.lineCap = lineCap;

+  ctx.moveTo(startX, startY);

+  ctx.lineTo(endX, endY);

+  ctx.lineTo(arrowCoordinates.p1.x, arrowCoordinates.p1.y);

+  ctx.moveTo(endX, endY);

+  ctx.lineTo(arrowCoordinates.p2.x, arrowCoordinates.p2.y);

+  ctx.closePath();

+  ctx.stroke();

+}

+

+Canvas.prototype.drawRoundedRect = function(

+    ctx, color, x, y, width, height, radius, type) {

+  ctx.beginPath();

+  ctx.moveTo(x, y + radius);

+  ctx.lineTo(x, y + height - radius);

+  ctx.quadraticCurveTo(x, y + height, x + radius, y + height);

+  ctx.lineTo(x + width - radius, y + height);

+  ctx.quadraticCurveTo(x + width, y + height, x + width, y + height - radius);

+  ctx.lineTo(x + width, y + radius);

+  ctx.quadraticCurveTo(x + width, y, x + width - radius, y);

+  ctx.lineTo(x + radius, y);

+  ctx.quadraticCurveTo(x, y, x, y + radius);

+  if (type == 'rect') {

+    ctx.fillStyle = changeColorToRgba(color, 0.5);

+    ctx.fill();

+  } else if (type == 'border') {

+    ctx.strokeStyle = color;

+    ctx.lineWidth = 2;

+    ctx.stroke();

+  }

+  ctx.closePath();

+}

+

+Canvas.prototype.blurImage = function(

+    realCanvas, simulateCanvas, layerId, startX, startY, endX, endY) {

+  var x = startX < endX ? startX : endX;

+  var y = startY < endY ? startY : endY;

+  var width = Math.abs(endX - startX - 1);

+  var height = Math.abs(endY - startY - 1);

+  simulateCanvas.width = $(layerId).clientWidth + 10;

+  simulateCanvas.height = $(layerId).clientHeight + 10;

+  var ctx = simulateCanvas.getContext('2d');

+  try {

+    ctx.drawImage(realCanvas, x, y, width, height, 0, 0, width, height);

+  } catch (error) {

+    console.log(error + ', width : height = ' + width + ' : ' + height);

+  }

+  var imageData = ctx.getImageData(0, 0, width, height);

+  imageData = this.boxBlur(imageData, width, height, 10);

+  ctx.putImageData(imageData, 0, 0);

+}

+

+Canvas.prototype.boxBlur = function(image, width, height, count) {

+  var j;

+  var pix = image.data;

+  var inner = 0;

+  var outer = 0;

+  var step = 0;

+  var rowOrColumn;

+  var nextPosition;

+  var nowPosition;

+  for(rowOrColumn = 0; rowOrColumn < 2; rowOrColumn++) {

+    if (rowOrColumn) {

+      // column blurring

+      outer = width;

+      inner = height;

+      step = width * 4;

+    } else {

+      // Row blurring

+      outer = height;

+      inner = width;

+      step = 4;

+    }

+    for (var i = 0; i < outer; i++) {

+      // Calculate for r g b a

+      nextPosition = (rowOrColumn == 0 ? (i * width * 4) : (4 * i));

+      for (var k = 0; k < 4; k++) {

+        nowPosition = nextPosition + k;

+        var pixSum = 0;

+          for(var m = 0; m < count; m++) {

+            pixSum += pix[nowPosition + step * m];

+          }

+          pix[nowPosition] = pix[nowPosition + step] =

+              pix[nowPosition + step * 2] = Math.floor(pixSum/count);

+          for (j = 3; j < inner-2; j++) {

+            pixSum = Math.max(0, pixSum - pix[nowPosition + (j - 2) * step]

+                + pix[nowPosition + (j + 2) * step]);

+            pix[nowPosition + j * step] = Math.floor(pixSum/count);

+          }

+          pix[nowPosition + j * step] = pix[nowPosition + (j + 1) * step] =

+              Math.floor(pixSum / count);

+      }

+    }

+  }

+  return image;

+}

+

+function changeColorToRgba(color, opacity) {

+  var sColor = color.toLowerCase();

+  var sColorChange = [];

+  for (var i = 1; i < sColor.length; i += 2) {

+    sColorChange.push(parseInt("0x" + sColor.slice(i, i + 2)));

+  }

+  return "rgba(" + sColorChange.join(",") + "," + opacity + ")";

+}

+

+// Calculate coordinates of arrow

+function calculateArrowCoordinates(

+    arrowWidth, arrowHeight, startX, startY, endX, endY) {

+  var p1 = function() {

+    var x = startX - endX;

+    var y = startY - endY;

+    var hypotenuse = Math.sqrt(x * x + y * y);

+    hypotenuse = (hypotenuse == 0 ? arrowHeight : hypotenuse);

+    var dx = Math.round(x / hypotenuse * arrowHeight);

+    var dy = Math.round(y / hypotenuse * arrowHeight);

+    return {x: endX + dx, y: endY + dy};

+  }

+

+  var p2 = function(p1, direct) {

+    var x = p1.x - startX;

+    var y = p1.y - startY;

+    var hypotenuse = Math.sqrt(x * x + y * y);

+    hypotenuse = (hypotenuse == 0 ? arrowHeight : hypotenuse);

+    var dx = Math.round((y / hypotenuse * arrowWidth) * direct);

+    var dy = Math.round((x / hypotenuse * arrowWidth) * direct);

+    return {x: p1.x + dx, y: p1.y - dy }

+  }

+

+  return {p1:p2(p1(), 1), p2: p2(p1(), -1) } ;

+}

diff --git a/src/js/facebook.js b/src/js/facebook.js
new file mode 100644
index 0000000..68cb091
--- /dev/null
+++ b/src/js/facebook.js
@@ -0,0 +1,139 @@
+const FB_APP_ID = CURRENT_LOCALE == 'zh_CN' ? 170328509685996 : 118170701590738;

+const FB_REDIRECT_URI = 'http://www.facebook.com/connect/login_success.html';

+const FB_PERMISSION = 'offline_access,user_photos,publish_stream';

+const FB_ACCESS_TOKEN_URL = 'https://www.facebook.com/dialog/oauth';

+const FB_PHOTO_UPLOAD_URL = 'https://graph.facebook.com/me/photos';

+const FB_USER_INFO_URL = 'https://graph.facebook.com/me';

+const FB_LOGOUT_URL = 'http://m.facebook.com/logout.php?confirm=1';

+

+var Facebook = {

+  siteId: 'facebook',

+  redirectUrl: FB_REDIRECT_URI,

+  currentUserId: null,

+  accessTokenCallback: null,

+

+  isRedirectUrl: function(url) {

+    return url.indexOf(FB_REDIRECT_URI) == 0;

+  },

+  

+  getAccessToken: function(callback) {

+    Facebook.accessTokenCallback = callback;

+    var url = FB_ACCESS_TOKEN_URL + '?client_id=' + FB_APP_ID +

+      '&redirect_uri=' + FB_REDIRECT_URI + '&scope=' + FB_PERMISSION +

+      '&response_type=token';

+    chrome.tabs.create({url: url});

+  },

+

+  parseAccessToken: function(url) {

+    var queryString = url.split('#')[1] || url.split('?')[1];

+    var queries = queryString.split('&');

+    var queryMap = {};

+    queries.forEach(function(pair) {

+      queryMap[pair.split('=')[0]] = pair.split('=')[1];

+    });

+    var accessToken = queryMap['access_token'];

+    if (accessToken) {

+      var user = new User({

+        accessToken: accessToken

+      });

+      Facebook.accessTokenCallback('success', user);

+    } else if (queryMap['error']) {

+      Facebook.accessTokenCallback('failure', 'user_denied');

+    }

+    Facebook.accessTokenCallback = null;

+  },

+

+  getUserInfo: function(user, callback) {

+    ajax({

+      url: FB_USER_INFO_URL,

+      parameters: {

+        'access_token': user.accessToken

+      },

+      success: function(userInfo) {

+        userInfo = JSON.parse(userInfo);

+        if (callback) {

+          user.id = userInfo.id;

+          user.name = userInfo.name;

+          callback('success', user);

+        }

+      },

+      status: {

+        others: function() {

+          if (callback)

+            callback('failure', 'failed_to_get_user_info');

+        }

+      }

+    });

+  },

+  

+  upload: function(user, caption, imageData, callback) {

+    caption = ajax.encodeForBinary(caption);

+    var params = {

+      'access_token': user.accessToken,

+      message: caption

+    };

+    

+    var binaryData = {

+      boundary: MULTIPART_FORMDATA_BOUNDARY,

+      data: imageData,

+      value: 'test.png',

+      type: 'image/png'

+    };

+    

+    ajax({

+      url: FB_PHOTO_UPLOAD_URL,

+      parameters: params,

+      multipartData: binaryData,

+      success: function(data) {

+        callback('success', JSON.parse(data).id);

+      },

+      status: {

+        others: function(data) {

+          var message;

+          if (data) {

+            data = JSON.parse(data);

+            if (data.error.message.indexOf('access token') >= 0) {

+              // User removed application permission

+              // {"error":{"type":"OAuthException",

+              // "message":"Error validating access token."}}

+              message = 'bad_access_token';

+            } else {

+              // {"error":{"type":"OAuthException",

+              // "message":"(#1) An unknown error occurred"}}

+              message = 'unknown_error';

+            }

+          } else {

+            message = 'failed_to_connect_to_server';

+          }

+          callback('failure', message);

+        }

+      }

+    });

+  },

+

+  getPhotoLink: function(user, photoId, callback) {

+    ajax({

+      url: 'https://graph.facebook.com/' + photoId,

+      parameters: {

+        'access_token': user.accessToken

+      },

+      complete: function(statusCode, data) {

+        if (statusCode == 200) {

+          callback('success', JSON.parse(data).link);

+        } else {

+          callback('failure', 'failed_to_get_photo_link');

+        }

+      }

+    });

+  },

+

+  logout: function(callback) {

+    ajax({

+      url: FB_LOGOUT_URL,

+      success: function(data) {

+        if (callback)

+          callback(data);

+      }

+    });

+  }

+};
\ No newline at end of file
diff --git a/src/js/hotkey_storage.js b/src/js/hotkey_storage.js
new file mode 100644
index 0000000..9cea0aa
--- /dev/null
+++ b/src/js/hotkey_storage.js
@@ -0,0 +1,54 @@
+var HotKey = (function() {

+  return {

+    setup: function() {

+      // Default enable hot key for capture.

+      if (!localStorage.getItem('hot_key_enabled'))

+        localStorage.setItem('hot_key_enabled', true);

+

+      // Set default hot key of capture, R V H P.

+      if (!this.get('area'))

+        this.set('area', 'R');

+      if (!this.get('viewport'))

+        this.set('viewport', 'V');

+      if (!this.get('fullpage'))

+        this.set('fullpage', 'H');

+      if (!this.get('screen'))

+        this.set('screen', 'P');

+

+      var screenCaptureHotKey = this.get('screen');

+      if (this.isEnabled()) {

+        this.set('screen', '@'); // Disable hot key for screen capture.

+      }

+    },

+

+    /**

+     * Set hot key by type.

+     * @param {String} type Hot key type, must be area/viewport/fullpage/screen.

+     * @param {String} value

+     */

+    set: function(type, value) {

+      var key = type + '_capture_hot_key';

+      localStorage.setItem(key, value);

+    },

+

+    get: function(type) {

+      return localStorage.getItem(type + '_capture_hot_key');

+    },

+

+    getCharCode: function(type) {

+      return this.get(type).charCodeAt(0);

+    },

+

+    enable: function() {

+      localStorage.setItem('hot_key_enabled', true);

+    },

+

+    disable: function(bg) {

+      localStorage.setItem('hot_key_enabled', false);

+    },

+

+    isEnabled: function() {

+      return localStorage.getItem('hot_key_enabled') == 'true';

+    }

+  }

+})();

diff --git a/src/js/imgur.js b/src/js/imgur.js
new file mode 100644
index 0000000..cfc399f
--- /dev/null
+++ b/src/js/imgur.js
@@ -0,0 +1,241 @@
+(function() {

+  const IMGUR_APP_KEY = '90922ae86b91b09541d74134a5bb0f6404e0c378f';

+  const IMGUR_APP_SECRET = 'b100656ebf595b7464e428615b9bc703';

+  const IMGUR_REQUEST_TOKEN_URL = 'https://api.imgur.com/oauth/request_token';

+  const IMGUR_USER_AUTHENTICATION_URL = 'https://api.imgur.com/oauth/authorize';

+  const IMGUR_ACCESS_TOKEN_URL = 'https://api.imgur.com/oauth/access_token';

+  const IMGUR_USER_INFO_URL = 'http://api.imgur.com/2/account.json';

+  const IMGUR_PHOTO_UPLOAD_URL = 'http://api.imgur.com/2/account/images.json';

+  const IMGUR_LOGOUT_URL = 'http://imgur.com/logout';

+  const OAUTH_SIGNATURE_METHOD = 'HMAC-SHA1';

+  const OAUTH_VERSION = '1.0';

+

+  var Imgur = window.Imgur = {

+    siteId: 'imgur',

+    currentUserId: null,

+    currentUserOauthToken: '',

+    currentUserOauthTokenSecret: '',

+    accessTokenCallback: null,

+

+    isRedirectUrl: function() {},

+

+    getAuthorizationHeader: function(message, accessor) {

+      OAuth.setTimestampAndNonce(message);

+      OAuth.SignatureMethod.sign(message, accessor);

+      return OAuth.getAuthorizationHeader("", message.parameters);

+    },

+

+    getRequestToken: function(callback) {

+      Imgur.accessTokenCallback = callback;

+      var message = {

+        action: IMGUR_REQUEST_TOKEN_URL,

+        method: 'POST',

+        parameters: {

+          'oauth_consumer_key': IMGUR_APP_KEY,

+          'oauth_signature_method': OAUTH_SIGNATURE_METHOD,

+          'oauth_version': OAUTH_VERSION

+        }

+      };

+      var accessor = {

+        consumerKey: IMGUR_APP_KEY,

+        consumerSecret: IMGUR_APP_SECRET

+      };

+

+      // Get oauth signature header

+      var header = Imgur.getAuthorizationHeader(message, accessor);

+

+      ajax({

+        url: IMGUR_REQUEST_TOKEN_URL,

+        method: 'POST',

+        headers: {

+          'Authorization': header

+        },

+        success: function(response) {

+          parameters = OAuth.getParameterMap(response);

+          var oauth_token = parameters['oauth_token'];

+          var oauth_token_secret = parameters['oauth_token_secret'];

+          Imgur.currentUserOauthToken = oauth_token;

+          Imgur.currentUserOauthTokenSecret = oauth_token_secret;

+          Imgur.getUserAuthentication(oauth_token);

+        },

+        status: {

+          others: function() {

+            callback('failure', 'imgur_failed_to_get_request_token');

+          }

+        }

+      });

+    },

+

+    getUserAuthentication: function(oauth_token) {

+      var url = IMGUR_USER_AUTHENTICATION_URL + '?oauth_token=' + oauth_token +

+        '&oauth_callback=ready';

+      chrome.tabs.create({url: url}, function(tab) {

+          chrome.tabs.onUpdated.addListener(

+            function(tabId, changeInfo, _tab) {

+              if (tabId == tab.id && changeInfo.url

+                  && changeInfo.url.indexOf('oauth_verifier=') > 0) {

+                chrome.tabs.remove(tabId);

+                Imgur.parseAccessToken(changeInfo.url);

+              }

+            });

+        });

+    },

+

+    parseAccessToken: function(url) {

+      var oauth_verifier = OAuth.getParameter(url, 'oauth_verifier');

+      Imgur.getAccessToken(Imgur.accessTokenCallback, oauth_verifier);

+      Imgur.accessTokenCallback = null;

+    },

+

+    getAccessToken: function(callback, oauth_verifier) {

+      if (!oauth_verifier) {

+        Imgur.getRequestToken(callback);

+        return;

+      }

+      var message = {

+        action: IMGUR_ACCESS_TOKEN_URL,

+        method: 'POST',

+        parameters: {

+          'oauth_consumer_key': IMGUR_APP_KEY,

+          'oauth_token': Imgur.currentUserOauthToken,

+          'oauth_token_secret': Imgur.currentUserOauthTokenSecret,

+          'oauth_signature_method': OAUTH_SIGNATURE_METHOD,

+          'oauth_verifier': oauth_verifier,

+          'oauth_version': OAUTH_VERSION

+        }

+      };

+      var accessor = {

+        consumerKey: IMGUR_APP_KEY,

+        consumerSecret: IMGUR_APP_SECRET,

+        tokenSecret: Imgur.currentUserOauthTokenSecret

+      };

+      var header = Imgur.getAuthorizationHeader(message, accessor);

+

+      ajax({

+        url: IMGUR_ACCESS_TOKEN_URL,

+        method: 'POST',

+        headers: {

+          'Authorization': header

+        },

+        success: function(response) {

+          responseMap = OAuth.getParameterMap(response);

+          var accessToken = responseMap.oauth_token;

+          var accessTokenSecret = responseMap.oauth_token_secret;

+          var user = new User({

+            id: null,

+            accessToken: accessToken,

+            accessTokenSecret: accessTokenSecret

+          });

+

+          callback('success', user);

+        },

+        status: {

+          others: function(data) {

+            callback('failure', 'imgur_failed_to_get_access_token');

+          }

+        }

+      });

+    },

+

+    getUserInfo: function(user, callback) {

+      var url = IMGUR_USER_INFO_URL;

+      var message = {

+        action: url,

+        method: 'GET',

+        parameters: {

+          'oauth_consumer_key': IMGUR_APP_KEY,

+          'oauth_token': user.accessToken,

+          'oauth_signature_method': OAUTH_SIGNATURE_METHOD,

+          'oauth_version': OAUTH_VERSION

+        }

+      };

+

+      var accessor = {

+        consumerSecret: IMGUR_APP_SECRET,

+        tokenSecret: user.accessTokenSecret

+      };

+

+      var header = Imgur.getAuthorizationHeader(message, accessor);

+      ajax({

+        url: url,

+        method: 'GET',

+        headers: {

+          'Authorization': header

+        },

+        success: function(data) {

+          if (callback) {

+            user.id = data.account.url;

+            user.name = data.account.url;

+            callback('success', user);

+          }

+        },

+        status: {

+          others: function(data) {

+            callback('failure', 'failed_to_get_user_info');

+          }

+        }

+      });

+    },

+

+    upload: function(user, caption, imageData, callback) {

+      caption = encodeURIComponent(caption);

+      var message = {

+        action: IMGUR_PHOTO_UPLOAD_URL,

+        method: 'POST',

+        parameters: {

+          'oauth_consumer_key': IMGUR_APP_KEY,

+          'oauth_token': user.accessToken,

+          'oauth_signature_method': OAUTH_SIGNATURE_METHOD,

+          'oauth_version': OAUTH_VERSION

+        }

+      };

+      var accessor = {

+        consumerSecret: IMGUR_APP_SECRET,

+        tokenSecret: user.accessTokenSecret

+      };

+      var header = Imgur.getAuthorizationHeader(message, accessor);

+

+      var binaryData = {

+        boundary: MULTIPART_FORMDATA_BOUNDARY,

+        name: 'image',

+        value: 'screencapture.png',

+        data: imageData,

+        type: 'image/png'

+      };

+

+      ajax({

+        url: IMGUR_PHOTO_UPLOAD_URL,

+        method: 'POST',

+        multipartData: binaryData,

+        headers: {

+          'Authorization': header

+        },

+        success: function(response) {

+          callback('success', response.images.links.original);

+        },

+        status: {

+          others: function(err, statusCode) {

+            if (statusCode == 401) {

+              callback('failure', 'bad_access_token');

+            } else {

+              callback('failure', 'failed_to_upload_image');

+            };

+          }

+        }

+      });

+    },

+

+    getPhotoLink: function(user, photoLink, callback) {

+      callback('success', photoLink);

+    },

+

+    logout: function(callback) {

+      ajax({

+        url: IMGUR_LOGOUT_URL,

+        success: function() {

+          callback();

+        }

+      });

+    }

+  };

+})();
\ No newline at end of file
diff --git a/src/js/isLoad.js b/src/js/isLoad.js
new file mode 100644
index 0000000..77ad5c8
--- /dev/null
+++ b/src/js/isLoad.js
@@ -0,0 +1,17 @@
+function checkScriptLoad() {

+  chrome.extension.onMessage.addListener(function(request, sender, response) {

+    if (request.msg == 'is_page_capturable') {

+      try {

+        if (isPageCapturable()) {

+          response({msg: 'capturable'});

+        } else {

+          response({msg: 'uncapturable'});

+        }

+      } catch(e) {

+        response({msg: 'loading'});

+      }

+    }

+  });

+}

+

+checkScriptLoad();

diff --git a/src/js/oauth.js b/src/js/oauth.js
new file mode 100644
index 0000000..85b2785
--- /dev/null
+++ b/src/js/oauth.js
@@ -0,0 +1,551 @@
+/*

+ * Copyright 2008 Netflix, Inc.

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *     http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+

+/* Here's some JavaScript software for implementing OAuth.

+

+   This isn't as useful as you might hope.  OAuth is based around

+   allowing tools and websites to talk to each other.  However,

+   JavaScript running in web browsers is hampered by security

+   restrictions that prevent code running on one website from

+   accessing data stored or served on another.

+

+   Before you start hacking, make sure you understand the limitations

+   posed by cross-domain XMLHttpRequest.

+

+   On the bright side, some platforms use JavaScript as their

+   language, but enable the programmer to access other web sites.

+   Examples include Google Gadgets, and Microsoft Vista Sidebar.

+   For those platforms, this library should come in handy.

+*/

+

+// The HMAC-SHA1 signature method calls b64_hmac_sha1, defined by

+// http://pajhome.org.uk/crypt/md5/sha1.js

+

+/* An OAuth message is represented as an object like this:

+   {method: "GET", action: "http://server.com/path", parameters: ...}

+

+   The parameters may be either a map {name: value, name2: value2}

+   or an Array of name-value pairs [[name, value], [name2, value2]].

+   The latter representation is more powerful: it supports parameters

+   in a specific sequence, or several parameters with the same name;

+   for example [["a", 1], ["b", 2], ["a", 3]].

+

+   Parameter names and values are NOT percent-encoded in an object.

+   They must be encoded before transmission and decoded after reception.

+   For example, this message object:

+   {method: "GET", action: "http://server/path", parameters: {p: "x y"}}

+   ... can be transmitted as an HTTP request that begins:

+   GET /path?p=x%20y HTTP/1.0

+   (This isn't a valid OAuth request, since it lacks a signature etc.)

+   Note that the object "x y" is transmitted as x%20y.  To encode

+   parameters, you can call OAuth.addToURL, OAuth.formEncode or

+   OAuth.getAuthorization.

+

+   This message object model harmonizes with the browser object model for

+   input elements of an form, whose value property isn't percent encoded.

+   The browser encodes each value before transmitting it. For example,

+   see consumer.setInputs in example/consumer.js.

+ */

+

+/* This script needs to know what time it is. By default, it uses the local

+   clock (new Date), which is apt to be inaccurate in browsers. To do

+   better, you can load this script from a URL whose query string contains

+   an oauth_timestamp parameter, whose value is a current Unix timestamp.

+   For example, when generating the enclosing document using PHP:

+

+   <script src="oauth.js?oauth_timestamp=<?=time()?>" ...

+

+   Another option is to call OAuth.correctTimestamp with a Unix timestamp.

+ */

+

+var OAuth; if (OAuth == null) OAuth = {};

+

+OAuth.setProperties = function setProperties(into, from) {

+    if (into != null && from != null) {

+        for (var key in from) {

+            into[key] = from[key];

+        }

+    }

+    return into;

+}

+

+OAuth.setProperties(OAuth, // utility functions

+{

+    percentEncode: function percentEncode(s) {

+        if (s == null) {

+            return "";

+        }

+        if (s instanceof Array) {

+            var e = "";

+            for (var i = 0; i < s.length; ++s) {

+                if (e != "") e += '&';

+                e += OAuth.percentEncode(s[i]);

+            }

+            return e;

+        }

+        s = encodeURIComponent(s);

+        // Now replace the values which encodeURIComponent doesn't do

+        // encodeURIComponent ignores: - _ . ! ~ * ' ( )

+        // OAuth dictates the only ones you can ignore are: - _ . ~

+        // Source: http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Functions:encodeURIComponent

+        s = s.replace(/\!/g, "%21");

+        s = s.replace(/\*/g, "%2A");

+        s = s.replace(/\'/g, "%27");

+        s = s.replace(/\(/g, "%28");

+        s = s.replace(/\)/g, "%29");

+        return s;

+    }

+,

+    decodePercent: function decodePercent(s) {

+        if (s != null) {

+            // Handle application/x-www-form-urlencoded, which is defined by

+            // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1

+            s = s.replace(/\+/g, " ");

+        }

+        return decodeURIComponent(s);

+    }

+,

+    /** Convert the given parameters to an Array of name-value pairs. */

+    getParameterList: function getParameterList(parameters) {

+        if (parameters == null) {

+            return [];

+        }

+        if (typeof parameters != "object") {

+            return OAuth.decodeForm(parameters + "");

+        }

+        if (parameters instanceof Array) {

+            return parameters;

+        }

+        var list = [];

+        for (var p in parameters) {

+            list.push([p, parameters[p]]);

+        }

+        return list;

+    }

+,

+    /** Convert the given parameters to a map from name to value. */

+    getParameterMap: function getParameterMap(parameters) {

+        if (parameters == null) {

+            return {};

+        }

+        if (typeof parameters != "object") {

+            return OAuth.getParameterMap(OAuth.decodeForm(parameters + ""));

+        }

+        if (parameters instanceof Array) {

+            var map = {};

+            for (var p = 0; p < parameters.length; ++p) {

+                var key = parameters[p][0];

+                if (map[key] === undefined) { // first value wins

+                    map[key] = parameters[p][1];

+                }

+            }

+            return map;

+        }

+        return parameters;

+    }

+,

+    getParameter: function getParameter(parameters, name) {

+        if (parameters instanceof Array) {

+            for (var p = 0; p < parameters.length; ++p) {

+                if (parameters[p][0] == name) {

+                    return parameters[p][1]; // first value wins

+                }

+            }

+        } else {

+            return OAuth.getParameterMap(parameters)[name];

+        }

+        return null;

+    }

+,

+    formEncode: function formEncode(parameters) {

+        var form = "";

+        var list = OAuth.getParameterList(parameters);

+        for (var p = 0; p < list.length; ++p) {

+            var value = list[p][1];

+            if (value == null) value = "";

+            if (form != "") form += '&';

+            form += OAuth.percentEncode(list[p][0])

+              +'='+ OAuth.percentEncode(value);

+        }

+        return form;

+    }

+,

+    decodeForm: function decodeForm(form) {

+        var list = [];

+        var nvps = form.split('&');

+        for (var n = 0; n < nvps.length; ++n) {

+            var nvp = nvps[n];

+            if (nvp == "") {

+                continue;

+            }

+            var equals = nvp.indexOf('=');

+            var name;

+            var value;

+            if (equals < 0) {

+                name = OAuth.decodePercent(nvp);

+                value = null;

+            } else {

+                name = OAuth.decodePercent(nvp.substring(0, equals));

+                value = OAuth.decodePercent(nvp.substring(equals + 1));

+            }

+            list.push([name, value]);

+        }

+        return list;

+    }

+,

+    setParameter: function setParameter(message, name, value) {

+        var parameters = message.parameters;

+        if (parameters instanceof Array) {

+            for (var p = 0; p < parameters.length; ++p) {

+                if (parameters[p][0] == name) {

+                    if (value === undefined) {

+                        parameters.splice(p, 1);

+                    } else {

+                        parameters[p][1] = value;

+                        value = undefined;

+                    }

+                }

+            }

+            if (value !== undefined) {

+                parameters.push([name, value]);

+            }

+        } else {

+            parameters = OAuth.getParameterMap(parameters);

+            parameters[name] = value;

+            message.parameters = parameters;

+        }

+    }

+,

+    setParameters: function setParameters(message, parameters) {

+        var list = OAuth.getParameterList(parameters);

+        for (var i = 0; i < list.length; ++i) {

+            OAuth.setParameter(message, list[i][0], list[i][1]);

+        }

+    }

+,

+    /** Fill in parameters to help construct a request message.

+        This function doesn't fill in every parameter.

+        The accessor object should be like:

+        {consumerKey:'foo', consumerSecret:'bar', accessorSecret:'nurn', token:'krelm', tokenSecret:'blah'}

+        The accessorSecret property is optional.

+     */

+    completeRequest: function completeRequest(message, accessor) {

+        if (message.method == null) {

+            message.method = "GET";

+        }

+        var map = OAuth.getParameterMap(message.parameters);

+        if (map.oauth_consumer_key == null) {

+            OAuth.setParameter(message, "oauth_consumer_key", accessor.consumerKey || "");

+        }

+        if (map.oauth_token == null && accessor.token != null) {

+            OAuth.setParameter(message, "oauth_token", accessor.token);

+        }

+        if (map.oauth_version == null) {

+            OAuth.setParameter(message, "oauth_version", "1.0");

+        }

+        if (map.oauth_timestamp == null) {

+            OAuth.setParameter(message, "oauth_timestamp", OAuth.timestamp());

+        }

+        if (map.oauth_nonce == null) {

+            OAuth.setParameter(message, "oauth_nonce", OAuth.nonce(6));

+        }

+        OAuth.SignatureMethod.sign(message, accessor);

+    }

+,

+    setTimestampAndNonce: function setTimestampAndNonce(message) {

+        OAuth.setParameter(message, "oauth_timestamp", OAuth.timestamp());

+        OAuth.setParameter(message, "oauth_nonce", OAuth.nonce(6));

+    }

+,

+    addToURL: function addToURL(url, parameters) {

+        newURL = url;

+        if (parameters != null) {

+            var toAdd = OAuth.formEncode(parameters);

+            if (toAdd.length > 0) {

+                var q = url.indexOf('?');

+                if (q < 0) newURL += '?';

+                else       newURL += '&';

+                newURL += toAdd;

+            }

+        }

+        return newURL;

+    }

+,

+    /** Construct the value of the Authorization header for an HTTP request. */

+    getAuthorizationHeader: function getAuthorizationHeader(realm, parameters) {

+        var header = 'OAuth realm="' + OAuth.percentEncode(realm) + '"';

+        var list = OAuth.getParameterList(parameters);

+        for (var p = 0; p < list.length; ++p) {

+            var parameter = list[p];

+            var name = parameter[0];

+            if (name.indexOf("oauth_") == 0) {

+                header += ',' + OAuth.percentEncode(name) + '="' + OAuth.percentEncode(parameter[1]) + '"';

+            }

+        }

+        return header;

+    }

+,

+    /** Correct the time using a parameter from the URL from which the last script was loaded. */

+    correctTimestampFromSrc: function correctTimestampFromSrc(parameterName) {

+        parameterName = parameterName || "oauth_timestamp";

+        var scripts = document.getElementsByTagName('script');

+        if (scripts == null || !scripts.length) return;

+        var src = scripts[scripts.length-1].src;

+        if (!src) return;

+        var q = src.indexOf("?");

+        if (q < 0) return;

+        parameters = OAuth.getParameterMap(OAuth.decodeForm(src.substring(q+1)));

+        var t = parameters[parameterName];

+        if (t == null) return;

+        OAuth.correctTimestamp(t);

+    }

+,

+    /** Generate timestamps starting with the given value. */

+    correctTimestamp: function correctTimestamp(timestamp) {

+        OAuth.timeCorrectionMsec = (timestamp * 1000) - (new Date()).getTime();

+    }

+,

+    /** The difference between the correct time and my clock. */

+    timeCorrectionMsec: 0

+,

+    timestamp: function timestamp() {

+        var t = (new Date()).getTime() + OAuth.timeCorrectionMsec;

+        return Math.floor(t / 1000);

+    }

+,

+    nonce: function nonce(length) {

+        var chars = OAuth.nonce.CHARS;

+        var result = "";

+        for (var i = 0; i < length; ++i) {

+            var rnum = Math.floor(Math.random() * chars.length);

+            result += chars.substring(rnum, rnum+1);

+        }

+        return result;

+    }

+});

+

+OAuth.nonce.CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";

+

+/** Define a constructor function,

+    without causing trouble to anyone who was using it as a namespace.

+    That is, if parent[name] already existed and had properties,

+    copy those properties into the new constructor.

+ */

+OAuth.declareClass = function declareClass(parent, name, newConstructor) {

+    var previous = parent[name];

+    parent[name] = newConstructor;

+    if (newConstructor != null && previous != null) {

+        for (var key in previous) {

+            if (key != "prototype") {

+                newConstructor[key] = previous[key];

+            }

+        }

+    }

+    return newConstructor;

+}

+

+/** An abstract algorithm for signing messages. */

+OAuth.declareClass(OAuth, "SignatureMethod", function OAuthSignatureMethod(){});

+

+OAuth.setProperties(OAuth.SignatureMethod.prototype, // instance members

+{

+    /** Add a signature to the message. */

+    sign: function sign(message) {

+        var baseString = OAuth.SignatureMethod.getBaseString(message);

+        var signature = this.getSignature(baseString);

+        OAuth.setParameter(message, "oauth_signature", signature);

+        return signature; // just in case someone's interested

+    }

+,

+    /** Set the key string for signing. */

+    initialize: function initialize(name, accessor) {

+        var consumerSecret;

+        if (accessor.accessorSecret != null

+            && name.length > 9

+            && name.substring(name.length-9) == "-Accessor")

+        {

+            consumerSecret = accessor.accessorSecret;

+        } else {

+            consumerSecret = accessor.consumerSecret;

+        }

+        this.key = OAuth.percentEncode(consumerSecret)

+             +"&"+ OAuth.percentEncode(accessor.tokenSecret);

+    }

+});

+

+/* SignatureMethod expects an accessor object to be like this:

+   {tokenSecret: "lakjsdflkj...", consumerSecret: "QOUEWRI..", accessorSecret: "xcmvzc..."}

+   The accessorSecret property is optional.

+ */

+// Class members:

+OAuth.setProperties(OAuth.SignatureMethod, // class members

+{

+    sign: function sign(message, accessor) {

+        var name = OAuth.getParameterMap(message.parameters).oauth_signature_method;

+        if (name == null || name == "") {

+            name = "HMAC-SHA1";

+            OAuth.setParameter(message, "oauth_signature_method", name);

+        }

+        OAuth.SignatureMethod.newMethod(name, accessor).sign(message);

+    }

+,

+    /** Instantiate a SignatureMethod for the given method name. */

+    newMethod: function newMethod(name, accessor) {

+        var impl = OAuth.SignatureMethod.REGISTERED[name];

+        if (impl != null) {

+            var method = new impl();

+            method.initialize(name, accessor);

+            return method;

+        }

+        var err = new Error("signature_method_rejected");

+        var acceptable = "";

+        for (var r in OAuth.SignatureMethod.REGISTERED) {

+            if (acceptable != "") acceptable += '&';

+            acceptable += OAuth.percentEncode(r);

+        }

+        err.oauth_acceptable_signature_methods = acceptable;

+        throw err;

+    }

+,

+    /** A map from signature method name to constructor. */

+    REGISTERED : {}

+,

+    /** Subsequently, the given constructor will be used for the named methods.

+        The constructor will be called with no parameters.

+        The resulting object should usually implement getSignature(baseString).

+        You can easily define such a constructor by calling makeSubclass, below.

+     */

+    registerMethodClass: function registerMethodClass(names, classConstructor) {

+        for (var n = 0; n < names.length; ++n) {

+            OAuth.SignatureMethod.REGISTERED[names[n]] = classConstructor;

+        }

+    }

+,

+    /** Create a subclass of OAuth.SignatureMethod, with the given getSignature function. */

+    makeSubclass: function makeSubclass(getSignatureFunction) {

+        var superClass = OAuth.SignatureMethod;

+        var subClass = function() {

+            superClass.call(this);

+        };

+        subClass.prototype = new superClass();

+        // Delete instance variables from prototype:

+        // delete subclass.prototype... There aren't any.

+        subClass.prototype.getSignature = getSignatureFunction;

+        subClass.prototype.constructor = subClass;

+        return subClass;

+    }

+,

+    getBaseString: function getBaseString(message) {

+        var URL = message.action;

+        var q = URL.indexOf('?');

+        var parameters;

+        if (q < 0) {

+            parameters = message.parameters;

+        } else {

+            // Combine the URL query string with the other parameters:

+            parameters = OAuth.decodeForm(URL.substring(q + 1));

+            var toAdd = OAuth.getParameterList(message.parameters);

+            for (var a = 0; a < toAdd.length; ++a) {

+                parameters.push(toAdd[a]);

+            }

+        }

+        return OAuth.percentEncode(message.method.toUpperCase())

+         +'&'+ OAuth.percentEncode(OAuth.SignatureMethod.normalizeUrl(URL))

+         +'&'+ OAuth.percentEncode(OAuth.SignatureMethod.normalizeParameters(parameters));

+    }

+,

+    normalizeUrl: function normalizeUrl(url) {

+        var uri = OAuth.SignatureMethod.parseUri(url);

+        var scheme = uri.protocol.toLowerCase();

+        var authority = uri.authority.toLowerCase();

+        var dropPort = (scheme == "http" && uri.port == 80)

+                    || (scheme == "https" && uri.port == 443);

+        if (dropPort) {

+            // find the last : in the authority

+            var index = authority.lastIndexOf(":");

+            if (index >= 0) {

+                authority = authority.substring(0, index);

+            }

+        }

+        var path = uri.path;

+        if (!path) {

+            path = "/"; // conforms to RFC 2616 section 3.2.2

+        }

+        // we know that there is no query and no fragment here.

+        return scheme + "://" + authority + path;

+    }

+,

+    parseUri: function parseUri (str) {

+        /* This function was adapted from parseUri 1.2.1

+           http://stevenlevithan.com/demo/parseuri/js/assets/parseuri.js

+         */

+        var o = {key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],

+                 parser: {strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ }};

+        var m = o.parser.strict.exec(str);

+        var uri = {};

+        var i = 14;

+        while (i--) uri[o.key[i]] = m[i] || "";

+        return uri;

+    }

+,

+    normalizeParameters: function normalizeParameters(parameters) {

+        if (parameters == null) {

+            return "";

+        }

+        var list = OAuth.getParameterList(parameters);

+        var sortable = [];

+        for (var p = 0; p < list.length; ++p) {

+            var nvp = list[p];

+            if (nvp[0] != "oauth_signature") {

+                sortable.push([ OAuth.percentEncode(nvp[0])

+                              + " " // because it comes before any character that can appear in a percentEncoded string.

+                              + OAuth.percentEncode(nvp[1])

+                              , nvp]);

+            }

+        }

+        sortable.sort(function(a,b) {

+                          if (a[0] < b[0]) return  -1;

+                          if (a[0] > b[0]) return 1;

+                          return 0;

+                      });

+        var sorted = [];

+        for (var s = 0; s < sortable.length; ++s) {

+            sorted.push(sortable[s][1]);

+        }

+        return OAuth.formEncode(sorted);

+    }

+});

+

+OAuth.SignatureMethod.registerMethodClass(["PLAINTEXT", "PLAINTEXT-Accessor"],

+    OAuth.SignatureMethod.makeSubclass(

+        function getSignature(baseString) {

+            return this.key;

+        }

+    ));

+

+OAuth.SignatureMethod.registerMethodClass(["HMAC-SHA1", "HMAC-SHA1-Accessor"],

+    OAuth.SignatureMethod.makeSubclass(

+        function getSignature(baseString) {

+            b64pad = '=';

+            var signature = b64_hmac_sha1(this.key, baseString);

+            return signature;

+        }

+    ));

+

+try {

+    OAuth.correctTimestampFromSrc();

+} catch(e) {

+}

diff --git a/src/js/options.js b/src/js/options.js
new file mode 100644
index 0000000..9d61498
--- /dev/null
+++ b/src/js/options.js
@@ -0,0 +1,181 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.  Use of this

+// source code is governed by a BSD-style license that can be found in the

+// LICENSE file.

+

+var bg = chrome.extension.getBackgroundPage();

+

+function $(id) {

+  return document.getElementById(id);

+}

+

+function isHighVersion() {

+  var version = navigator.userAgent.match(/Chrome\/(\d+)/)[1];

+  return version > 9;

+}

+

+function init() {

+  i18nReplace('optionTitle', 'options');

+  i18nReplace('saveAndClose', 'save_and_close');

+  i18nReplace('screenshootQualitySetting', 'quality_setting');

+  i18nReplace('lossyScreenShot', 'lossy');

+  i18nReplace('losslessScreenShot', 'lossless');

+  i18nReplace('shorcutSetting', 'shortcut_setting');

+  i18nReplace('settingShortcutText', 'shortcutsetting_text');

+  if (isHighVersion()) {

+    $('lossyScreenShot').innerText += ' (JPEG)';

+    $('losslessScreenShot').innerText += ' (PNG)';

+  }

+  $('saveAndClose').addEventListener('click', saveAndClose);

+  initScreenCaptureQuality();

+  HotKeySetting.setup();

+}

+

+function save() {

+  localStorage.screenshootQuality =

+      $('lossy').checked ? 'jpeg' : '' ||

+      $('lossless').checked ? 'png' : '';

+

+  return HotKeySetting.save();

+}

+

+function saveAndClose() {

+  if (save())

+    chrome.tabs.getSelected(null, function(tab) {

+      chrome.tabs.remove(tab.id);

+    });

+}

+

+function initScreenCaptureQuality() {

+  $('lossy').checked = localStorage.screenshootQuality == 'jpeg';

+  $('lossless').checked = localStorage.screenshootQuality == 'png';

+}

+

+function i18nReplace(id, name) {

+  return $(id).innerText = chrome.i18n.getMessage(name);

+}

+

+const CURRENT_LOCALE = chrome.i18n.getMessage('@@ui_locale');

+if (CURRENT_LOCALE != 'zh_CN') {

+  UI.addStyleSheet('./i18n_styles/en_options.css');

+}

+

+var HotKeySetting = (function() {

+  const CHAR_CODE_OF_AT = 64;

+  const CHAR_CODE_OF_A = 65;

+  const CHAR_CODE_OF_Z = 90;

+  var hotKeySelection = null;

+

+  var hotkey = {

+    setup: function() {

+      hotKeySelection = document.querySelectorAll('#hot-key-setting select');

+      // i18n.

+      $('area-capture-text').innerText =

+        chrome.i18n.getMessage('capture_area');

+      $('viewport-capture-text').innerText =

+        chrome.i18n.getMessage('capture_window');

+      $('full-page-capture-text').innerText =

+        chrome.i18n.getMessage('capture_webpage');

+      //$('screen-capture-text').innerText = chrome.i18n.getMessage('capture_screen');

+

+      for (var i = 0; i < hotKeySelection.length; i++) {

+        hotKeySelection[i].add(new Option('--', '@'));

+        for (var j = CHAR_CODE_OF_A; j <= CHAR_CODE_OF_Z; j++) {

+          var value = String.fromCharCode(j);

+          var option = new Option(value, value);

+          hotKeySelection[i].add(option);

+        }

+      }

+

+      $('area-capture-hot-key').selectedIndex =

+        HotKey.getCharCode('area') - CHAR_CODE_OF_AT;

+      $('viewport-capture-hot-key').selectedIndex =

+        HotKey.getCharCode('viewport') - CHAR_CODE_OF_AT;

+      $('full-page-capture-hot-key').selectedIndex =

+        HotKey.getCharCode('fullpage') - CHAR_CODE_OF_AT;

+      $('screen-capture-hot-key').selectedIndex =

+        HotKey.getCharCode('screen') - CHAR_CODE_OF_AT;

+

+      $('settingShortcut').addEventListener('click', function() {

+        hotkey.setState(this.checked);

+      }, false);

+

+      hotkey.setState(HotKey.isEnabled());

+    },

+

+    validate: function() {

+      var hotKeyLength =

+        Array.prototype.filter.call(hotKeySelection,

+            function (element) {

+              return element.value != '@'

+            }

+        ).length;

+      if (hotKeyLength != 0) {

+        var validateMap = {};

+        validateMap[hotKeySelection[0].value] = true;

+        validateMap[hotKeySelection[1].value] = true;

+        validateMap[hotKeySelection[2].value] = true;

+        if (hotKeyLength > 3 && hotKeySelection[3].value != '@') {

+          hotKeyLength -= 1;

+        }

+

+        if (Object.keys(validateMap).length < hotKeyLength) {

+          ErrorInfo.show('hot_key_conflict');

+          return false;

+        }

+      }

+      ErrorInfo.hide();

+      return true;

+    },

+

+    save: function() {

+      var result = true;

+      if ($('settingShortcut').checked) {

+        if (this.validate()) {

+          HotKey.enable();

+          HotKey.set('area', $('area-capture-hot-key').value);

+          HotKey.set('viewport', $('viewport-capture-hot-key').value);

+          HotKey.set('fullpage', $('full-page-capture-hot-key').value);

+        } else {

+          result = false;

+        }

+      } else {

+        HotKey.disable(bg);

+      }

+      return result;

+    },

+

+    setState: function(enabled) {

+      $('settingShortcut').checked = enabled;

+      UI.setStyle($('hot-key-setting'), 'color', enabled ? '' : '#6d6d6d');

+      for (var i = 0; i < hotKeySelection.length; i++) {

+        hotKeySelection[i].disabled = !enabled;

+      }

+      ErrorInfo.hide();

+    },

+

+    focusScreenCapture: function() {

+      $('screen-capture-hot-key').focus();

+    }

+  };

+  return hotkey;

+})();

+

+var ErrorInfo = (function() {

+  return {

+    show: function(msgKey) {

+      var infoWrapper = $('error-info');

+      var msg = chrome.i18n.getMessage(msgKey);

+      infoWrapper.innerText = msg;

+      UI.show(infoWrapper);

+    },

+

+    hide: function() {

+      var infoWrapper = $('error-info');

+      if (infoWrapper) {

+        UI.hide(infoWrapper);

+      }

+    }

+  };

+})();

+

+document.addEventListener('DOMContentLoaded', init);

diff --git a/src/js/page.js b/src/js/page.js
new file mode 100644
index 0000000..2c5702d
--- /dev/null
+++ b/src/js/page.js
@@ -0,0 +1,907 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.

+// Use of this source code is governed by a BSD-style license that can be

+// found in the LICENSE file.

+var page = {

+  startX: 150,

+  startY: 150,

+  endX: 400,

+  endY: 300,

+  moveX: 0,

+  moveY: 0,

+  pageWidth: 0,

+  pageHeight: 0,

+  visibleWidth: 0,

+  visibleHeight: 0,

+  dragging: false,

+  moving: false,

+  resizing: false,

+  isMouseDown: false,

+  scrollXCount: 0,

+  scrollYCount: 0,

+  scrollX: 0,

+  scrollY: 0,

+  captureWidth: 0,

+  captureHeight: 0,

+  isSelectionAreaTurnOn: false,

+  fixedElements_ : [],

+  marginTop: 0,

+  marginLeft: 0,

+  modifiedBottomRightFixedElements: [],

+  originalViewPortWidth: document.documentElement.clientWidth,

+  defaultScrollBarWidth: 17, // Default scroll bar width on windows platform.

+

+  hookBodyScrollValue: function(needHook) {

+    document.documentElement.setAttribute(

+        "__screen_capture_need_hook_scroll_value__", needHook);

+    var event = document.createEvent('Event');

+    event.initEvent('__screen_capture_check_hook_status_event__', true, true);

+    document.documentElement.dispatchEvent(event);

+  },

+

+  /**

+   * Determine if the page scrolled to bottom or right.

+   */

+  isScrollToPageEnd: function(coordinate) {

+    var body = document.body;

+    var docElement = document.documentElement;

+    if (coordinate == 'x')

+      return docElement.clientWidth + body.scrollLeft == body.scrollWidth;

+    else if (coordinate == 'y')

+      return docElement.clientHeight + body.scrollTop == body.scrollHeight;

+  },

+

+  /**

+   * Detect if the view port is located to the corner of page.

+   */

+  detectPagePosition: function() {

+    var body = document.body;

+    var pageScrollTop = body.scrollTop;

+    var pageScrollLeft = body.scrollLeft;

+    if (pageScrollTop == 0 && pageScrollLeft == 0) {

+      return 'top_left';

+    } else if (pageScrollTop == 0 && this.isScrollToPageEnd('x')) {

+      return 'top_right';

+    } else if (this.isScrollToPageEnd('y') && pageScrollLeft == 0) {

+      return 'bottom_left';

+    } else if (this.isScrollToPageEnd('y') && this.isScrollToPageEnd('x')) {

+      return 'bottom_right';

+    }

+    return null;

+  },

+

+  /**

+   * Detect fixed-positioned element's position in the view port.

+   * @param {Element} elem

+   * @return {String|Object} Return position of the element in the view port:

+   *   top_left, top_right, bottom_left, bottom_right, or null.

+   */

+  detectCapturePositionOfFixedElement: function(elem) {

+    var docElement = document.documentElement;

+    var viewPortWidth = docElement.clientWidth;

+    var viewPortHeight = docElement.clientHeight;

+    var offsetWidth = elem.offsetWidth;

+    var offsetHeight = elem.offsetHeight;

+    var offsetTop = elem.offsetTop;

+    var offsetLeft = elem.offsetLeft;

+    var result = [];

+

+    // Compare distance between element and the edge of view port to determine

+    // the capture position of element.

+    if (offsetTop <= viewPortHeight - offsetTop - offsetHeight) {

+      result.push('top');

+    } else if (offsetTop < viewPortHeight) {

+      result.push('bottom');

+    }

+    if (offsetLeft <= viewPortWidth - offsetLeft - offsetWidth) {

+      result.push('left');

+    } else if (offsetLeft < viewPortWidth) {

+      result.push('right');

+    }

+

+    // If the element is out of view port, then ignore.

+    if (result.length != 2)

+      return null;

+    return result.join('_');

+  },

+

+  restoreFixedElements: function() {

+    this.fixedElements_.forEach(function(element) {

+      element[1].style.visibility = 'visible';

+    });

+    this.fixedElements_ = [];

+  },

+

+  /**

+   * Iterate DOM tree and cache visible fixed-position elements.

+   */

+  cacheVisibleFixedPositionedElements: function() {

+    var nodeIterator = document.createNodeIterator(

+        document.documentElement,

+        NodeFilter.SHOW_ELEMENT,

+        null,

+        false

+    );

+    var currentNode;

+    while (currentNode = nodeIterator.nextNode()) {

+      var nodeComputedStyle =

+          document.defaultView.getComputedStyle(currentNode, "");

+      // Skip nodes which don't have computeStyle or are invisible.

+      if (!nodeComputedStyle)

+        continue;

+      if (nodeComputedStyle.position == "fixed" &&

+          nodeComputedStyle.display != 'none' &&

+          nodeComputedStyle.visibility != 'hidden') {

+        var position =

+          this.detectCapturePositionOfFixedElement(currentNode);

+        if (position)

+          this.fixedElements_.push([position, currentNode]);

+      }

+    }

+  },

+

+  // Handle fixed-position elements for capture.

+  handleFixedElements: function(capturePosition) {

+    var docElement = document.documentElement;

+    var body = document.body;

+

+    // If page has no scroll bar, then return directly.

+    if (docElement.clientHeight == body.scrollHeight &&

+        docElement.clientWidth == body.scrollWidth)

+      return;

+    

+    if (!this.fixedElements_.length) {

+      this.cacheVisibleFixedPositionedElements();

+    }

+

+    this.fixedElements_.forEach(function(element) {

+      if (element[0] == capturePosition)

+        element[1].style.visibility = 'visible';

+      else

+        element[1].style.visibility = 'hidden';

+    });

+  },

+

+  handleSecondToLastCapture: function() {

+    var docElement = document.documentElement;

+    var body = document.body;

+    var bottomPositionElements = [];

+    var rightPositionElements = [];

+    var that = this;

+    this.fixedElements_.forEach(function(element) {

+      var position = element[0];

+      if (position == 'bottom_left' || position == 'bottom_right') {

+        bottomPositionElements.push(element[1]);

+      } else if (position == 'bottom_right' || position == 'top_right') {

+        rightPositionElements.push(element[1]);

+      }

+    });

+

+    // Determine if the current capture is last but one.

+    var remainingCaptureHeight = body.scrollHeight - docElement.clientHeight -

+      body.scrollTop;

+    if (remainingCaptureHeight > 0 &&

+        remainingCaptureHeight < docElement.clientHeight) {

+      bottomPositionElements.forEach(function(element) {

+        if (element.offsetHeight > remainingCaptureHeight) {

+          element.style.visibility = 'visible';

+          var originalBottom = window.getComputedStyle(element).bottom;

+          that.modifiedBottomRightFixedElements.push(

+            ['bottom', element, originalBottom]);

+          element.style.bottom = -remainingCaptureHeight + 'px';

+        }

+      });

+    }

+

+    var remainingCaptureWidth = body.scrollWidth - docElement.clientWidth -

+      body.scrollLeft;

+    if (remainingCaptureWidth > 0 &&

+        remainingCaptureWidth < docElement.clientWidth) {

+      rightPositionElements.forEach(function(element) {

+        if (element.offsetWidth > remainingCaptureWidth) {

+          element.style.visibility = 'visible';

+          var originalRight = window.getComputedStyle(element).right;

+          that.modifiedBottomRightFixedElements.push(

+            ['right', element, originalRight]);

+          element.style.right = -remainingCaptureWidth + 'px';

+        }

+      });

+    }

+  },

+

+  restoreBottomRightOfFixedPositionElements: function() {

+    this.modifiedBottomRightFixedElements.forEach(function(data) {

+      var property = data[0];

+      var element = data[1];

+      var originalValue = data[2];

+      element.style[property] = originalValue;

+    });

+    this.modifiedBottomRightFixedElements = [];

+  },

+  

+  hideAllFixedPositionedElements: function() {

+    this.fixedElements_.forEach(function(element) {

+      element[1].style.visibility = 'hidden';

+    });

+  },

+

+  hasScrollBar: function(axis) {

+    var body = document.body;

+    var docElement = document.documentElement;

+    if (axis == 'x') {

+      if (window.getComputedStyle(body).overflowX == 'scroll')

+        return true;

+      return Math.abs(body.scrollWidth - docElement.clientWidth) >=

+          page.defaultScrollBarWidth;

+    } else if (axis == 'y') {

+      if (window.getComputedStyle(body).overflowY == 'scroll')

+        return true;

+      return Math.abs(body.scrollHeight - docElement.clientHeight) >=

+          page.defaultScrollBarWidth;

+    }

+  },

+

+  getOriginalViewPortWidth: function() {

+    chrome.extension.sendMessage({ msg: 'original_view_port_width'},

+      function(originalViewPortWidth) {

+        if (originalViewPortWidth) {

+          page.originalViewPortWidth = page.hasScrollBar('y') ?

+            originalViewPortWidth - page.defaultScrollBarWidth : originalViewPortWidth;

+        } else {

+          page.originalViewPortWidth = document.documentElement.clientWidth;

+        }

+      });

+  },

+  

+  calculateSizeAfterZooming: function(originalSize) {

+    var originalViewPortWidth = page.originalViewPortWidth;

+    var currentViewPortWidth = document.documentElement.clientWidth;

+    if (originalViewPortWidth == currentViewPortWidth)

+      return originalSize;

+    return Math.round(

+        originalViewPortWidth * originalSize / currentViewPortWidth);

+  },

+

+  getZoomLevel: function() {

+    return page.originalViewPortWidth / document.documentElement.clientWidth;

+  },

+

+  handleRightFloatBoxInGmail: function() {

+    var mainframe = document.getElementById('canvas_frame');

+    var boxContainer = document.querySelector('body > .dw');

+    var fBody = mainframe.contentDocument.body;

+    if (fBody.clientHeight + fBody.scrollTop == fBody.scrollHeight) {

+      boxContainer.style.display = 'block';

+    } else {

+      boxContainer.style.display = 'none';

+    }

+  },

+

+  getViewPortSize: function() {

+    var result = {

+      width: document.documentElement.clientWidth,

+      height: document.documentElement.clientHeight

+    };

+

+    if (document.compatMode == 'BackCompat') {

+      result.width = document.body.clientWidth;

+      result.height = document.body.clientHeight;

+    }

+

+    return result;

+  },

+

+  /**

+   * Check if the page is only made of invisible embed elements.

+   */

+  checkPageIsOnlyEmbedElement: function() {

+    var bodyNode = document.body.children;

+    var isOnlyEmbed = false;

+    for (var i = 0; i < bodyNode.length; i++) {

+      var tagName = bodyNode[i].tagName;

+      if (tagName == 'OBJECT' || tagName == 'EMBED' || tagName == 'VIDEO' ||

+          tagName == 'SCRIPT' || tagName == 'LINK') {

+        isOnlyEmbed = true;

+      } else if (bodyNode[i].style.display != 'none'){

+        isOnlyEmbed = false;

+        break;

+      }

+    }

+    return isOnlyEmbed;

+  },

+

+  isGMailPage: function(){

+    var hostName = window.location.hostname;

+    if (hostName == 'mail.google.com' &&

+        document.getElementById('canvas_frame')) {

+      return true;

+    }

+    return false;

+  },

+

+  /**

+  * Receive messages from background page, and then decide what to do next

+  */

+  addMessageListener: function() {

+    chrome.extension.onMessage.addListener(function(request, sender, response) {

+      if (page.isSelectionAreaTurnOn) {

+        page.removeSelectionArea();

+      }

+      switch (request.msg) {

+        case 'capture_window': response(page.getWindowSize()); break;

+        case 'show_selection_area': page.showSelectionArea(); break;

+        case 'scroll_init': // Capture whole page.

+          response(page.scrollInit(0, 0, document.body.scrollWidth,

+              document.body.scrollHeight, 'captureWhole'));

+          break;

+        case 'scroll_next':

+          page.visibleWidth = request.visibleWidth;

+          page.visibleHeight = request.visibleHeight;

+          response(page.scrollNext());

+          break;

+        case 'capture_selected':

+          response(page.scrollInit(

+              page.startX, page.startY,

+              page.calculateSizeAfterZooming(page.endX - page.startX),

+              page.calculateSizeAfterZooming(page.endY - page.startY),

+              'captureSelected'));

+          break;

+      }

+    });

+  },

+

+  /**

+  * Send Message to background page

+  */

+  sendMessage: function(message) {

+    chrome.extension.sendMessage(message);

+  },

+

+  /**

+  * Initialize scrollbar position, and get the data browser

+  */

+  scrollInit: function(startX, startY, canvasWidth, canvasHeight, type) {

+    this.hookBodyScrollValue(true);

+    page.captureHeight = canvasHeight;

+    page.captureWidth = canvasWidth;

+    var docWidth = document.body.scrollWidth;

+    var docHeight = document.body.scrollHeight;

+    window.scrollTo(startX, startY);

+

+    this.handleFixedElements('top_left');

+    this.handleSecondToLastCapture();

+

+    if (page.isGMailPage() && type == 'captureWhole') {

+      var frame = document.getElementById('canvas_frame');

+      docHeight = page.captureHeight = canvasHeight =

+          frame.contentDocument.height;

+      docWidth = page.captureWidth = canvasWidth = frame.contentDocument.width;

+      frame.contentDocument.body.scrollTop = 0;

+      frame.contentDocument.body.scrollLeft = 0;

+      page.handleRightFloatBoxInGmail();

+    }

+    page.scrollXCount = 0;

+    page.scrollYCount = 1;

+    page.scrollX = window.scrollX; // document.body.scrollLeft

+    page.scrollY = window.scrollY;

+    var viewPortSize = page.getViewPortSize();

+    return {

+      'msg': 'scroll_init_done',

+      'startX': page.calculateSizeAfterZooming(startX),

+      'startY': page.calculateSizeAfterZooming(startY),

+      'scrollX': window.scrollX,

+      'scrollY': window.scrollY,

+      'docHeight': docHeight,

+      'docWidth': docWidth,

+      'visibleWidth': viewPortSize.width,

+      'visibleHeight': viewPortSize.height,

+      'canvasWidth': canvasWidth,

+      'canvasHeight': canvasHeight,

+      'scrollXCount': 0,

+      'scrollYCount': 0,

+      'zoom': page.getZoomLevel()

+    };

+  },

+

+  /**

+  * Calculate the next position of the scrollbar

+  */

+  scrollNext: function() {

+    if (page.scrollYCount * page.visibleWidth >= page.captureWidth) {

+      page.scrollXCount++;

+      page.scrollYCount = 0;

+    }

+    if (page.scrollXCount * page.visibleHeight < page.captureHeight) {

+      this.restoreBottomRightOfFixedPositionElements();

+      var viewPortSize = page.getViewPortSize();

+      window.scrollTo(

+          page.scrollYCount * viewPortSize.width + page.scrollX,

+          page.scrollXCount * viewPortSize.height + page.scrollY);

+

+      var pagePosition = this.detectPagePosition();

+      if (pagePosition) {

+        this.handleFixedElements(pagePosition);

+      } else {

+        this.hideAllFixedPositionedElements();

+      }

+      this.handleSecondToLastCapture();

+

+      if (page.isGMailPage()) {

+        var frame = document.getElementById('canvas_frame');

+        frame.contentDocument.body.scrollLeft =

+            page.scrollYCount * viewPortSize.width;

+        frame.contentDocument.body.scrollTop =

+            page.scrollXCount * viewPortSize.height;

+        page.handleRightFloatBoxInGmail();

+      }

+      var x = page.scrollXCount;

+      var y = page.scrollYCount;

+      page.scrollYCount++;

+      return { msg: 'scroll_next_done',scrollXCount: x, scrollYCount: y };

+    }  else {

+      window.scrollTo(page.startX, page.startY);

+      this.restoreFixedElements();

+      this.hookBodyScrollValue(false);

+      return {'msg': 'scroll_finished'};

+    }

+  },

+

+  /**

+  * Show the selection Area

+  */

+  showSelectionArea: function() {

+    page.createFloatLayer();

+    setTimeout(page.createSelectionArea, 100);

+  },

+

+  getWindowSize: function() {

+    var docWidth = document.body.clientWidth;

+    var docHeight = document.body.clientHeight;

+    if (page.isGMailPage()) {

+      var frame = document.getElementById('canvas_frame');

+      docHeight = frame.contentDocument.height;

+      docWidth = frame.contentDocument.width;

+    }

+    return {'msg':'capture_window',

+            'docWidth': docWidth,

+            'docHeight': docHeight};

+  },

+

+  getSelectionSize: function() {

+    page.removeSelectionArea();

+    setTimeout(function() {

+      page.sendMessage({

+        'msg': 'capture_selected',

+        'x': page.startX,

+        'y': page.startY,

+        'width': page.endX - page.startX,

+        'height': page.endY - page.startY,

+        'visibleWidth': document.documentElement.clientWidth,

+        'visibleHeight': document.documentElement.clientHeight,

+        'docWidth': document.body.clientWidth,

+        'docHeight': document.body.clientHeight

+      })}, 100);

+  },

+

+  /**

+  * Create a float layer on the webpage

+  */

+  createFloatLayer: function() {

+    page.createDiv(document.body, 'sc_drag_area_protector');

+  },

+  

+  matchMarginValue: function(str) {

+    return str.match(/\d+/);

+  },

+

+  /**

+  * Load the screenshot area interface

+  */

+  createSelectionArea: function() {

+    var areaProtector = $('sc_drag_area_protector');

+    var zoom = page.getZoomLevel();

+    var bodyStyle = window.getComputedStyle(document.body, null);

+    if ('relative' == bodyStyle['position']) {

+      page.marginTop = page.matchMarginValue(bodyStyle['marginTop']);

+      page.marginLeft = page.matchMarginValue(bodyStyle['marginLeft']);

+      areaProtector.style.top =  - parseInt(page.marginTop) + 'px';

+      areaProtector.style.left =  - parseInt(page.marginLeft) + 'px';

+    }

+    areaProtector.style.width =

+      Math.round((document.body.clientWidth + parseInt(page.marginLeft)) / zoom) + 'px';

+    areaProtector.style.height =

+      Math.round((document.body.clientHeight + parseInt(page.marginTop)) / zoom) + 'px';

+    areaProtector.onclick = function() {

+      event.stopPropagation();

+      return false;

+    };

+

+    // Create elements for area capture.

+    page.createDiv(areaProtector, 'sc_drag_shadow_top');

+    page.createDiv(areaProtector, 'sc_drag_shadow_bottom');

+    page.createDiv(areaProtector, 'sc_drag_shadow_left');

+    page.createDiv(areaProtector, 'sc_drag_shadow_right');

+

+    var areaElement = page.createDiv(areaProtector, 'sc_drag_area');

+    page.createDiv(areaElement, 'sc_drag_container');

+    page.createDiv(areaElement, 'sc_drag_size');

+

+    // Add event listener for 'cancel' and 'capture' button.

+    var cancel = page.createDiv(areaElement, 'sc_drag_cancel');

+    cancel.addEventListener('mousedown', function () {

+      // Remove area capture containers and event listeners.

+      page.removeSelectionArea();

+    }, true);

+    cancel.innerHTML = chrome.i18n.getMessage("cancel");

+

+    var crop = page.createDiv(areaElement, 'sc_drag_crop');

+    crop.addEventListener('mousedown', function() {

+      page.removeSelectionArea();

+      page.sendMessage({msg: 'capture_selected'});

+    }, false);

+    crop.innerHTML = chrome.i18n.getMessage('ok');

+

+    page.createDiv(areaElement, 'sc_drag_north_west');

+    page.createDiv(areaElement, 'sc_drag_north_east');

+    page.createDiv(areaElement, 'sc_drag_south_east');

+    page.createDiv(areaElement, 'sc_drag_south_west');

+

+    areaProtector.addEventListener('mousedown', page.onMouseDown, false);

+    document.addEventListener('mousemove', page.onMouseMove, false);

+    document.addEventListener('mouseup', page.onMouseUp, false);

+    $('sc_drag_container').addEventListener('dblclick', function() {

+      page.removeSelectionArea();

+      page.sendMessage({msg: 'capture_selected'});

+    }, false);

+

+    page.pageHeight = $('sc_drag_area_protector').clientHeight;

+    page.pageWidth = $('sc_drag_area_protector').clientWidth;

+

+    var areaElement = $('sc_drag_area');

+    areaElement.style.left = page.getElementLeft(areaElement) + 'px';

+    areaElement.style.top = page.getElementTop(areaElement) + 'px';

+    

+    page.startX = page.getElementLeft(areaElement);

+    page.startY = page.getElementTop(areaElement); 

+    page.endX = page.getElementLeft(areaElement) + 250;

+    page.endY = page.getElementTop(areaElement) + 150;

+    

+    areaElement.style.width = '250px';

+    areaElement.style.height = '150px';

+    page.isSelectionAreaTurnOn = true;

+    page.updateShadow(areaElement);

+    page.updateSize();

+  },

+  

+  getElementLeft: function(obj) {

+    return (document.body.scrollLeft +

+        (document.documentElement.clientWidth - 

+        obj.offsetWidth) / 2);

+  },

+  

+  getElementTop: function(obj) {

+    return (document.body.scrollTop + 

+        (document.documentElement.clientHeight - 200 - 

+        obj.offsetHeight) / 2);

+  },

+

+  /**

+  * Init selection area due to the position of the mouse when mouse down

+  */

+  onMouseDown: function() {

+    if (event.button != 2) {

+      var element = event.target;

+

+      if (element) {

+        var elementName = element.tagName;

+        if (elementName && document) {

+          page.isMouseDown = true;

+

+          var areaElement = $('sc_drag_area');

+          var xPosition = event.pageX;

+          var yPosition = event.pageY;

+

+          if (areaElement) {

+            if (element == $('sc_drag_container')) {

+              page.moving = true;

+              page.moveX = xPosition - areaElement.offsetLeft;

+              page.moveY = yPosition - areaElement.offsetTop;

+            } else if (element == $('sc_drag_north_east')) {

+              page.resizing = true;

+              page.startX = areaElement.offsetLeft;

+              page.startY = areaElement.offsetTop + areaElement.clientHeight;

+            } else if (element == $('sc_drag_north_west')) {

+              page.resizing = true;

+              page.startX = areaElement.offsetLeft + areaElement.clientWidth;

+              page.startY = areaElement.offsetTop + areaElement.clientHeight;

+            } else if (element == $('sc_drag_south_east')) {

+              page.resizing = true;

+              page.startX = areaElement.offsetLeft;

+              page.startY = areaElement.offsetTop;

+            } else if (element == $('sc_drag_south_west')) {

+              page.resizing = true;

+              page.startX = areaElement.offsetLeft + areaElement.clientWidth;

+              page.startY = areaElement.offsetTop;

+            } else {

+              page.dragging = true;

+              page.endX = 0;

+              page.endY = 0;

+              page.endX = page.startX = xPosition;

+              page.endY = page.startY = yPosition;

+            }

+          }

+          event.preventDefault();

+        }

+      }

+    }

+  },

+

+  /**

+  * Change selection area position when mouse moved

+  */

+  onMouseMove: function() {

+    var element = event.target;

+    if (element && page.isMouseDown) {

+      var areaElement = $('sc_drag_area');

+      if (areaElement) {

+        var xPosition = event.pageX;

+        var yPosition = event.pageY;

+        if (page.dragging || page.resizing) {

+          var width = 0;

+          var height = 0;

+          var zoom = page.getZoomLevel();

+          var viewWidth = Math.round(document.body.clientWidth / zoom);

+          var viewHeight = Math.round(document.body.clientHeight / zoom);

+          if (xPosition > viewWidth) {

+            xPosition = viewWidth;

+          } else if (xPosition < 0) {

+            xPosition = 0;

+          }

+          if (yPosition > viewHeight) {

+            yPosition = viewHeight;

+          } else if (yPosition < 0) {

+            yPosition = 0;

+          }

+          page.endX = xPosition;

+          page.endY = yPosition;

+          if (page.startX > page.endX) {

+            width = page.startX - page.endX;

+            areaElement.style.left = xPosition + 'px';

+          } else {

+            width = page.endX - page.startX;

+            areaElement.style.left = page.startX + 'px';

+          }

+          if (page.startY > page.endY) {

+            height = page.startY - page.endY;

+            areaElement.style.top = page.endY + 'px';

+          } else {

+            height = page.endY - page.startY;

+            areaElement.style.top = page.startY + 'px';

+          }

+          areaElement.style.height = height + 'px';

+          areaElement.style.width  = width + 'px';

+          if (window.innerWidth < xPosition) {

+            document.body.scrollLeft = xPosition - window.innerWidth;

+          }

+          if (document.body.scrollTop + window.innerHeight < yPosition + 25) {

+            document.body.scrollTop = yPosition - window.innerHeight + 25;

+          }

+          if (yPosition < document.body.scrollTop) {

+            document.body.scrollTop -= 25;

+          }

+        } else if (page.moving) {

+          var newXPosition = xPosition - page.moveX;

+          var newYPosition = yPosition - page.moveY;

+          if (newXPosition < 0) {

+            newXPosition = 0;

+          } else if (newXPosition + areaElement.clientWidth > page.pageWidth) {

+            newXPosition = page.pageWidth - areaElement.clientWidth;

+          }

+          if (newYPosition < 0) {

+            newYPosition = 0;

+          } else if (newYPosition + areaElement.clientHeight >

+                     page.pageHeight) {

+            newYPosition = page.pageHeight - areaElement.clientHeight;

+          }

+

+          areaElement.style.left = newXPosition + 'px';

+          areaElement.style.top = newYPosition + 'px';

+          page.endX = newXPosition + areaElement.clientWidth;

+          page.startX = newXPosition;

+          page.endY = newYPosition + areaElement.clientHeight;

+          page.startY = newYPosition;

+

+        }

+        var crop = document.getElementById('sc_drag_crop');

+        var cancel = document.getElementById('sc_drag_cancel');

+        if (event.pageY + 25 > document.body.clientHeight) {

+          crop.style.bottom = 0;

+          cancel.style.bottom = 0

+        } else {

+          crop.style.bottom = '-25px';

+          cancel.style.bottom = '-25px';

+        }

+

+        var dragSizeContainer = document.getElementById('sc_drag_size');

+        if (event.pageY < 18) {

+          dragSizeContainer.style.top = 0;

+        } else {

+          dragSizeContainer.style.top = '-18px';

+        }

+        page.updateShadow(areaElement);

+        page.updateSize();

+

+      }

+    }

+  },

+

+ /**

+  * Fix the selection area position when mouse up

+  */

+  onMouseUp: function() {

+    page.isMouseDown = false;

+    if (event.button != 2) {

+      page.resizing = false;

+      page.dragging = false;

+      page.moving = false;

+      page.moveX = 0;

+      page.moveY = 0;

+      var temp;

+      if (page.endX < page.startX) {

+        temp = page.endX;

+        page.endX = page.startX;

+        page.startX = temp;

+      }

+      if (page.endY < page.startY) {

+        temp = page.endY;

+        page.endY = page.startY;

+        page.startY = temp;

+      }

+    }

+  },

+

+  /**

+  * Update the location of the shadow layer

+  */

+  updateShadow: function(areaElement) {

+    $('sc_drag_shadow_top').style.height =

+        parseInt(areaElement.style.top) + 'px';

+    $('sc_drag_shadow_top').style.width = (parseInt(areaElement.style.left) +

+        parseInt(areaElement.style.width) + 1) + 'px';

+    $('sc_drag_shadow_left').style.height =

+        (page.pageHeight - parseInt(areaElement.style.top)) + 'px';

+    $('sc_drag_shadow_left').style.width =

+        parseInt(areaElement.style.left) + 'px';

+

+    var height = (parseInt(areaElement.style.top) +

+        parseInt(areaElement.style.height) + 1);

+    height = (height < 0) ? 0 : height;

+    var width = (page.pageWidth) - 1 - (parseInt(areaElement.style.left) +

+        parseInt(areaElement.style.width));

+    width = (width < 0) ? 0 : width;

+    $('sc_drag_shadow_right').style.height = height + 'px';

+    $('sc_drag_shadow_right').style.width =  width + 'px';

+

+    height = (page.pageHeight - 1 - (parseInt(areaElement.style.top) +

+        parseInt(areaElement.style.height)));

+    height = (height < 0) ? 0 : height;

+    width = (page.pageWidth) - parseInt(areaElement.style.left);

+    width = (width < 0) ? 0 : width;

+    $('sc_drag_shadow_bottom').style.height = height + 'px';

+    $('sc_drag_shadow_bottom').style.width = width + 'px';

+  },

+

+  /**

+  * Remove selection area

+  */

+  removeSelectionArea: function() {

+    document.removeEventListener('mousedown', page.onMouseDown, false);

+    document.removeEventListener('mousemove', page.onMouseMove, false);

+    document.removeEventListener('mouseup', page.onMouseUp, false);

+    $('sc_drag_container').removeEventListener('dblclick',function() {

+      page.removeSelectionArea();

+      page.sendMessage({msg: 'capture_selected'});}, false);

+    page.removeElement('sc_drag_area_protector');

+    page.removeElement('sc_drag_area');

+    page.isSelectionAreaTurnOn = false;

+  },

+

+  /**

+  * Refresh the size info

+  */

+  updateSize: function() {

+    var width = Math.abs(page.endX - page.startX);

+    var height = Math.abs(page.endY - page.startY);

+    $('sc_drag_size').innerText = page.calculateSizeAfterZooming(width) +

+      ' x ' + page.calculateSizeAfterZooming(height);

+  },

+

+  /**

+  * create div

+  */

+  createDiv: function(parent, id) {

+    var divElement = document.createElement('div');

+    divElement.id = id;

+    parent.appendChild(divElement);

+    return divElement;

+  },

+

+  /**

+  * Remove an element

+  */

+  removeElement: function(id) {

+    if($(id)) {

+      $(id).parentNode.removeChild($(id));

+    }

+  },

+

+  injectCssResource: function(cssResource) {

+    var css = document.createElement('LINK');

+    css.type = 'text/css';

+    css.rel = 'stylesheet';

+    css.href = chrome.extension.getURL(cssResource);

+    (document.head || document.body || document.documentElement).

+        appendChild(css);

+  },

+

+  injectJavaScriptResource: function(scriptResource) {

+    var script = document.createElement("script");

+    script.type = "text/javascript";

+    script.charset = "utf-8";

+    script.src = chrome.extension.getURL(scriptResource);

+    (document.head || document.body || document.documentElement).

+        appendChild(script);

+  },

+

+  /**

+  * Remove an element

+  */

+  init: function() { 

+    if (document.body.hasAttribute('screen_capture_injected')) {

+      return;

+    }

+    if (isPageCapturable()) {

+      chrome.extension.sendMessage({msg: 'page_capturable'});

+    } else {

+      chrome.extension.sendMessage({msg: 'page_uncapturable'});

+    }

+    this.injectCssResource('style.css');

+    this.addMessageListener();

+    this.injectJavaScriptResource("js/page_context.js");

+

+    // Retrieve original width of view port and cache.

+    page.getOriginalViewPortWidth();

+  }

+};

+

+/**

+ * Indicate if the current page can be captured.

+ */

+var isPageCapturable = function() {

+  return !page.checkPageIsOnlyEmbedElement();

+};

+

+function $(id) {

+  return document.getElementById(id);

+}

+

+page.init();

+

+window.addEventListener('resize', function() {

+  if (page.isSelectionAreaTurnOn) {

+    page.removeSelectionArea();

+    page.showSelectionArea();

+  }

+

+  // Reget original width of view port if browser window resized or page zoomed.

+  page.getOriginalViewPortWidth();

+}, false);

+

+// Send page url for retriving and parsing access token for facebook and picasa.

+var message = {

+  msg: 'url_for_access_token',

+  url: window.location.href

+}

+if (window.location.href.indexOf('https://api.weibo.com/oauth2/default.html') == 0) {

+  message.siteId = 'sina'

+}

+page.sendMessage(message);

diff --git a/src/js/page_context.js b/src/js/page_context.js
new file mode 100644
index 0000000..ef35563
--- /dev/null
+++ b/src/js/page_context.js
@@ -0,0 +1,300 @@
+var __screenCapturePageContext__ = {

+  clone: function(object) {

+    function StubObj() { }

+    StubObj.prototype = object;

+    var newObj = new StubObj();

+    newObj.getInternalObject = function() {

+      return this.__proto__;

+    }

+    newObj.toString = function() {

+      try {

+        return this.__proto__.toString();

+      } catch (e) {

+        return 'object Object';

+      }

+    }

+    return newObj;

+  },

+

+  bind: function(newThis, func) {

+    var args = [];

+    for(var i = 2;i < arguments.length; i++) {

+      args.push(arguments[i]);

+    }

+    return function() {

+      return func.apply(newThis, args);

+    }

+  },

+

+  bodyWrapperDelegate_: null,

+  currentHookStatus_: false,

+

+  scrollValueHooker: function(oldValue, newValue, reason) {

+    // When we hook the value of scrollLeft/Top of body, it always returns 0.

+    return 0;

+  },

+

+  toggleBodyScrollValueHookStatus: function() {

+    this.currentHookStatus_ = !this.currentHookStatus_;

+    if (this.currentHookStatus_) {

+      var This = this;

+      try {

+        document.__defineGetter__('body', function() {

+          return This.bodyWrapperDelegate_.getWrapper();

+        });

+      } catch (e) {

+        window.console.log('error' + e);

+      }

+      this.bodyWrapperDelegate_.watch('scrollLeft', this.scrollValueHooker);

+      this.bodyWrapperDelegate_.watch('scrollTop', this.scrollValueHooker);

+    } else {

+      this.bodyWrapperDelegate_.unwatch('scrollLeft', this.scrollValueHooker);

+      this.bodyWrapperDelegate_.unwatch('scrollTop', this.scrollValueHooker);

+      var This = this;

+      try {

+        document.__defineGetter__('body', function() {

+          return This.bodyWrapperDelegate_.getWrapper().getInternalObject();

+        });

+      } catch (e) {

+        window.console.log('error' + e);

+      }

+    }

+  },

+

+  checkHookStatus: function() {

+    var needHookScrollValue = document.documentElement.getAttributeNode(

+        '__screen_capture_need_hook_scroll_value__');

+    needHookScrollValue =

+        !!(needHookScrollValue && needHookScrollValue.nodeValue == 'true');

+    if (this.currentHookStatus_ != needHookScrollValue)

+      this.toggleBodyScrollValueHookStatus();

+  },

+

+  init: function() {

+    if (!this.bodyWrapperDelegate_) {

+      this.bodyWrapperDelegate_ =

+          new __screenCapturePageContext__.ObjectWrapDelegate(

+              document.body, '^(DOCUMENT_[A-Z_]+|[A-Z_]+_NODE)$');

+      document.documentElement.addEventListener(

+          '__screen_capture_check_hook_status_event__',

+          __screenCapturePageContext__.bind(this, this.checkHookStatus));

+    }

+  }

+};

+

+// ObjectWrapDelegate class will create a empty object(wrapper), map its

+// prototype to the 'originalObject', then search all non-function properties

+// (except those properties which match the propertyNameFilter) of the

+// 'orginalObject' and set corresponding getter/setter to the wrapper.

+// Then you can manipulate the wrapper as 'originalObject' because the wrapper

+// use the corresponding getter/setter to access the corresponding properties in

+// 'originalObject' and the all function calls can be call through the prototype

+// inherit.

+// After createing the wrapper object, you can use watch method to monitor any

+// property which you want to know when it has been read(get) or change(set).

+// Please see the detail comment on method watch.

+// Remember the ObjectWrapDelegate returns the wrapDelegateObject instead of

+// really wrapper object. You have to use ObjectWrapDelegate.getWrapper to get

+// real wrapper object.

+// parameter @originalObject, object which you want to wrap

+// parameter @propertyNameFilter, string, regular expression pattern string for

+//    those properties you don't put in the wrap object.

+__screenCapturePageContext__.ObjectWrapDelegate = function(

+    originalObject, propertyNameFilter) {

+  this.window_ = window;

+

+  // The wrapper is the object we use to wrap the 'originalObject'.

+  this.wrapper_ = __screenCapturePageContext__.clone(originalObject);

+

+  // This array saves all properties we set our getter/setter for them.

+  this.properties_ = [];

+

+  // This object to save all watch handlers. Each watch handler is bind to one

+  // certain property which is in properties_.

+  this.watcherTable_ = {};

+

+  // Check the propertyNameFilter parameter.

+  if (typeof propertyNameFilter == 'undefined') {

+    propertyNameFilter = '';

+  } else if (typeof propertyNameFilter != 'string') {

+    try {

+      propertyNameFilter = propertyNameFilter.toString();

+    } catch (e) {

+      propertyNameFilter = '';

+    }

+  }

+  if (propertyNameFilter.length) {

+    this.propertyNameFilter_ = new RegExp('');

+    this.propertyNameFilter_.compile(propertyNameFilter);

+  } else {

+    this.propertyNameFilter_ = null;

+  }

+

+  // For closure to access the private data of class.

+  var This = this;

+

+  // Set the getter object.

+  function setGetterAndSetter(wrapper, propertyName) {

+    wrapper.__defineGetter__(propertyName, function() {

+      var internalObj = this.getInternalObject();

+      var originalReturnValue = internalObj[propertyName];

+      var returnValue = originalReturnValue;

+

+      // See whether this property has been watched.

+      var watchers = This.watcherTable_[propertyName];

+      if (watchers) {

+        // copy the watcher to a cache in case someone call unwatch inside the

+        // watchHandler.

+        var watchersCache = watchers.concat();

+        for (var i = 0, l = watchersCache.length; i < l; ++i) {

+          var watcher = watchersCache[i];

+          if (!watcher) {

+            window.console.log('wrapper\'s watch for ' + propertyName +

+                              ' is unavailable!');

+            continue;  // should never happend

+          }

+          originalReturnValue = returnValue;

+          try {

+            returnValue = watcher(returnValue, returnValue, 'get');

+          } catch (e) {

+            returnValue = originalReturnValue;

+          }

+        }

+      }

+      return returnValue;

+    });

+

+    // Set the setter object.

+    wrapper.__defineSetter__(propertyName, function(value) {

+      var internalObj = this.getInternalObject();

+      var originalValue = value;

+      var userValue = originalValue;

+      var oldValue;

+      try {

+        oldValue = internalObj[propertyName];

+      } catch (e) {

+        oldValue = null;

+      }

+

+      // See whether this property has been watched.

+      var watchers = This.watcherTable_[propertyName];

+      if (watchers) {

+        // copy the watcher to a cache in case someone call unwatch inside the

+        // watchHandler.

+        var watchersCache = watchers.concat();

+        for (var i = 0, l = watchersCache.length; i < l; ++i) {

+          var watcher = watchersCache[i];

+          if (!watcher) {

+            window.console.log('wrapper\'s watch for ' + propertyName +

+                              ' is unavailable!');

+            continue;  // should never happend

+          }

+          originalValue = userValue;

+          try {

+            userValue = watcher(oldValue, userValue, 'set');

+          } catch (e) {

+            userValue = originalValue;

+          }

+        }

+      }

+      internalObj[propertyName] = userValue;

+    });

+  };

+

+  this.cleanUp_ = function() {

+    This.window_.removeEventListener('unload', This.cleanUp_, false);

+

+    // Delete all properties

+    for (var i = 0, l = This.properties_.length; i < l; ++i) {

+      delete This.wrapper_[This.properties_[i]];

+    }

+    This.window_ = null;

+    This.wrapper_ = null;

+    This.properties_ = null;

+    This.watcherTable_ = null;

+    This.propertyNameFilter_ = null;

+    This = null;

+  }

+

+  // We only bridge the non-function properties.

+  for (var prop in originalObject) {

+    if (this.propertyNameFilter_ && this.propertyNameFilter_.test(prop)) {

+      this.propertyNameFilter_.test('');

+      continue;

+    }

+    if (typeof originalObject[prop] != 'function') {

+      this.properties_.push(prop);

+      setGetterAndSetter(this.wrapper_, prop);

+    }

+  }

+

+  // Listen the unload event.

+  this.window_.addEventListener('unload', this.cleanUp_, false);

+};

+

+__screenCapturePageContext__.ObjectWrapDelegate.prototype.getWrapper =

+    function() {

+  return this.wrapper_;

+}

+

+// Check whether a property is in the wrapper or not. If yes, return true.

+// Otherwise return false.

+__screenCapturePageContext__.ObjectWrapDelegate.prototype.hasProperty =

+    function(propertyName) {

+  for (var i = 0, l = this.properties_.length; i < l; ++i) {

+    if (propertyName == this.properties_[i])

+      return true;

+  }

+  return false;

+}

+

+// Watches for a property to be accessed or be assigned a value and runs a

+// function when that occurs.

+// Watches for accessing a property or assignment to a property named prop in

+// this object, calling handler(oldval, newval, reason) whenever prop is

+// get/set and storing the return value in that property.

+// A watchpoint can filter (or nullify) the value assignment, by returning a

+// modified newval (or by returning oldval).

+// When watchpoint is trigering by get opeartor, the oldval is equal with

+// newval. The reason will be 'get'.

+// When watchpoint is trigering by set opeartor, The reason will be 'set'.

+// If you delete a property for which a watchpoint has been set,

+// that watchpoint does not disappear. If you later recreate the property,

+// the watchpoint is still in effect.

+// To remove a watchpoint, use the unwatch method.

+// If register the watchpoint successfully, return true. Otherwise return false.

+__screenCapturePageContext__.ObjectWrapDelegate.prototype.watch = function(

+    propertyName, watchHandler) {

+  if (!this.hasProperty(propertyName))

+    return false;

+  var watchers = this.watcherTable_[propertyName];

+  if (watchers) {

+    for (var i = 0, l = watchers.length; i < l; ++i) {

+      if (watchHandler == watchers[i])

+        return true;

+    }

+  } else {

+    watchers = new Array();

+    this.watcherTable_[propertyName] = watchers;

+  }

+  watchers.push(watchHandler);

+  return true;

+}

+

+// Removes a watchpoint set with the watch method.

+__screenCapturePageContext__.ObjectWrapDelegate.prototype.unwatch = function(

+    propertyName, watchHandler) {

+  if (!this.hasProperty(propertyName))

+    return false;

+  var watchers = this.watcherTable_[propertyName];

+  if (watchers) {

+    for (var i = 0, l = watchers.length; i < l; ++i) {

+      if (watchHandler == watchers[i]) {

+        watchers.splice(i, 1);

+        return true;

+      }

+    }

+  }

+  return false;

+}

+__screenCapturePageContext__.init();

diff --git a/src/js/picasa.js b/src/js/picasa.js
new file mode 100644
index 0000000..e6343a8
--- /dev/null
+++ b/src/js/picasa.js
@@ -0,0 +1,237 @@
+(function() {

+  const ALBUM_NAME = 'Screen Capture';

+  const CLIENT_ID = '368358534491.apps.googleusercontent.com';

+  const AUTH_URL = 'https://accounts.google.com/o/oauth2/auth';

+  const REDIRECT_URI = 'https://picasaweb.google.com';

+  const SCOPE = 'https://picasaweb.google.com/data/';

+  const RESPONSE_TYPE = 'token';

+  const ALBUM_URL = 'https://picasaweb.google.com/data/feed/api/user/' +

+    'default';

+  const CREATE_ALBUM_URL = 'https://picasaweb.google.com/data/feed/api/user/' +

+    'default';

+  const UPLOAD_BASE_URL = 'https://picasaweb.google.com/data/feed/api/user/' +

+    'default/albumid/';

+  const LOGOUT_URL = 'https://picasaweb.google.com/bye?continue=' +

+    'https://www.google.com/accounts/Logout?continue=' +

+    'https://picasaweb.google.com';

+

+  // var picasa = window.Picasa = new Site('picasa');

+  var picasa = window.Picasa = {

+    siteId: 'picasa',

+    currentUserId: null,

+    redirectUrl: REDIRECT_URI,

+    accessTokenCallback: null,

+    

+    getAccessToken: function(callback) {

+      picasa.accessTokenCallback = callback;

+      var url = AUTH_URL + '?client_id=' + CLIENT_ID + '&redirect_uri=' +

+        REDIRECT_URI + '&scope=' + SCOPE + '&response_type=' + RESPONSE_TYPE;

+      chrome.tabs.create({ url: url});

+    },

+

+    parseRedirectUrl: function(url) {

+      var result = false;

+      if (url.indexOf(REDIRECT_URI) == 0) {

+        var hash = url.split('#')[1];

+        if (hash) {

+          var params = hash.split('&');

+          var paramMap = {};

+          params.forEach(function(param) {

+            paramMap[param.split('=')[0]] = param.split('=')[1];

+          });

+

+          var accessToken = paramMap['access_token'];

+          var expires = paramMap['expires_in'];

+          if (accessToken && expires) {

+            result = {

+              accessToken: accessToken,

+              expires: expires

+            };

+          } else {

+            result = 'bad_redirect_url'; // Should never happened.

+          }

+        } else {

+          var search = url.split('?')[1];

+          if (search == 'error=access_denied')

+            result = 'access_denied';

+        }

+      }

+      return result;

+    },

+    

+    isRedirectUrl: function(url) {

+      return picasa.parseRedirectUrl(url) != false;

+    },

+    

+    parseAccessToken: function(redirectUrl) {

+      var parsedResult = picasa.parseRedirectUrl(redirectUrl);

+      if (parsedResult && typeof parsedResult == 'object') {

+        var user = new User({

+          accessToken: parsedResult.accessToken,

+          expires: new Date().getTime() + parsedResult.expires * 1000

+        });

+        picasa.accessTokenCallback('success', user);

+      } else {

+        picasa.accessTokenCallback('failure', 'user_denied');

+      }

+      picasa.accessTokenCallback = null;

+    },

+    

+    getUserInfo: function(user, callback) {

+      ajax({

+        url: ALBUM_URL,

+        parameters: {

+          fields: 'title,gphoto:nickname,entry/title,entry/gphoto:id',

+          alt: 'json',

+          access_token: user.accessToken

+        },

+        success: function(res) {

+          var userId = res.feed.title.$t;

+          var userName = res.feed.gphoto$nickname.$t;

+          user.id = userId;

+          user.name = userName;

+          

+          var albums = res.feed.entry;

+          if (albums) {

+            var length = albums.length;

+

+            // Check if user has created album "Screen Capture".

+            for (var i = 0; i < length; i++) {

+              var albumName = albums[i].title.$t;

+              if (albumName == ALBUM_NAME) {

+                user.albumId = albums[i].gphoto$id.$t;

+                break;

+              }

+            }

+          }

+

+          // Create album "Screen Capture" and retrieve album id.

+          if (!user.albumId) {

+            picasa.createAlbum(user.accessToken, function(result,

+                                                          albumIdOrMessage) {

+              if (result == 'success') {

+                user.albumId = albumIdOrMessage;

+                callback('success', user);

+              } else {

+                callback('failure', albumIdOrMessage);

+              }

+            });

+          } else {

+            callback('success', user);

+          }

+        },

+        status: {

+          404: function() {

+            callback('failure', 'failed_to_get_user_info');

+          }

+        }

+      });

+    },

+

+    createAlbum: function(accessToken, callback) {

+      var data = '<entry xmlns="http://www.w3.org/2005/Atom" ' +

+        'xmlns:media="http://search.yahoo.com/mrss/" ' +

+        'xmlns:gphoto="http://schemas.google.com/photos/2007">' +

+        '<title type="text">' + ALBUM_NAME +

+        '</title><category scheme="http://schemas.google.com/g/2005#kind" ' +

+        'term="http://schemas.google.com/photos/2007#album"></category>' +

+        '</entry>';

+

+      ajax({

+        method: 'POST',

+        url: CREATE_ALBUM_URL,

+        parameters: {

+          alt: 'json'

+        },

+        data: data,

+        headers: {

+          'GData-Version': 2,

+          'Content-Type': 'application/atom+xml',

+          'Authorization': 'OAuth ' + accessToken

+        },

+        complete: function(statusCode, album) {

+          if (statusCode == 201) {

+            var albumId = album.entry.gphoto$id.$t;

+            callback('success', albumId);

+          } else {

+            callback('failure', 'failure_to_create_album')

+          }

+        }

+      });

+    },

+

+    upload: function(user, caption, imageData, callback) {

+      caption = ajax.convertEntityString(caption);

+      caption = ajax.encodeForBinary(caption);

+

+      var imageFile = new Date().getTime() + '.png';

+      var headers = {

+        'GData-Version': 2,

+        'Content-Type': 'multipart/related; boundary=' +

+          MULTIPART_FORMDATA_BOUNDARY,

+        'Authorization': 'OAuth ' + user.accessToken

+      };

+

+      var captionData = '<entry xmlns="http://www.w3.org/2005/Atom">' +

+        '<title>' + imageFile + '</title>' +

+        '<summary>' + caption + '</summary>' +

+        '<category scheme="http://schemas.google.com/g/2005#kind" ' +

+        'term="http://schemas.google.com/photos/2007#photo"/></entry>';

+

+      var dataPart1 = {

+        contentType: 'application/atom+xml',

+        data: captionData

+      };

+      var dataPart2 = {

+        contentType: 'image/png',

+        data: imageData

+      };

+      var multipartData = {

+        boundary: MULTIPART_FORMDATA_BOUNDARY,

+        dataList: [dataPart1, dataPart2]

+      };

+

+      ajax({

+        url: UPLOAD_BASE_URL + user.albumId + '?alt=json',

+        headers: headers,

+        multipartData: multipartData,

+        complete: function(statusCode, res) {

+          if (statusCode == 201) {

+            var link = res.entry.link;

+            callback('success', link);

+          } else {

+            var message = 'failed_to_upload_image';

+            if (statusCode == 403) {

+              // bad access token

+              message = 'bad_access_token';

+            } else if (statusCode == 404 && res == 'No album found.') {

+              // Invalid album id.

+              message = 'invalid_album_id'

+            }

+            callback('failure', message);

+          }

+        }

+      });

+    },

+

+    getPhotoLink: function(user, photolinks, callback) {

+      for (var i = 0; i < photolinks.length; i++) {

+        var link = photolinks[i];

+        if (link.type == 'text/html' &&

+            link.rel == 'http://schemas.google.com/photos/2007#canonical') {

+          callback('success', link.href);

+          break;

+        }

+      }

+    },

+

+    logout: function(callback) {

+      ajax({

+        url: LOGOUT_URL,

+        success: function() {

+          callback();

+        }

+      });

+    }

+  };

+})();
\ No newline at end of file
diff --git a/src/js/popup.js b/src/js/popup.js
new file mode 100644
index 0000000..3f5690f
--- /dev/null
+++ b/src/js/popup.js
@@ -0,0 +1,173 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.  Use of this

+// source code is governed by a BSD-style license that can be found in the

+// LICENSE file.
+
+function $(id) {
+  return document.getElementById(id);
+}
+
+chrome.extension.onMessage.addListener(function(request, sender, response) {
+  if (request.msg == 'page_capturable') {
+    $('tip').style.display = 'none';
+    $('captureSpecialPageItem').style.display = 'none';
+    $('captureWindowItem').style.display = 'block';
+    $('captureAreaItem').style.display = 'block';
+    $('captureWebpageItem').style.display = 'block';
+  } else if (request.msg == 'page_uncapturable') {
+    i18nReplace('tip', 'special');
+    $('tip').style.display = 'block';
+    $('captureSpecialPageItem').style.display = 'none';
+    $('captureWindowItem').style.display = 'none';
+    $('captureAreaItem').style.display = 'none';
+    $('captureWebpageItem').style.display = 'none';
+  }
+});
+
+function toDo(what) {
+  var bg = chrome.extension.getBackgroundPage();
+  switch (what) {
+    case 'capture_window':
+      bg.screenshot.captureWindow();
+      window.close();
+      break;
+    case 'capture_area':
+      bg.screenshot.showSelectionArea();
+      window.close();
+      break;
+    case 'capture_webpage':
+      bg.screenshot.captureWebpage();
+      $('loadDiv').style.display = 'block';
+      $('item').style.display = 'none';
+      break;
+    case 'capture_special_page':
+      bg.screenshot.captureSpecialPage();
+      window.close();
+      break;
+  }
+}
+
+function i18nReplace(id, name) {
+  return $(id).innerHTML = chrome.i18n.getMessage(name);
+}
+
+function resizeDivWidth(id, width) {
+  $(id).style.width = width + "px";
+}
+
+function init() {
+  i18nReplace('captureSpecialPageText', 'capture_window');
+  i18nReplace('capturing', 'capturing');
+  i18nReplace('captureWindowText', 'capture_window');
+  i18nReplace('captureAreaText', 'capture_area');
+  i18nReplace('captureWebpageText', 'capture_webpage');
+  i18nReplace('optionItem', 'option');
+
+  $('option').addEventListener('click', function () {
+    chrome.tabs.create({ url: 'options.html'});
+  }, false);
+  if (HotKey.isEnabled()) {
+    $('captureWindowShortcut').style.display = 'inline';
+    $('captureAreaShortcut').style.display = 'inline';
+    $('captureWebpageShortcut').style.display = 'inline';
+    document.body.style.minWidth = "190px"
+  } else {
+    $('captureWindowShortcut').style.display = 'none';
+    $('captureAreaShortcut').style.display = 'none';
+    $('captureWebpageShortcut').style.display = 'none';
+    document.body.style.minWidth = "140px";
+  }
+  var isScriptLoad = false;
+  chrome.tabs.getSelected(null, function(tab) {
+    if (tab.url.indexOf('chrome') == 0 || tab.url.indexOf('about') == 0) {
+      i18nReplace('tip', 'special');
+      return;
+    } else {
+      $('tip').style.display = 'none';
+      $('captureSpecialPageItem').style.display = 'block';
+      showOption();
+    }
+    chrome.tabs.sendMessage(tab.id, {msg: 'is_page_capturable'},
+      function(response) {
+        isScriptLoad = true;
+        if (response.msg == 'capturable') {
+          $('tip').style.display = 'none';
+          $('captureSpecialPageItem').style.display = 'none';
+          $('captureWindowItem').style.display = 'block';
+          $('captureAreaItem').style.display = 'block';
+          $('captureWebpageItem').style.display = 'block';
+          var textWidth = $('captureWindowText')['scrollWidth'];
+          resizeDivWidth('captureWindowText', textWidth);
+          resizeDivWidth('captureAreaText', textWidth);
+          resizeDivWidth('captureWebpageText', textWidth);
+          var bg = chrome.extension.getBackgroundPage();
+          if (bg.screenshot.isThisPlatform('mac')) {
+            $('captureAreaShortcut').innerText = '\u2325\u2318R';
+            $('captureWindowShortcut').innerText = '\u2325\u2318V';
+            $('captureWebpageShortcut').innerText = '\u2325\u2318H';
+          }
+        } else if (response.msg == 'uncapturable') {
+          i18nReplace('tip', 'special');
+          $('tip').style.display = 'block';
+        } else {
+          i18nReplace('tip', 'loading');
+        }
+      });
+  });
+  chrome.tabs.executeScript(null, {file: 'js/isLoad.js'});
+  var insertScript = function() {
+    if (isScriptLoad == false) {
+      chrome.tabs.getSelected(null, function(tab) {
+        if (tab.url.indexOf('chrome') == 0 ||
+            tab.url.indexOf('about') == 0) {
+          i18nReplace('tip', 'special');
+        } else {
+          $('tip').style.display = 'none';
+          $('captureSpecialPageItem').style.display = 'block';
+          showOption();
+        }
+      });
+    }
+    var captureItems = document.querySelectorAll('li.menuI');
+    var showSeparator = false;
+    for (var i = 0; i < captureItems.length; i++) {
+      if (window.getComputedStyle(captureItems[i]).display != 'none') {
+        showSeparator = true;
+        break;
+      }
+    }
+    $('separatorItem').style.display = showSeparator ? 'block' : 'none';
+  }
+  setTimeout(insertScript, 500);
+
+  // Update hot key.
+  if (HotKey.get('area') != '@')
+    $('captureAreaShortcut').innerText = 'Ctrl+Alt+' + HotKey.get('area');
+  if (HotKey.get('viewport') != '@') {
+    $('captureWindowShortcut').innerText = 'Ctrl+Alt+' +
+        HotKey.get('viewport');
+  }
+  if (HotKey.get('fullpage') != '@') {
+    $('captureWebpageShortcut').innerText ='Ctrl+Alt+' +
+        HotKey.get('fullpage');
+  }
+
+  $('captureSpecialPageItem').addEventListener('click', function(e) {
+    toDo('capture_special_page');
+  });
+  $('captureAreaItem').addEventListener('click', function(e) {
+    toDo('capture_area');
+  });
+  $('captureWindowItem').addEventListener('click', function(e) {
+    toDo('capture_window');
+  });
+  $('captureWebpageItem').addEventListener('click', function(e) {
+    toDo('capture_webpage');
+  });
+}
+
+function showOption() {
+  $('option').style.display = 'block';
+  $('separatorItem').style.display = 'block';
+}
+
+document.addEventListener('DOMContentLoaded', init);
diff --git a/src/js/sha1.js b/src/js/sha1.js
new file mode 100644
index 0000000..1b55982
--- /dev/null
+++ b/src/js/sha1.js
@@ -0,0 +1,202 @@
+/*

+ * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined

+ * in FIPS PUB 180-1

+ * Version 2.1a Copyright Paul Johnston 2000 - 2002.

+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet

+ * Distributed under the BSD License

+ * See http://pajhome.org.uk/crypt/md5 for details.

+ */

+

+/*

+ * Configurable variables. You may need to tweak these to be compatible with

+ * the server-side, but the defaults work in most cases.

+ */

+var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */

+var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */

+var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */

+

+/*

+ * These are the functions you'll usually want to call

+ * They take string arguments and return either hex or base-64 encoded strings

+ */

+function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));}

+function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));}

+function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));}

+function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));}

+function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));}

+function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));}

+

+/*

+ * Perform a simple self-test to see if the VM is working

+ */

+function sha1_vm_test()

+{

+  return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";

+}

+

+/*

+ * Calculate the SHA-1 of an array of big-endian words, and a bit length

+ */

+function core_sha1(x, len)

+{

+  /* append padding */

+  x[len >> 5] |= 0x80 << (24 - len % 32);

+  x[((len + 64 >> 9) << 4) + 15] = len;

+

+  var w = Array(80);

+  var a =  1732584193;

+  var b = -271733879;

+  var c = -1732584194;

+  var d =  271733878;

+  var e = -1009589776;

+

+  for(var i = 0; i < x.length; i += 16)

+  {

+    var olda = a;

+    var oldb = b;

+    var oldc = c;

+    var oldd = d;

+    var olde = e;

+

+    for(var j = 0; j < 80; j++)

+    {

+      if(j < 16) w[j] = x[i + j];

+      else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);

+      var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)),

+                       safe_add(safe_add(e, w[j]), sha1_kt(j)));

+      e = d;

+      d = c;

+      c = rol(b, 30);

+      b = a;

+      a = t;

+    }

+

+    a = safe_add(a, olda);

+    b = safe_add(b, oldb);

+    c = safe_add(c, oldc);

+    d = safe_add(d, oldd);

+    e = safe_add(e, olde);

+  }

+  return Array(a, b, c, d, e);

+

+}

+

+/*

+ * Perform the appropriate triplet combination function for the current

+ * iteration

+ */

+function sha1_ft(t, b, c, d)

+{

+  if(t < 20) return (b & c) | ((~b) & d);

+  if(t < 40) return b ^ c ^ d;

+  if(t < 60) return (b & c) | (b & d) | (c & d);

+  return b ^ c ^ d;

+}

+

+/*

+ * Determine the appropriate additive constant for the current iteration

+ */

+function sha1_kt(t)

+{

+  return (t < 20) ?  1518500249 : (t < 40) ?  1859775393 :

+         (t < 60) ? -1894007588 : -899497514;

+}

+

+/*

+ * Calculate the HMAC-SHA1 of a key and some data

+ */

+function core_hmac_sha1(key, data)

+{

+  var bkey = str2binb(key);

+  if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz);

+

+  var ipad = Array(16), opad = Array(16);

+  for(var i = 0; i < 16; i++)

+  {

+    ipad[i] = bkey[i] ^ 0x36363636;

+    opad[i] = bkey[i] ^ 0x5C5C5C5C;

+  }

+

+  var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz);

+  return core_sha1(opad.concat(hash), 512 + 160);

+}

+

+/*

+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally

+ * to work around bugs in some JS interpreters.

+ */

+function safe_add(x, y)

+{

+  var lsw = (x & 0xFFFF) + (y & 0xFFFF);

+  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);

+  return (msw << 16) | (lsw & 0xFFFF);

+}

+

+/*

+ * Bitwise rotate a 32-bit number to the left.

+ */

+function rol(num, cnt)

+{

+  return (num << cnt) | (num >>> (32 - cnt));

+}

+

+/*

+ * Convert an 8-bit or 16-bit string to an array of big-endian words

+ * In 8-bit function, characters >255 have their hi-byte silently ignored.

+ */

+function str2binb(str)

+{

+  var bin = Array();

+  var mask = (1 << chrsz) - 1;

+  for(var i = 0; i < str.length * chrsz; i += chrsz)

+    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32);

+  return bin;

+}

+

+/*

+ * Convert an array of big-endian words to a string

+ */

+function binb2str(bin)

+{

+  var str = "";

+  var mask = (1 << chrsz) - 1;

+  for(var i = 0; i < bin.length * 32; i += chrsz)

+    str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask);

+  return str;

+}

+

+/*

+ * Convert an array of big-endian words to a hex string.

+ */

+function binb2hex(binarray)

+{

+  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";

+  var str = "";

+  for(var i = 0; i < binarray.length * 4; i++)

+  {

+    str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +

+           hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8  )) & 0xF);

+  }

+  return str;

+}

+

+/*

+ * Convert an array of big-endian words to a base-64 string

+ */

+function binb2b64(binarray)

+{

+  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

+  var str = "";

+  for(var i = 0; i < binarray.length * 4; i += 3)

+  {

+    var triplet = (((binarray[i   >> 2] >> 8 * (3 -  i   %4)) & 0xFF) << 16)

+                | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 )

+                |  ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF);

+    for(var j = 0; j < 4; j++)

+    {

+      if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;

+      else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);

+    }

+  }

+  return str;

+}

diff --git a/src/js/shortcut.js b/src/js/shortcut.js
new file mode 100644
index 0000000..7959178
--- /dev/null
+++ b/src/js/shortcut.js
@@ -0,0 +1,35 @@
+var shortcutKey = {

+

+  init: function() {

+    if (document.body.hasAttribute('screen_capture_injected')) {

+      return;

+    }

+    document.body.setAttribute('screen_capture_injected', true);

+    document.body.addEventListener('keydown', shortcutKey.handleShortcut,

+      false);

+  },

+

+  isThisPlatform: function(operationSystem) {

+    return navigator.userAgent.toLowerCase().indexOf(operationSystem) > -1;

+  },

+

+  handleShortcut: function (event) {

+    var isMac = shortcutKey.isThisPlatform('mac');

+    var keyCode = event.keyCode;

+    // Send compose key like Ctrl + Alt + alphabetical-key to background.

+    if ((event.ctrlKey && event.altKey && !isMac ||

+          event.metaKey && event.altKey && isMac) &&

+        keyCode > 64 && keyCode < 91) {

+      shortcutKey.sendMessage({

+        msg: 'capture_hot_key',

+        keyCode: keyCode

+      });

+    }

+  },

+

+  sendMessage: function(message) {

+    chrome.extension.sendMessage(message);

+  }

+};

+

+shortcutKey.init();

diff --git a/src/js/showimage.js b/src/js/showimage.js
new file mode 100644
index 0000000..294353d
--- /dev/null
+++ b/src/js/showimage.js
@@ -0,0 +1,824 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.  Use of this
+// source code is governed by a BSD-style license that can be found in the
+// LICENSE file.
+
+function isHighVersion() {
+  var version = navigator.userAgent.match(/Chrome\/(\d+)/)[1];
+  return version > 9;
+}
+
+function $(id) {
+  return document.getElementById(id);
+}
+function i18nReplace(id, messageKey) {
+  return $(id).innerHTML = chrome.i18n.getMessage(messageKey);
+}
+UploadUI.init();
+
+var bg = chrome.extension.getBackgroundPage();
+var canvas = new Canvas();
+var photoshop = {
+  canvas: document.createElement("canvas"),
+  tabTitle: '',
+  startX: 0,
+  startY: 0,
+  endX: 0,
+  endY: 0,
+  dragFlag: false,
+  flag: 'rectangle',
+  layerId: 'layer0',
+  canvasId: '',
+  color: '#ff0000',
+  highlightColor: '',
+  lastValidAction: 0,
+  markedArea: [],
+  isDraw: true,
+  offsetX: 0,
+  offsetY: 36,
+  nowHeight: 0,
+  nowWidth: 0,
+  highlightType: 'border',
+  highlightMode: 'rectangle',
+  text: '',
+
+  i18nReplace: i18nReplace,
+
+  initCanvas: function() {
+    $('canvas').width = $('mask-canvas').width = $('photo').style.width =
+        photoshop.canvas.width = bg.screenshot.canvas.width;
+    $('canvas').height = $('mask-canvas').height = $('photo').style.height =
+        photoshop.canvas.height = bg.screenshot.canvas.height;
+    var context = photoshop.canvas.getContext('2d');
+    context.drawImage(bg.screenshot.canvas, 0, 0);
+    context = $('canvas').getContext('2d');
+    context.drawImage(photoshop.canvas, 0, 0);
+    $('canvas').style.display = 'block';
+  },
+
+  init: function() {
+    photoshop.initTools();
+    photoshop.initCanvas();
+    photoshop.tabTitle = bg.screenshot.tab.title;
+    var showBoxHeight = function() {
+      $('showBox').style.height = window.innerHeight - photoshop.offsetY - 1;
+    }
+    setTimeout(showBoxHeight, 50);
+  },
+
+  markCurrentElement: function(element) {
+    if (element && element.parentNode) {
+      var children = element.parentNode.children;
+      for (var i = 0; i < children.length; i++) {
+        var node = children[i];
+        if (node == element) {
+          element.className = 'mark';
+        } else {
+          node.className = '';
+        }
+      }
+    }
+  },
+
+  setHighLightMode: function() {
+    photoshop.highlightType = localStorage.highlightType || 'border';
+    photoshop.color = localStorage.highlightColor || '#FF0000';
+    $(photoshop.layerId).style.border = '2px solid ' + photoshop.color;
+    if (photoshop.highlightType == 'rect') {
+      $(photoshop.layerId).style.backgroundColor = photoshop.color;
+      $(photoshop.layerId).style.opacity = 0.5;
+    }
+    if (photoshop.flag == 'rectangle') {
+      $(photoshop.layerId).style.borderRadius = '0 0';
+    } else if (photoshop.flag == 'radiusRectangle') {
+      $(photoshop.layerId).style.borderRadius = '6px 6px';
+    } else if (photoshop.flag == 'ellipse') {
+      $(photoshop.layerId).style.border = '0';
+      $(photoshop.layerId).style.backgroundColor = '';
+      $(photoshop.layerId).style.opacity = 1;
+    }
+
+  },
+
+  setBlackoutMode: function() {
+    photoshop.color = '#000000';
+    $(photoshop.layerId).style.opacity = 1;
+    $(photoshop.layerId).style.backgroundColor = '#000000';
+    $(photoshop.layerId).style.border = '2px solid #000000';
+  },
+
+  setTextMode: function() {
+    localStorage.fontSize = localStorage.fontSize || '16';
+    photoshop.color = localStorage.fontColor =
+        localStorage.fontColor || '#FF0000';
+    $(photoshop.layerId).setAttribute('contentEditable', true);
+    $(photoshop.layerId).style.border = '1px dotted #333333';
+    $(photoshop.layerId).style.cursor = 'text';
+    $(photoshop.layerId).style.lineHeight = localStorage.fontSize + 'px';
+    $(photoshop.layerId).style.fontSize = localStorage.fontSize + 'px';
+    $(photoshop.layerId).style.color = photoshop.color;
+    $(photoshop.layerId).innerHTML = '<br/>';
+    var layer = $(photoshop.layerId);
+    var id = photoshop.layerId;
+    layer.addEventListener('blur', function() {
+      photoshop.setTextToArray(id);
+    }, true);
+    layer.addEventListener('click', function() {
+      this.style.border = '1px dotted #333333';
+    }, true);
+    layer.addEventListener('mouseout', function() {
+      if (!photoshop.dragFlag) {
+        this.style.borderWidth = 0;
+      }
+    }, false);
+    layer.addEventListener('mousemove', function() {
+      this.style.border = '1px dotted #333333';
+    }, false);
+  },
+
+  setTextToArray: function(id) {
+    var str = $(id).innerText.split("\n");
+    if (photoshop.markedArea.length > 0) {
+      for (var i = photoshop.markedArea.length - 1; i >= 0; i--) {
+        if (photoshop.markedArea[i].id == id) {
+          photoshop.markedArea[i].context = str;
+          break;
+        }
+      }
+      $(id).style.borderWidth = 0;
+    }
+  },
+
+  openOptionPage: function() {
+    chrome.tabs.create({url: chrome.extension.getURL("options.html")});
+  },
+
+  closeCurrentTab: function() {
+    chrome.tabs.getSelected(null, function(tab) {
+      chrome.tabs.remove(tab.id);
+    });
+  },
+
+  finish: function() {
+    var context = $('canvas').getContext('2d');
+    context.drawImage(photoshop.canvas, 0, 0);
+  },
+
+  colorRgba: function(color, opacity) {
+    var sColor = color.toLowerCase();
+    var sColorChange = [];
+    for (var i = 1; i < sColor.length; i += 2) {
+      sColorChange.push(parseInt("0x" + sColor.slice(i,i + 2)));
+    }
+    return "rgba(" + sColorChange.join(",") + "," + opacity + ")";
+  },
+
+  /**
+  * Undo the current operation
+  */
+  toDo: function(element, what) {
+    photoshop.flag = what;
+    photoshop.isDraw = true;
+    photoshop.markCurrentElement(element);
+  },
+
+  setDivStyle: function(x, y) {
+    $(photoshop.layerId).setAttribute("style", "");
+    $(photoshop.layerId).setAttribute("contentEditable", false);
+    switch(photoshop.flag) {
+      case 'rectangle':
+      case 'radiusRectangle':
+      case 'ellipse':
+        photoshop.setHighLightMode();
+        break;
+      case 'redact':
+        photoshop.setBlackoutMode();
+        break;
+      case 'text':
+        photoshop.setTextMode();
+        break;
+      case 'line':
+      case 'arrow':
+        photoshop.drawLineOnMaskCanvas(x, y, x, y, 'lineStart',
+            photoshop.layerId);
+        break;
+      case 'blur':
+        photoshop.createCanvas(photoshop.layerId);
+        break;
+    }
+  },
+
+  /**
+  * Create a layer and set style
+  */
+  createDiv: function() {
+    photoshop.lastValidAction++;
+    photoshop.layerId = 'layer' + photoshop.lastValidAction;
+    if ($(photoshop.layerId)) {
+      photoshop.removeElement(photoshop.layerId);
+    }
+    var divElement = document.createElement('div');
+    divElement.id = photoshop.layerId;
+    divElement.className = 'layer';
+    $('photo').appendChild(divElement);
+    if (photoshop.flag  == 'blur') {
+      photoshop.createCanvas(photoshop.layerId);
+    }
+    return divElement;
+  },
+
+  createCanvas: function(parentId) {
+    photoshop.canvasId = 'cav-' + parentId;
+    if (!$(photoshop.canvasId)) {
+      var cav = document.createElement('canvas');
+      cav.id = photoshop.canvasId;
+      cav.width = 10;
+      cav.height = 10;
+      $(photoshop.layerId).appendChild(cav);
+      return cav;
+    }
+    return $(photoshop.canvasId);
+
+  },
+
+  createCloseButton: function(parent, id, left, top, flag) {
+    var imgElement = document.createElement('img');
+    imgElement.id = id;
+    imgElement.src = 'images/cross.png';
+    imgElement.className = 'closeButton';
+    imgElement.style.left = left - 15 + 'px';
+    if (photoshop.flag == 'line' || photoshop.flag == 'arrow') {
+      imgElement.style.left = left / 2 - 5 + 'px';
+      imgElement.style.top = top / 2 - 5 + 'px';
+    }
+    imgElement.onclick = function() {
+      $(parent).style.display = 'none';
+      photoshop.removeLayer(parent);
+    };
+    $(parent).onmousemove = function() {
+      if (!photoshop.dragFlag) {
+        photoshop.showCloseButton(id);
+        $(parent).style.zIndex = 110;
+        photoshop.isDraw = (flag == 'text' ? false : photoshop.isDraw);
+      }
+    };
+    $(parent).onmouseout = function() {
+      photoshop.hideCloseButton(id);
+      $(parent).style.zIndex = 100;
+      photoshop.isDraw = true;
+    };
+    $(parent).appendChild(imgElement);
+    return imgElement;
+  },
+
+  showCloseButton: function(id) {
+    $(id).style.display = 'block';
+  },
+
+  hideCloseButton: function(id) {
+    $(id).style.display = 'none';
+    photoshop.isDraw = true;
+  },
+
+  removeLayer: function(id) {
+    for (var i = 0; i < photoshop.markedArea.length; i++) {
+      if (photoshop.markedArea[i].id == id) {
+        photoshop.markedArea.splice(i, 1);
+        break;
+      }
+    }
+    photoshop.removeElement(id);
+  },
+
+  /**
+  *  Set the starting point(x,y) when mouse pressed
+  */
+  onMouseDown: function(event) {
+    if (photoshop.isDraw && event.button != 2) {
+      photoshop.startX = event.pageX + $('showBox').scrollLeft -
+          photoshop.offsetX;
+      photoshop.startY = event.pageY + $('showBox').scrollTop -
+          photoshop.offsetY;
+      photoshop.setDivStyle(photoshop.startX, photoshop.startY);
+      photoshop.dragFlag = true;
+
+      $(photoshop.layerId).style.left = photoshop.startX + 'px';
+      $(photoshop.layerId).style.top = photoshop.startY + 'px';
+      $(photoshop.layerId).style.height = 0;
+      $(photoshop.layerId).style.width = 0;
+      $(photoshop.layerId).style.display = 'block';
+    }
+  },
+
+  onMouseUp: function(event) {
+    $('mask-canvas').style.zIndex = 10;
+    photoshop.endX = event.pageX + $('showBox').scrollLeft -
+           photoshop.offsetX;
+      if (photoshop.endX > photoshop.canvas.width) {
+        photoshop.endX = photoshop.canvas.width ;
+      }
+
+      if (photoshop.endX < 0) {
+        photoshop.endX = 0;
+      }
+
+      photoshop.endY = event.pageY + $('showBox').scrollTop -
+           photoshop.offsetY;
+      if (photoshop.endY > photoshop.canvas.height) {
+        photoshop.endY = photoshop.canvas.height ;
+      }
+      if (photoshop.endY < 0) {
+        photoshop.endY = 0;
+      }
+    if (photoshop.isDraw && photoshop.dragFlag && (photoshop.endX !=
+        photoshop.startX || photoshop.endY != photoshop.startY)) {
+      if (photoshop.flag == 'line' || photoshop.flag == 'arrow') {
+        photoshop.drawLineOnMaskCanvas(photoshop.startX, photoshop.startY,
+            photoshop.endX, photoshop.endY, 'drawEnd', photoshop.layerId);
+      } else if (photoshop.flag == 'blur') {
+        canvas.blurImage(photoshop.canvas, $(photoshop.canvasId),
+            photoshop.layerId, photoshop.startX, photoshop.startY,
+            photoshop.endX, photoshop.endY);
+      } else if (photoshop.flag == 'ellipse') {
+        photoshop.drawEllipseOnMaskCanvas(photoshop.endX,
+            photoshop.endY, 'end', photoshop.layerId);
+      }
+      photoshop.markedArea.push({
+        'id': photoshop.layerId,
+        'startX': photoshop.startX,
+        'startY': photoshop.startY,
+        'endX': photoshop.endX,
+        'endY': photoshop.endY,
+        'width': photoshop.nowWidth,
+        'height': photoshop.nowHeight,
+        'flag': photoshop.flag,
+        'highlightType': photoshop.highlightType,
+        'fontSize': localStorage.fontSize,
+        'color': photoshop.color,
+        'context': ''
+      });
+      $(photoshop.layerId).focus();
+      var imageBtnId = 'close_' + photoshop.layerId;
+      photoshop.createCloseButton(photoshop.layerId, imageBtnId,
+          photoshop.nowWidth, photoshop.nowHeight, photoshop.flag);
+      photoshop.createDiv();
+    } else if (photoshop.endX ==
+        photoshop.startX && photoshop.endY == photoshop.startY) {
+      photoshop.removeElement(photoshop.layerId);
+      photoshop.createDiv();
+    }
+    photoshop.dragFlag = false;
+
+  },
+
+  /**
+  * Refresh div‘s height and width when the mouse move
+  */
+  onMouseMove: function(event) {
+    if(photoshop.dragFlag) {
+      $('mask-canvas').style.zIndex = 200;
+      photoshop.endX = event.pageX + $('showBox').scrollLeft -
+           photoshop.offsetX;
+      if (photoshop.endX > photoshop.canvas.width) {
+        photoshop.endX = photoshop.canvas.width ;
+      }
+
+      if (photoshop.endX < 0) {
+        photoshop.endX = 0;
+      }
+
+      photoshop.endY = event.pageY + $('showBox').scrollTop -
+           photoshop.offsetY;
+      if (photoshop.endY > photoshop.canvas.height) {
+        photoshop.endY = photoshop.canvas.height ;
+      }
+      if (photoshop.endY < 0) {
+        photoshop.endY = 0;
+      }
+      photoshop.nowHeight = photoshop.endY - photoshop.startY - 1 ;
+      photoshop.nowWidth = photoshop.endX - photoshop.startX - 1 ;
+
+      if(photoshop.nowHeight < 0) {
+        $(photoshop.layerId).style.top = photoshop.endY + 'px';
+        photoshop.nowHeight = -1 * photoshop.nowHeight;
+      }
+      if(photoshop.nowWidth < 0) {
+        $(photoshop.layerId).style.left = photoshop.endX + 'px';
+        photoshop.nowWidth = -1 * photoshop.nowWidth;
+      }
+
+      $(photoshop.layerId).style.height = photoshop.nowHeight - 3;
+      $(photoshop.layerId).style.width = photoshop.nowWidth - 3;
+      if (photoshop.flag == 'line' || photoshop.flag == 'arrow') {
+        photoshop.drawLineOnMaskCanvas(photoshop.startX, photoshop.startY,
+            photoshop.endX, photoshop.endY, 'lineDrawing', photoshop.layerId);
+      } else if (photoshop.flag == 'blur') {
+        $(photoshop.layerId).style.height = photoshop.nowHeight ;
+        $(photoshop.layerId).style.width = photoshop.nowWidth ;
+        canvas.blurImage(photoshop.canvas, $(photoshop.canvasId),
+            photoshop.layerId, photoshop.startX, photoshop.startY,
+            photoshop.endX, photoshop.endY);
+      } else if (photoshop.flag == 'ellipse') {
+        photoshop.drawEllipseOnMaskCanvas(photoshop.endX,
+            photoshop.endY, 'drawing', photoshop.layerId);
+      }
+    }
+
+  },
+
+  /**
+  * Remove a div
+  */
+  removeElement: function(id) {
+    if($(id)) {
+      $(id).parentNode.removeChild($(id));
+    }
+  },
+
+  /**
+  * Use fillStyle, fillText and fillRect functions to draw rectangles,
+  * and render to canvas
+  */
+  draw: function() {
+    var context = $('canvas').getContext('2d');
+    for (var j = 0; j < photoshop.markedArea.length; j++) {
+      var mark = photoshop.markedArea[j];
+      var x = (mark.startX < mark.endX) ? mark.startX : mark.endX;
+      var y = (mark.startY < mark.endY) ? mark.startY : mark.endY;
+      var width = mark.width;
+      var height = mark.height;
+      var color = mark.color;
+      switch(mark.flag) {
+        case 'rectangle':
+          if (mark.highlightType == 'border') {
+            canvas.drawStrokeRect(context, color, x, y, width, height, 2);
+          } else {
+            var color = changeColorToRgba(color, 0.5);
+            canvas.drawFillRect(context, color, x, y, width, height);
+          }
+          break;
+        case 'radiusRectangle':
+          canvas.drawRoundedRect(
+              context, color, x, y, width, height, 6, mark.highlightType);
+          break;
+        case 'ellipse':
+          x = (mark.startX + mark.endX) / 2;
+          y = (mark.startY + mark.endY) / 2;
+          var xAxis = Math.abs(mark.endX - mark.startX) / 2;
+          var yAxis = Math.abs(mark.endY - mark.startY) / 2;
+          canvas.drawEllipse(
+              context, color, x, y, xAxis, yAxis, 3, mark.highlightType);
+          break;
+        case 'redact':
+          canvas.drawFillRect(context, color, x, y, width, height);
+          break;
+        case 'text':
+          for (var i = 0; i < mark.context.length; i++) {
+            canvas.setText(
+                context, mark.context[i], color, mark.fontSize, 'arial',
+                mark.fontSize, x, y + mark.fontSize * i, width);
+          }
+          break;
+        case 'blur':
+          var imageData = context.getImageData(
+              x, y, photoshop.markedArea[j].width,
+              photoshop.markedArea[j].height);
+          imageData = canvas.boxBlur(
+              imageData, photoshop.markedArea[j].width,
+              photoshop.markedArea[j].height, 10);
+          context.putImageData(
+              imageData, x, y, 0, 0, photoshop.markedArea[j].width,
+              photoshop.markedArea[j].height);
+          break;
+        case 'line':
+          canvas.drawLine(
+              context, color, 'round', 2,
+              mark.startX, mark.startY, mark.endX, mark.endY);
+          break;
+        case 'arrow':
+          canvas.drawArrow(
+              context, color, 2, 4, 10, 'round',
+              mark.startX, mark.startY, mark.endX, mark.endY);
+          break;
+      }
+    }
+  },
+
+  drawLineOnMaskCanvas: function(startX, startY, endX, endY, type, layerId) {
+    var ctx = $('mask-canvas').getContext('2d');
+    ctx.clearRect(0, 0, $('mask-canvas').width, $('mask-canvas').height);
+    if (type == 'drawEnd') {
+      var offset = 20;
+      var width = Math.abs(endX - photoshop.startX) > 0 ?
+          Math.abs(endX - photoshop.startX): 0;
+      var height = Math.abs(endY - photoshop.startY) > 0 ?
+          Math.abs(endY - photoshop.startY): 0;
+      var offsetLeft = parseInt($(layerId).style.left);
+      var offsetTop = parseInt($(layerId).style.top);
+      startX = startX - offsetLeft + offset / 2;
+      startY = startY - offsetTop + offset / 2;
+      endX = endX - offsetLeft + offset / 2;
+      endY = endY - offsetTop + offset / 2;
+      $(layerId).style.left = offsetLeft - offset / 2;
+      $(layerId).style.top = offsetTop - offset / 2;
+      var cavCopy = photoshop.createCanvas(layerId);
+      cavCopy.width = width + offset;
+      cavCopy.height = height + offset;
+      ctx = cavCopy.getContext('2d');
+    }
+    if (localStorage.lineType == 'line') {
+      canvas.drawLine(ctx, localStorage.lineColor, 'round', 2,
+        startX, startY, endX, endY);
+    } else {
+      canvas.drawArrow(ctx, localStorage.lineColor, 2, 4, 10, 'round',
+          startX, startY, endX, endY)
+    }
+
+  },
+
+  createColorPadStr: function(element, type) {
+    var colorList = ['#000000', '#0036ff', '#008000', '#dacb23', '#d56400',
+      '#c70000', '#be00b3', '#1e2188', '#0090ff', '#22cc01', '#ffff00',
+      '#ff9600', '#ff0000', '#ff008e', '#7072c3', '#49d2ff', '#9dff3d',
+      '#ffffff', '#ffbb59', '#ff6b6b', '#ff6bbd'];
+
+    var div = document.createElement("div");
+    div.id = "colorpad";
+    element.appendChild(div);
+
+    for(var i = 0; i < colorList.length; i++) {
+      var a = document.createElement("a");
+      var color = colorList[i];
+      a.id = color;
+      a.title = color;
+      a.style.backgroundColor = color;
+      if (color == '#ffffff') {
+        a.style.border = "1px solid #444";
+        a.style.width = "12px"
+        a.style.height = "12px";
+      }
+      a.addEventListener('click', function(e) {
+        photoshop.colorPadPick(e.target.id, type);
+        return false;
+      });
+      div.appendChild(a);
+    }
+  },
+
+  colorPadPick: function(color, type) {
+    photoshop.color = color;
+    if(type == 'highlight') {
+      localStorage.highlightColor = color;
+      photoshop.setHighlightColorBoxStyle(color);
+    } else if(type == 'text') {
+      localStorage.fontColor = color;
+      $('fontColorBox').style.backgroundColor = color;
+    } else if (type == 'line') {
+      localStorage.lineColor = color;
+      photoshop.setLineColorBoxStyle();
+    } else if (type == 'ellipse') {
+      $('ellipseBox').style.borderColor = color;
+    }
+  },
+
+  setHighlightColorBoxStyle: function(color) {
+    var highlightColorBox = $('highlightColorBox');
+    highlightColorBox.style.borderColor = color;
+    localStorage.highlightType = localStorage.highlightType || 'border';
+    if (localStorage.highlightType == 'border') {
+      highlightColorBox.style.background = '#ffffff';
+      highlightColorBox.style.opacity = 1;
+      $('borderMode').className = 'mark';
+      $('rectMode').className = '';
+    } else if (localStorage.highlightType == 'rect') {
+      highlightColorBox.style.background = color;
+      highlightColorBox.style.opacity = 0.5;
+      $('borderMode').className = '';
+      $('rectMode').className = 'mark';
+    }
+    if (photoshop.flag == 'rectangle') {
+      highlightColorBox.style.borderRadius = '0 0';
+    } else if (photoshop.flag == 'radiusRectangle') {
+      highlightColorBox.style.borderRadius = '3px 3px';
+    } else if (photoshop.flag == 'ellipse') {
+      highlightColorBox.style.borderRadius = '12px 12px';
+    }
+    photoshop.markCurrentElement($(photoshop.flag));
+  },
+
+  setBlackoutColorBoxStyle: function() {
+    localStorage.blackoutType = localStorage.blackoutType || 'redact';
+    if (localStorage.blackoutType == 'redact') {
+      $('blackoutBox').className = 'rectBox';
+      $('redact').className = 'mark';
+      $('blur').className = '';
+    } else if (localStorage.blackoutType == 'blur') {
+      $('blackoutBox').className = 'blurBox';
+      $('redact').className = '';
+      $('blur').className = 'mark';
+    }
+  },
+
+  setFontSize: function(size) {
+    var id = 'size_' + size;
+    localStorage.fontSize = size;
+    $('size_10').className = '';
+    $('size_16').className = '';
+    $('size_18').className = '';
+    $('size_32').className = '';
+    $(id).className = 'mark';
+  },
+
+  setLineColorBoxStyle: function() {
+    localStorage.lineType = localStorage.lineType || 'line';
+    photoshop.color = localStorage.lineColor =
+        localStorage.lineColor || '#FF0000';
+    var ctx = $('lineIconCav').getContext('2d');
+    ctx.clearRect(0, 0, 14, 14);
+    if (localStorage.lineType == 'line') {
+      $('straightLine').className = 'mark';
+      $('arrow').className = '';
+      canvas.drawLine(ctx, photoshop.color, 'round', 2, 1, 13, 13, 1);
+    } else if (localStorage.lineType == 'arrow') {
+      $('straightLine').className = '';
+      $('arrow').className = 'mark';
+      canvas.drawArrow(ctx, photoshop.color, 2, 4, 7, 'round',1, 13, 13, 1);
+    }
+
+  },
+
+  initTools: function() {
+    photoshop.i18nReplace('tHighlight', 'highlight');
+    photoshop.i18nReplace('tRedact', 'redact');
+    photoshop.i18nReplace('redactText', 'solid_black');
+    photoshop.i18nReplace('tText', 'text');
+    photoshop.i18nReplace('tSave', 'save');
+    photoshop.i18nReplace('tClose', 'close');
+    photoshop.i18nReplace('border', 'border');
+    photoshop.i18nReplace('rect', 'rect');
+    photoshop.i18nReplace('blurText', 'blur');
+    photoshop.i18nReplace('lineText', 'line');
+    photoshop.i18nReplace('size_10', 'size_small');
+    photoshop.i18nReplace('size_16', 'size_normal');
+    photoshop.i18nReplace('size_18', 'size_large');
+    photoshop.i18nReplace('size_32', 'size_huge');
+    var fontSize = localStorage.fontSize = localStorage.fontSize || 16;
+    if (fontSize != 10 && fontSize != 16 && fontSize != 18 && fontSize != 32) {
+      localStorage.fontSize = 16;
+    }
+    localStorage.highlightMode = photoshop.flag =
+        localStorage.highlightMode || 'rectangle';
+    localStorage.highlightColor = localStorage.highlightColor || '#FF0000';
+    localStorage.fontColor = localStorage.fontColor || '#FF0000';
+    localStorage.highlightType = photoshop.highlightType =
+        localStorage.highlightType || 'border';
+    localStorage.blackoutType = localStorage.blackoutType || 'redact';
+    localStorage.lineType = localStorage.lineType || 'line';
+    localStorage.lineColor = localStorage.lineColor || '#FF0000';
+    photoshop.setHighlightColorBoxStyle(localStorage.highlightColor);
+    $('fontColorBox').style.backgroundColor =
+        localStorage.fontColor || '#FF0000';
+    $('btnHighlight').addEventListener('click', function() {
+      photoshop.toDo(this, localStorage.highlightMode);
+      photoshop.setHighlightColorBoxStyle(localStorage.highlightColor);
+    }, false);
+
+    $('btnBlackout').addEventListener('click', function() {
+      photoshop.toDo(this, localStorage.blackoutType);
+      photoshop.setBlackoutColorBoxStyle();
+    }, false);
+
+    $('btnText').addEventListener('click', function() {
+      photoshop.toDo(this, 'text');
+    }, false);
+
+    $('btnLine').addEventListener('click', function() {
+      photoshop.toDo(this, localStorage.lineType);
+      photoshop.setLineColorBoxStyle();
+    }, false);
+
+
+
+    photoshop.setHighlightColorBoxStyle(localStorage.highlightColor);
+    $('borderMode').addEventListener('click', function() {
+      localStorage.highlightType = 'border';
+    }, false);
+    $('rectMode').addEventListener('click', function() {
+      localStorage.highlightType = 'rect';
+    }, false);
+    $('rectangle').addEventListener('click', function() {
+      localStorage.highlightMode = photoshop.flag = 'rectangle';
+      photoshop.markCurrentElement(this);
+    }, false);
+    $('radiusRectangle').addEventListener('click', function() {
+      localStorage.highlightMode = photoshop.flag = 'radiusRectangle';
+      photoshop.markCurrentElement(this);
+    }, false);
+    $('ellipse').addEventListener('click', function() {
+      localStorage.highlightMode = photoshop.flag = 'ellipse';
+      photoshop.markCurrentElement(this);
+    }, false);
+    photoshop.setBlackoutColorBoxStyle();
+    $('redact').addEventListener('click', function() {
+      localStorage.blackoutType = 'redact';
+    }, false);
+    $('blur').addEventListener('click', function() {
+      localStorage.blackoutType = 'blur';
+    }, false);
+
+    photoshop.setLineColorBoxStyle();
+
+    photoshop.createColorPadStr($('highlightColorPad'), 'highlight');
+    photoshop.createColorPadStr($('fontColorPad'), 'text');
+    photoshop.createColorPadStr($('lineColorPad'), 'line');
+
+    $('straightLine').addEventListener('click', function() {
+      localStorage.lineType = 'line';
+      photoshop.setLineColorBoxStyle();
+    }, false);
+    $('arrow').addEventListener('click', function() {
+      localStorage.lineType = 'arrow';
+      photoshop.setLineColorBoxStyle();
+    }, false);
+
+    photoshop.setFontSize(localStorage.fontSize);
+    $('size_10').addEventListener('click', function() {
+      photoshop.setFontSize(10);
+    }, false);
+    $('size_16').addEventListener('click', function() {
+      photoshop.setFontSize(16);
+    }, false);
+    $('size_18').addEventListener('click', function() {
+      photoshop.setFontSize(18);
+    }, false);
+    $('size_32').addEventListener('click', function() {
+      photoshop.setFontSize(32);
+    }, false);
+  },
+
+  drawEllipseOnMaskCanvas: function(endX, endY, type, layerId) {
+    var ctx = $('mask-canvas').getContext('2d');
+    ctx.clearRect(0, 0, $('mask-canvas').width, $('mask-canvas').height);
+    var x = (photoshop.startX + endX) / 2;
+    var y = (photoshop.startY + endY) / 2;
+    var xAxis = Math.abs(endX - photoshop.startX) / 2;
+    var yAxis = Math.abs(endY - photoshop.startY) / 2;
+    canvas.drawEllipse(ctx, photoshop.color, x, y, xAxis, yAxis, 3,
+        photoshop.highlightType);
+    if (type == 'end') {
+      var offsetLeft = parseInt($(layerId).style.left);
+      var offsetTop = parseInt($(layerId).style.top);
+      var startX = photoshop.startX - offsetLeft ;
+      var startY = photoshop.startY - offsetTop ;
+      var newEndX = photoshop.endX - offsetLeft ;
+      var newEndY = photoshop.endY - offsetTop ;
+      x = (startX + newEndX) / 2;
+      y = (startY + newEndY) / 2;
+      xAxis = Math.abs(newEndX - startX) / 2;
+      yAxis = Math.abs(newEndY - startY) / 2;
+      var cavCopy = photoshop.createCanvas(layerId);
+      cavCopy.width = Math.abs(endX - photoshop.startX);
+      cavCopy.height = Math.abs(endY - photoshop.startY);
+      var ctxCopy = cavCopy.getContext('2d');
+      canvas.drawEllipse(ctxCopy, photoshop.color, x, y,
+          xAxis, yAxis, 3, photoshop.highlightType);
+      ctx.clearRect(0, 0, $('mask-canvas').width, $('mask-canvas').height);
+    }
+  },
+
+  showTip: function(className, message, delay) {
+    delay = delay || 2000;
+    var div = document.createElement('div');
+    div.className = className;
+    div.innerHTML = message;
+    document.body.appendChild(div);
+    div.style.left = (document.body.clientWidth - div.clientWidth) / 2 + 'px';
+    window.setTimeout(function() {
+      document.body.removeChild(div);
+    }, delay);
+  }
+};
+
+photoshop.init();
+$('photo').addEventListener('mousemove', photoshop.onMouseMove, true);
+$('photo').addEventListener('mousedown', photoshop.onMouseDown, true);
+$('photo').addEventListener('mouseup', photoshop.onMouseUp, true);
+document.addEventListener('mouseup', photoshop.onMouseUp, true);
+document.addEventListener('mousemove', photoshop.onMouseMove, true);
+
+$('canvas').addEventListener(
+    'selectstart', function f(e) { return false });
+$('mask-canvas').addEventListener(
+    'selectstart', function f(e) { return false });
+$('btnClose').addEventListener('click', photoshop.closeCurrentTab);
+$('uploadAccountList').addEventListener('click', function(e) {
+  var target = e.target;
+  var classList = Array.prototype.slice.call(target.classList)
+  if (classList.indexOf('accountName') >= 0) {
+    var site = target.dataset.site;
+    var userId = target.dataset.userId;
+    UploadUI.upload(site, userId);
+  } else if (classList.indexOf('deleteBtn') >= 0) {
+    var accountId = target.dataset.accountId;
+    UploadUI.deleteAccountItem(accountId);
+  }
+});
diff --git a/src/js/sina_microblog.js b/src/js/sina_microblog.js
new file mode 100644
index 0000000..18b153b
--- /dev/null
+++ b/src/js/sina_microblog.js
@@ -0,0 +1,122 @@
+const APPKEY = '1350884563';

+const AUTHORIZE_URL = 'https://api.weibo.com/oauth2/authorize';

+const REDIRECT_URL = 'https://api.weibo.com/oauth2/default.html'

+const SINA_USER_INFO_URL = 'https://api.weibo.com/2/users/show.json';

+const SINA_PHOTO_UPLOAD_URL = 'https://upload.api.weibo.com/2/statuses/upload.json';

+const SINA_LOGOUT_URL = 'https://api.weibo.com/2/account/end_session.json';

+

+var SinaMicroblog = {

+  siteId: 'sina',

+  currentUserId: null,

+  accessTokenCallback: null,

+

+  isRedirectUrl: function() {},

+

+  getAccessToken: function(callback) {

+    SinaMicroblog.accessTokenCallback = callback;

+    var url = AUTHORIZE_URL + '?client_id=' + APPKEY +

+      '&redirect_uri=' + REDIRECT_URL + '&response_type=token';

+    chrome.tabs.create({url: url});

+  },

+  

+  parseAccessToken: function(url) {

+    var result = 'failure';

+    var msgOrUser = 'sina_failed_to_get_access_token';

+    var hash = url.split('#')[1];

+    if (hash && typeof hash == 'string') {

+      var keyValues = hash.split('&');

+      var response = {};

+

+      for (var keyValue, i = 0, l = keyValues.length; i < l; i++) {

+        keyValue = keyValues[i].split('=');

+        response[keyValue[0]] = keyValue[1];

+      }

+

+      if (!response.error && response.access_token && response.uid) {

+        result = 'success';

+        msgOrUser = new User({

+          id: response.uid,

+          accessToken: response.access_token

+        });

+      }

+    }

+

+    SinaMicroblog.accessTokenCallback(result, msgOrUser);

+    SinaMicroblog.accessTokenCallback = null;

+  },

+

+  getUserInfo: function(user, callback) {

+    ajax({

+      url: SINA_USER_INFO_URL,

+      parameters: {

+        access_token: user.accessToken,

+        uid: user.id

+      },

+      success: function(data) {

+        if (callback) {

+          user.name = data.screen_name;

+          callback('success', user);

+        }

+      },

+      status: {

+        others: function(data) {

+          callback('failure', 'failed_to_get_user_info');

+        }

+      }

+    });

+  },

+

+  upload: function(user, caption, imageData, callback) {

+    caption = encodeURIComponent(caption);

+    var params = {

+      access_token: user.accessToken,

+      status: caption

+    };

+    var binaryData = {

+      boundary: MULTIPART_FORMDATA_BOUNDARY,

+      name: 'pic',

+      value: 'test.png',

+      data: imageData,

+      type: 'image/png'

+    };

+    

+    ajax({

+      url: SINA_PHOTO_UPLOAD_URL,

+      parameters: params,

+      multipartData: binaryData,

+      success: function(microblog) {

+        callback('success', microblog.id);

+      },

+      status: {

+        others: function(res) {

+          var message = 'failed_to_upload_image';

+          var errorCode = res.error_code;

+          var invalidAccessTokenCodes = 

+            [21332, 21314, 21315, 21316, 21317, 21319, 21327];

+          if (invalidAccessTokenCodes.indexOf(errorCode) >= 0) {

+            message = 'bad_access_token';

+          }

+          callback('failure', message);

+        }

+      }

+    });

+  },

+

+  getPhotoLink: function(user, microblogId, callback) {

+    var photoLink = 'http://weibo.com/' + user.id + '/profile';

+    callback('success', photoLink);

+  },

+  

+  logout: function(callback) {

+    var params = {source: APPKEY};

+    ajax({

+      url: SINA_LOGOUT_URL,

+      parameters: params,

+      complete: function(statusCode, data) {

+        // Response status 403 means no user signed in

+        if ((statusCode == 200 || statusCode == 403) && callback)

+          callback(data);

+      }

+    });

+  }

+};
\ No newline at end of file
diff --git a/src/js/site.js b/src/js/site.js
new file mode 100644
index 0000000..6bb43f8
--- /dev/null
+++ b/src/js/site.js
@@ -0,0 +1,63 @@
+var Site = function(id) {

+  this.siteId = id;

+};

+

+Site.prototype = {

+  /**

+   * Get access token by user's authorization.

+   * @param {Function} callback call-back function, parameters:

+   *   {String} result, success or failure; {User|String} user with access

+   *   token, etc. or error message.

+   */

+  getAccessToken: function(callback) {},

+

+  /**

+   * Check if the url is redirect url for retrieving access token of current

+   * site.

+   * @param {String} url

+   * @return {Boolean} result

+   */

+  isRedirectUrl: function(url) {},

+

+  /**

+   * Parse and get access token from redirect url, then call call-back function

+   * of passed by calling getAccessToken method with access token.

+   * @param {String} url

+   */

+  parseAccessToken: function(url) {},

+

+  /**

+   * Get user information.

+   * @param {User} user

+   * @param {Function} callback call-back function, parameters:

+   *   {String} result, success or failure; {User|String} user with user id,

+   *   user name, etc. or error message.

+   */

+  getUserInfo: function(user, callback) {},

+

+  /**

+   * Upload image.

+   * @param {User} user user data with access token, etc.

+   * @param {String} caption image description

+   * @param {String} imageData binary image data

+   * @param callback  call-back function, parameters:

+   *   {String} result, success or failure; {String} photo id or error message.

+   */

+  upload: function(user, caption, imageData, callback) {},

+

+  /**

+   * Get photo link.

+   * @param {User} user user data with id, access token, etc.

+   * @param {String} photoId

+   * @param {Function} callback call-back function, parameters:

+   *   {String} result, success or failure; {String} photo link or error

+   *   message.

+   */

+  getPhotoLink: function(user, photoId, callback) {},

+

+  /**

+   * Log out current signed in user.

+   * @param callback

+   */

+  logout: function(callback) {}

+};
\ No newline at end of file
diff --git a/src/js/ui.js b/src/js/ui.js
new file mode 100644
index 0000000..e13ea13
--- /dev/null
+++ b/src/js/ui.js
@@ -0,0 +1,70 @@
+var UI = {

+

+  show: function(element) {

+    if (UI.getStyle(element, 'display') == 'none') {

+      // Set display value to be defined by style sheet

+      var cssRules = window.getMatchedCSSRules(element, '', true);

+      var ruleLength = cssRules.length;

+      var display;

+      for (var i = ruleLength - 1; i >= 0 ; --i) {

+        display = cssRules[i].style.display;

+        if (display && display != 'none') {

+          element.style.display = display;

+          return;

+        }

+      }

+

+      // Set display value to be UA default value

+      var tmpElement = document.createElement(element.nodeName);

+      document.body.appendChild(tmpElement);

+      display = UI.getStyle(tmpElement, 'display');

+      document.body.removeChild(tmpElement);

+      element.style.display = display;

+    }

+  },

+

+  hide: function(element) {

+    element.style.display = 'none';

+  },

+

+  setStyle: function(element) {

+    var argLength = arguments.length;

+    var arg1 = arguments[1];

+    if (argLength == 2 && arg1.constructor == Object) {

+      for (var prop in arg1) {

+        var camelCasedProp = prop.replace(/-([a-z])/gi, function(n, letter) {

+          return letter.toUpperCase();

+        });

+        element.style[camelCasedProp] = arg1[prop];

+      }

+    } else if (argLength == 3)

+      element.style[arg1] = arguments[2];

+  },

+

+  getStyle: function(element, property) {

+    return window.getComputedStyle(element)[property];

+  },

+

+  addClass: function(element, className) {

+    var classes = element.className.split(' ');

+    classes.push(className);

+    element.className = classes.join(' ');

+  },

+

+  removeClass: function(element, className) {

+    var classes = element.className.split(' ');

+    var index = classes.indexOf(className);

+    if (index >= 0) {

+      classes.splice(index, 1);

+      element.className = classes.join(' ');

+    }

+  },

+

+  addStyleSheet: function(path) {

+    var link = document.createElement('link');

+    link.setAttribute('type', 'text/css');

+    link.setAttribute('rel', 'stylesheet');

+    link.setAttribute('href', path);

+    document.head.appendChild(link);

+  }

+};
\ No newline at end of file
diff --git a/src/js/upload_ui.js b/src/js/upload_ui.js
new file mode 100644
index 0000000..5b073f5
--- /dev/null
+++ b/src/js/upload_ui.js
@@ -0,0 +1,487 @@
+const CURRENT_LOCALE = chrome.i18n.getMessage('@@ui_locale');

+const MULTIPART_FORMDATA_BOUNDARY = 'Google_Chrome_Screen_Capture';

+const HIDE_ERROR_INFO_DELAY_TIME = 5000;

+

+var UploadUI = {

+  currentSite: '',

+  uploading: false,

+  sites: {},

+

+  registerSite: function(id, siteObject) {

+    this.sites[id] = siteObject;

+  },

+

+  getSiteObject: function(id) {

+    return this.sites[id];

+  },

+

+  setUploading: function(state) {

+    UploadUI.uploading = state;

+  },

+

+  init: function() {

+    // Register supported site for image sharing.

+    UploadUI.registerSite(SinaMicroblog.siteId, SinaMicroblog);

+    UploadUI.registerSite(Facebook.siteId, Facebook);

+    UploadUI.registerSite(Picasa.siteId, Picasa);

+    UploadUI.registerSite(Imgur.siteId, Imgur);

+

+    // Import style sheet for current locale

+    if (CURRENT_LOCALE == 'zh_CN')

+      UI.addStyleSheet('./i18n_styles/zh_CN_upload_image.css');

+    else

+      UI.addStyleSheet('./i18n_styles/en_US_upload_image.css');

+

+    // Get i18n message

+    i18nReplace('shareToSinaMicroblogText', SinaMicroblog.siteId +

+      '_upload_header');

+    i18nReplace('shareToFacebookText', Facebook.siteId + '_upload_header');

+    i18nReplace('shareToPicasaText', Picasa.siteId + '_upload_header');

+    i18nReplace('lastStep', 'return_to_site_selection');

+    i18nReplace('closeUploadWrapper', 'close_upload_wrapper');

+    i18nReplace('imageCaptionText', 'image_caption');

+    i18nReplace('photoSizeTip', 'photo_size_tip');

+    i18nReplace('shareToImgurText', Imgur.siteId + '_upload_header');

+    $('requiredFlag').setAttribute('title',

+      chrome.i18n.getMessage('invalid_caption'));

+

+    // Add event listeners

+    //$('btnUpload').addEventListener('click', UploadUI.showUploadWrapper, false);

+    $('btnSave').addEventListener('click', UploadUI.saveImage, false);

+    $('closeUploadWrapper').addEventListener('click',

+      UploadUI.hideUploadWrapper, false);

+

+    $('picasaBtn').addEventListener('click', function() {

+      UploadUI.showUploadContentWrapper(Picasa.siteId);

+    });

+    $('facebookBtn').addEventListener('click', function() {

+      UploadUI.showUploadContentWrapper(Facebook.siteId);

+    }, false);

+    $('sinaMicroblogBtn').addEventListener('click', function() {

+      UploadUI.showUploadContentWrapper(SinaMicroblog.siteId);

+    }, false);

+    $('imgurBtn').addEventListener('click', function() {

+      UploadUI.showUploadContentWrapper(Imgur.siteId);

+    }, false);

+    $('shareToOtherAccount').addEventListener('click', function() {

+      var currentSite = UploadUI.currentSite;

+

+      // Validate image description first

+      if (UploadUI.validatePhotoDescription(currentSite)) {

+        var callback = function() {

+          var authenticationTip =

+            chrome.i18n.getMessage('user_authentication_tip');

+          UploadUI.showAuthenticationProgress(authenticationTip);

+          UploadUI.getAccessToken(currentSite);

+        };

+        var users = Account.getUsers(currentSite);

+        var numberOfUsers = Object.keys(users).length;

+

+        // Logout when user has authenticated app

+        if (numberOfUsers) {

+          var logoutTip = chrome.i18n.getMessage('user_logout_tip');

+          UploadUI.showAuthenticationProgress(logoutTip);

+          var site = UploadUI.getSiteObject(currentSite);

+          site.logout(callback);

+        } else {

+          callback();

+        }

+      }

+    }, false);

+    $('lastStep').addEventListener('click', UploadUI.showUploadSitesWrapper,

+      false);

+  },

+

+  showUploadWrapper: function() {

+    var uploadWrapper = $('uploadWrapper');

+    UI.show(uploadWrapper);

+

+    // Reset upload wrapper position

+    var viewportWidth = window.innerWidth;

+    var viewportHeight = window.innerHeight;

+    var wrapperWidth = uploadWrapper.offsetWidth;

+    var wrapperHeight = uploadWrapper.offsetHeight;

+

+    var left = (viewportWidth - wrapperWidth) / 2;

+    var top = (viewportHeight - wrapperHeight) / 3;

+    left = left < 0 ? 0 : left;

+    top = top < 0 ? 0 : top;

+

+    var scrollTop = document.body.scrollTop;

+    var scrollLeft = document.body.scrollLeft;

+

+    UI.setStyle(uploadWrapper, {

+      top: top + scrollTop + 'px',

+      left: left + scrollLeft + 'px'

+    });

+    UploadUI.showUploadSitesWrapper();

+    UploadUI.showOverlay();

+  },

+

+  hideUploadWrapper: function() {

+    UI.hide($('uploadWrapper'));

+    UploadUI.hideOverlay();

+  },

+

+  showOverlay: function() {

+    var overlay = $('overlay');

+    UI.setStyle(overlay, {

+      width: document.body.scrollWidth + 'px',

+      height: document.body.scrollHeight + 'px'

+    });

+    UI.show($('overlay'));

+  },

+

+  hideOverlay: function() {

+    UI.hide($('overlay'));

+  },

+

+  updateUploadHeader: function(title) {

+    $('uploadHeader').firstElementChild.firstElementChild.innerText = title;

+  },

+

+  showUploadSitesWrapper: function() {

+    var uploadHeader = chrome.i18n.getMessage('upload_sites_header');

+    UploadUI.updateUploadHeader(uploadHeader);

+    UI.show($('uploadSitesWrapper'));

+    UploadUI.hideUploadContentWrapper();

+    UI.hide($('lastStep'));

+  },

+

+  hideUploadSitesWrapper: function() {

+    UI.hide($('uploadSitesWrapper'));

+  },

+

+  showUploadContentWrapper: function(site) {

+    UploadUI.currentSite = site;

+

+    // Update upload wrapper UI

+    var uploadHeader = chrome.i18n.getMessage(site + '_upload_header');

+    UploadUI.updateUploadHeader(uploadHeader);

+    UploadUI.hideUploadSitesWrapper();

+    UploadUI.hideErrorInfo();

+    UploadUI.hideAuthenticationProgress();

+    UploadUI.clearPhotoDescription();

+    UI.show($('uploadContentWrapper'));

+    UI.show($('lastStep'));

+    UploadUI.updateShareToOtherAccountText(site);

+    UploadUI.togglePhotoDescriptionRequiredFlag(site);

+

+    // Show authenticated accounts of current site

+    UploadUI.clearAccounts();

+    var users = Account.getUsers(site);

+    for (var userId in users) {

+      UploadUI.addAuthenticatedAccount(site, userId);

+    }

+  },

+

+  hideUploadContentWrapper: function() {

+    UI.hide($('uploadContentWrapper'));

+  },

+

+  clearPhotoDescription: function() {

+    $('imageCaption').value = '';

+  },

+

+  validatePhotoDescription: function(site) {

+    var caption = $('imageCaption');

+    var invalidCaptionMsg = chrome.i18n.getMessage('invalid_caption');

+

+    // Validate photo description

+    if (site == SinaMicroblog.siteId && caption.value == '') {

+      UploadUI.showErrorInfo(invalidCaptionMsg);

+      caption.focus();

+      return false;

+    }

+    return true;

+  },

+

+  togglePhotoDescriptionRequiredFlag: function(siteId) {

+    if (siteId == SinaMicroblog.siteId)

+      UI.show($('requiredFlag'));

+    else

+      UI.hide($('requiredFlag'));

+  },

+

+  updateShareToOtherAccountText: function(siteId) {

+    var users = Account.getUsers(siteId);

+    var userLength = Object.keys(users).length;

+    if (userLength)

+      i18nReplace('shareToOtherAccount', 'share_to_other_account');

+    else

+      i18nReplace('shareToOtherAccount', 'share_to_' + siteId + '_account');

+  },

+

+  showErrorInfo: function(text) {

+    UI.show($('errorWrapper'));

+    $('errorInfo').innerHTML = text;

+    setTimeout(function() {

+      UploadUI.hideErrorInfo();

+    }, HIDE_ERROR_INFO_DELAY_TIME);

+  },

+

+  hideErrorInfo: function() {

+    UI.hide($('errorWrapper'));

+  },

+

+  showProgressBar: function(accountId) {

+    var progress = document.querySelector('#' + accountId +

+      ' .progressBar');

+    UI.show(progress);

+  },

+

+  hideProgressBar: function(accountId) {

+    var progress = document.querySelector('#' + accountId +

+      ' .progressBar');

+    UI.hide(progress);

+  },

+

+  showAuthenticationProgress: function(title) {

+    var progress = $('authenticationProgress');

+    progress.setAttribute('title', title);

+    UI.show(progress);

+  },

+

+  hideAuthenticationProgress: function() {

+    UI.hide($('authenticationProgress'));

+  },

+

+  setProgress: function(accountId, loaded, total) {

+    console.log('In setProgress, loaded: ' + loaded + ', total: ' + total);

+    var progress = document.querySelector('#' + accountId + ' .progressBar');

+

+    // One progress bar has 4 parts to represent progress

+    var level = parseInt(loaded / total / 0.25);

+    UI.setStyle(progress, 'background-position-y', '-' + (12 * level) + 'px');

+  },

+

+  showPhotoLink: function(accountId, link) {

+    var photoLink = document.querySelector('#' + accountId + ' .photoLink');

+    photoLink.setAttribute('href', link);

+    UI.setStyle(photoLink, 'display', 'inline');

+  },

+

+  hidePhotoLink: function(accountId) {

+    var photoLink = document.querySelector('#' + accountId + ' .photoLink');

+    UI.hide(photoLink);

+  },

+

+  showUploadInfo: function(accountId, text) {

+    var uploadInfo = document.querySelector('#' + accountId + ' .uploadInfo');

+    uploadInfo.innerHTML = text;

+    UI.show(uploadInfo);

+  },

+

+  hideUploadInfo: function(accountId) {

+    var uploadInfo = document.querySelector('#' + accountId + ' .uploadInfo');

+    UI.hide(uploadInfo);

+  },

+

+  clearAccounts: function() {

+    $('uploadAccountList').innerHTML = '';

+  },

+

+  addAuthenticatedAccount: function(site, userId) {

+    var template = $('accountItemTemplate').innerHTML;

+

+    // Replace i18n message

+    template = template.replace(/\$\{accountId\}/gi, site + '_' + userId);

+    var shareToText = chrome.i18n.getMessage('share_to');

+    template = template.replace(/\$\{accountName\}/gi,

+      shareToText + ' ' + Account.getUser(site, userId)['name']);

+    template = template.replace('${site}', site);

+    template = template.replace('${userId}', userId);

+    template = template.replace(/\$\{deletionTitle\}/gi,

+      chrome.i18n.getMessage('deletion_title'));

+    template = template.replace(/\$\{photoLinkText\}/gi,

+      chrome.i18n.getMessage('photo_link_text'));

+    template = template.replace(/\$\{progressInfo\}/gi,

+      chrome.i18n.getMessage('progress_info'));

+

+    // At most show 3 authenticated users

+    var uploadAccountList = $('uploadAccountList');

+    var accountsNumber = uploadAccountList.childElementCount;

+    if (accountsNumber == 2) {

+      uploadAccountList.removeChild(uploadAccountList.lastElementChild);

+    }

+    uploadAccountList.innerHTML = template + uploadAccountList.innerHTML;

+

+    $('accountName').addEventListener('click', function(e) {

+      UploadUI.upload(site, userId);

+    });

+    $('deleteBtn').addEventListener('click', function(e) {

+      e.stopPropagation();

+      UploadUI.deleteAccountItem(site + '_' + userId);

+    });

+

+    UploadUI.updateShareToOtherAccountText(site);

+  },

+

+  deleteAccountItem: function(accountId, noConfirm) {

+    if (UploadUI.uploading && !noConfirm)

+      return;

+    var confirmText = chrome.i18n.getMessage('account_deletion_confirm');

+    if (noConfirm || confirm(confirmText)) {

+      $('uploadAccountList').removeChild($(accountId));

+

+      // Clear localStorage

+      var site = accountId.split('_')[0];

+      var userId = accountId.split('_')[1];

+      Account.removeUser(site, userId);

+      UploadUI.updateShareToOtherAccountText(site);

+    }

+  },

+

+  upload: function(siteId, userId) {

+    if (UploadUI.uploading)

+      return;

+

+    // Initialize UI

+    var accountId = siteId + '_' + userId;

+    UploadUI.hideErrorInfo();

+    UploadUI.hideUploadInfo(accountId);

+    UploadUI.hidePhotoLink(accountId);

+    if (!UploadUI.validatePhotoDescription(siteId))

+      return;

+    var caption = $('imageCaption').value;

+

+    // Get ready for upload image.

+    photoshop.draw();

+    UploadUI.setUploading(true);

+    UploadUI.showProgressBar(accountId);

+

+    var site = UploadUI.getSiteObject(siteId);

+    var user = Account.getUser(siteId, userId);

+    var imageData = UploadUI.getImageData();

+    var infoText;

+

+    var callback = function(result, photoIdOrMessage) {

+      if (result == 'success') {

+        infoText = chrome.i18n.getMessage('get_photo_link');

+        UploadUI.showUploadInfo(accountId, infoText);

+        site.getPhotoLink(user, photoIdOrMessage, function(photoLinkResult,

+                                                           photoLinkOrMessage) {

+          if (photoLinkResult == 'success') {

+            UploadUI.setUploading(false);

+            UploadUI.hideUploadInfo(accountId);

+            UploadUI.showPhotoLink(accountId, photoLinkOrMessage);

+          } else {

+            UploadUI.showErrorInfo(photoLinkOrMessage);

+          }

+        });

+      } else {

+        if (photoIdOrMessage == 'bad_access_token' ||

+            photoIdOrMessage == 'invalid_album_id') {

+          Account.removeUser(site.siteId, site.currentUserId);

+          UploadUI.deleteAccountItem(accountId, true);

+          UploadUI.getAccessToken(siteId);

+        }

+        UploadUI.setUploading(false);

+        UploadUI.hideProgressBar(accountId);

+        UploadUI.showErrorInfo(chrome.i18n.getMessage(photoIdOrMessage));

+      }

+      UploadUI.hideProgressBar(accountId);

+    };

+

+    if (user) {

+      site.currentUserId = user.id;

+      site.upload(user, caption, imageData, callback);

+    } else {

+      UploadUI.getAccessToken(siteId);

+    }

+  },

+

+  getAccessToken: function(siteId) {

+    var site = UploadUI.getSiteObject(siteId);

+    var accessTokenCallback = function(result, userOrMessage) {

+      if (result == 'success') {

+        UploadUI.getUserInfo(siteId, userOrMessage);

+      } else {

+        // Show error information according to error reason

+        UploadUI.showErrorInfo(chrome.i18n.getMessage(userOrMessage));

+        UploadUI.hideAuthenticationProgress();

+      }

+    };

+

+    site.getAccessToken(accessTokenCallback);

+  },

+

+  getUserInfo: function(siteId, user) {

+    var site = UploadUI.getSiteObject(siteId);

+    site.getUserInfo(user, function(result, userOrMessage) {

+      if (result == 'success') {

+        var userId = user.id;

+        // Check if the authenticated user is added.

+        if (!Account.getUser(siteId, userId)) {

+          site.currentUserId = userId;

+          Account.addUser(siteId, user);

+          UploadUI.addAuthenticatedAccount(siteId, userId);

+        }

+        UploadUI.upload(siteId, userId);

+      } else {

+        var msg = chrome.i18n.getMessage(userOrMessage);

+        UploadUI.showErrorInfo(msg);

+      }

+      UploadUI.hideAuthenticationProgress();

+    });

+  },

+

+  getImageData: function() {

+    var dataUrl = $('canvas').toDataURL('image/png');

+    var imageDataIndex = dataUrl.indexOf('data:image/png;base64,');

+    if (imageDataIndex != 0) {

+      return;

+    }

+

+    // Decode to binary data

+    return atob(dataUrl.substr(imageDataIndex + 22));

+  },

+

+  saveImage: function() {

+    $('canvas').toBlob(function(blob) {

+      console.log(chrome.extension.getBackgroundPage());

+      saveAs(blob, chrome.extension.getBackgroundPage().screenshot.screenshotName+".png");

+    });

+  }

+};

+

+(function() {

+// Cache tab id of edit page, so that we can get tab focus after getting access

+// token.

+var tabIdOfEditPage;

+chrome.tabs.getSelected(null, function(tab) {

+  tabIdOfEditPage = tab.id;

+});

+

+function selectTab(tabId) {

+  chrome.tabs.update(tabId, {

+    selected: true

+  });

+}

+

+function closeTab(tabId) {

+  chrome.tabs.remove(tabId);

+}

+

+function parseAccessToken(senderId, url, siteId) {

+  var sites = UploadUI.sites;

+  for (var id in sites) {

+    var site = sites[id];

+    if ((siteId && id == siteId) || site.isRedirectUrl(url)) {

+      selectTab(tabIdOfEditPage);

+      closeTab(senderId);

+      site.parseAccessToken(url);

+      return true;

+    }

+  }

+  return false;

+}

+

+chrome.extension.onMessage.addListener(function(request, sender) {

+  switch (request.msg) {

+  case 'url_for_access_token':

+    parseAccessToken(sender.tab.id, request.url, request.siteId);

+    break;

+  }

+});

+})();