Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use a port for communication #15

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
full-page-screen-capture.pem
.DS_Store
*~
100 changes: 98 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,103 @@ Or, for development:
4. Click on “Load unpacked extension…”
5. Select the folder for this app

### Extra notes:
### Extra notes

* Don’t move your mouse around during the screen capture, it will cause the scroll bar to appear!
* For best results, select `View -> Actual Size` in Chrome.
* Please report any bugs that you find.

### Adding screen capture ability to your own extension

It's simple to extract the capture code to use in your own extension.

* Copy `api.js` and `page.js` into your extension. If you change the name
of `page.js` you _must_ put the new file name into the
`injectedCaptureFilename` variable at the top of `api.js`.
* Load `api.js` into your extension HTML (see `popup.html` for an example).
* Call `pageCaptureAPI()`. This returns a Javascript object with 3
functions on it: `captureToBlob`, `captureToCanvas` and `captureToFile`,
described below. _Note_: only call `pageCaptureAPI()` once. Keep its
result to then make as many captures as you need. This is because
`pageCaptureAPI()` sets Chrome up to process messages from the injected
capture script, and we only want to do that once.

#### captureToBlob

The call signature of `captureToBlob` is

```javascript
captureToBlob(tab, callback, errback, progress);
```

* `tab` is a Chrome
[Tab](http://developer.chrome.com/extensions/tabs.html#type-Tab)
object. You'll typically get this from a call to a function like
[chrome.tabs.getCurrent](http://developer.chrome.com/extensions/tabs.html#method-getCurrent).

* `callback(blob)` (optional): pass a function that will be called with a
[Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) instance
containing the PNG data of the screen capture.

* `errback(reason)` (optional): pass a function that will be called if an
error occurs. The `reason` argument can be a string or a
[FileError](https://developer.mozilla.org/en-US/docs/Web/API/FileError)
instance. The possible string values are

* `execute timeout`: the call to inject the `page.js` file into the tab
timed out.
* `internal error`: the screen capture logic did something unexpected
(please report this!).
* `invalid url`: the URL of the tab is not legal for screen capture
(due to Chrome restrictions).

* `progress(amount)` (optional): pass a function that will be called to
indicate progress of the screen capture. The function will be called with
`0` when screen capture is initiated, which will allow you to do any
required UI initialization (see `popup.js` for an example). Thereafter,
`progress` will be called with values that increase to `1.0`.

#### captureToCanvas

The call signature of `captureToCanvas` is

```javascript
captureToCanvas(tab, callback, errback, progress);
```

* `tab` see `captureToBlob` above.

* `callback(canvas)` (optional): pass a function that will be called with a
[Canvas](https://developer.mozilla.org/en/docs/HTML/Canvas) instance
containing the full image of the screen capture. If you want to obtain a
[Data URI](https://en.wikipedia.org/wiki/Data_URI_scheme) from the
canvas, call `canvas.toDataURL()`.

* `errback(reason)` (optional): see `captureToBlob` above.

* `progress(amount)` (optional): see `captureToBlob` above.

#### captureToFile

The call signature of `captureToFile` is

```javascript
captureToFile(tab, filename, callback, errback, progress);
```

* `tab` see `captureToBlob` above.

* `filename` is the base name of a temporary file to create in the
browser's file system (see the
[File API documentation](https://developer.mozilla.org/en-US/docs/Web/API/File)
for more information).

* `callback(filename)` (optional): pass a function that will be called with
the full pathname of the created file. This will be something like
`filesystem:chrome-extension:///temporary/fdpohaocaechififmbbbbbknoalclacl/name`
where the final `name` component is the `filename` you passed to
`captureToFile`. The returned filename is suitable for opening with
`window.open` (see `popup.js` for an example).

* `errback(reason)` (optional): see `captureToBlob` above.

* `progress(amount)` (optional): see `captureToBlob` above.
223 changes: 223 additions & 0 deletions api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
var pageCaptureAPI = function() {
var injectedCaptureFilename = 'page.js',
currentCapture = null,
initialized = false,
matches = [/^https?:\/\/.*\//, /^ftp:\/\/.*\//, /^file:\/\/.*\//],
noMatches = [/^https?:\/\/chrome.google.com\//],

validURL = function (url) {
// URL Matching test - to verify we can talk to this URL.
// Couldn't find a better way to tell if executeScript would
// fail -- so just testing against known urls for now.
var i;
for (i = 0; i < noMatches.length; i++) {
if (noMatches[i].test(url)) {
return false;
}
}
for (i = 0; i < matches.length; i++) {
if (matches[i].test(url)) {
return true;
}
}
return false;
},

listen = function() {
// Set up to receive and process messages from capture-content.js
chrome.runtime.onConnect.addListener(function(port) {
// Check details of the port to ensure we don't react to
// anything we should ignore. A chrome.runtime.connect
// call not meant for us *must* be ignored. Do not close
// the port, do not call the errback, etc.
if (port.name === 'page capture' && port.sender.id === chrome.runtime.id) {
port.onMessage.addListener(function(request) {
if (request.msg === 'capture') {
currentCapture.progress(request.complete);
capture(request, port);
}
else if (request.msg === 'done') {
currentCapture.callback(currentCapture.screenshot.canvas);
currentCapture = null;
}
else {
console.error('Received unknown request from ' +
injectedCaptureFilename + ': ', request);
port.disconnect();
currentCapture.errback('internal error');
}
});

// Ask for the first arrangement.
requestArrangement(port);
}
});
},

inject = function(tab, callback) {
// Inject capture content script into the given tab. Call the
// callback with a Boolean to indicate success or failure.
var loaded = false,
timeout = 3000,
timedOut = false;

// Inject the capture script into the tab.
chrome.tabs.executeScript(tab.id, {file: injectedCaptureFilename}, function() {
if (!timedOut) {
loaded = true;
callback(true);
}
});

// Return a false value if the execution of capture content
// script doesn't complete quickly enough.
window.setTimeout(
function() {
if (!loaded) {
console.error('Timed out too early while waiting for ' +
'chrome.tabs.executeScript. Try increasing the timeout.');
timedOut = true;
callback(false);
}
},
timeout);
},

requestArrangement = function(port) {
port.postMessage('send arrangement');
},

capture = function(data, port) {
var canvas;
if (!currentCapture.screenshot.canvas) {
canvas = document.createElement('canvas');
canvas.width = data.totalWidth;
canvas.height = data.totalHeight;
currentCapture.screenshot.ctx = canvas.getContext('2d');
currentCapture.screenshot.canvas = canvas;
// Scale to account for device pixel ratios greater than one.
// On a MacBook Pro with Retina display, window.devicePixelRatio = 2
if (window.devicePixelRatio !== 1){
currentCapture.screenshot.ctx.scale(1 / window.devicePixelRatio,
1 / window.devicePixelRatio);
}
}

// Capture the currently visible part of the tab and save it into
// the full screenshot we're assembling.
chrome.tabs.captureVisibleTab(
null, {format: 'png', quality: 100},
function(dataURI) {
if (dataURI) {
var image = new Image();
image.onload = function() {
currentCapture.screenshot.ctx.drawImage(image, data.x, data.y);
// Tell the injected capture code to move on.
requestArrangement(port);
};
image.src = dataURI;
}
else {
console.error('Oops! invalid dataURI from captureVisibleTab', dataURI);
}
}
);
},

getBlob = function(canvas) {
// standard dataURI can be too big, let's blob instead
// http://code.google.com/p/chromium/issues/detail?id=69227#c27

var dataURI = canvas.toDataURL(),
// convert base64 to raw binary data held in a string
// doesn't handle URLEncoded DataURIs
byteString = atob(dataURI.split(',')[1]),
// separate out the mime component
mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0],
// write the bytes of the string to an ArrayBuffer
ab = new ArrayBuffer(byteString.length),
ia = new Uint8Array(ab),
i;

for (i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}

return new Blob([ab], {type: mimeString});
},

saveBlob = function(blob, filename, callback, errback) {
var onWriteEnd = function() {
// Return the name of the file that now contains the blob.
callback('filesystem:chrome-extension://' + chrome.runtime.id + '/temporary/' + filename);
};

window.webkitRequestFileSystem(TEMPORARY, 1024*1024, function(fs){
fs.root.getFile(filename, {create:true}, function(fileEntry) {
fileEntry.createWriter(function(fileWriter) {
fileWriter.onwriteend = onWriteEnd;
fileWriter.write(blob);
}, errback);
}, errback);
}, errback);
},

captureToCanvas = function(tab, callback, errback, progress) {
// Call callback with a new canvas object holding the full screenshot.

if (!validURL(tab.url)) {
errback('invalid url');
return;
}

if (currentCapture) {
console.error('Oops... Capture apparently already in progress!');
return;
}

currentCapture = {
callback: callback || function(){},
errback: errback || function(){},
progress: progress || function(){},
screenshot: {}
};

if (!initialized) {
listen();
initialized = true;
}

// Inject the capture content script into the tab.
inject(tab, function(injected) {
if (injected) {
// Let our caller know that the capture is about to begin.
progress(0);
}
else {
errback('execute timeout');
}
});
},

captureToBlob = function(tab, callback, errback, progress) {
captureToCanvas(tab,
function(canvas) {
callback(getBlob(canvas));
},
errback, progress);
};

captureToFile = function(tab, filename, callback, errback, progress) {
captureToBlob(tab,
function(blob) {
saveBlob(blob, filename, callback, errback);
},
errback, progress);
};

return {
captureToBlob: captureToBlob,
captureToCanvas: captureToCanvas,
captureToFile: captureToFile
};
};
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Full Page Screen Capture",
"version": "0.0.7",
"version": "0.0.9",
"manifest_version": 2,
"description": "Screen capture your current page in entirety and reliably!",
"browser_action": {
Expand Down
Loading