Skip to content

Loading…

Bug 586047 - add image data support to the clipboard api #427

Merged
merged 15 commits into from

3 participants

@ZER0

No description provided.

@ochameau ochameau commented on the diff
packages/addon-kit/lib/clipboard.js
((11 lines not shown))
+ try {
+ let input = Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(Ci.nsIStringInputStream);
+
+ input.setData(image, image.length);
+
+ imageTools.decodeImageData(input, flavor, container);
+ }
+ catch (e) {
+ throw new Error("Unable to decode data given in a valid image.");
+ }
+
+ // Store directly the input stream makes the cliboard's data available
+ // for Firefox but not to the others application or to the OS. Therefore,
+ // a `nsISupportsInterfacePointer` object that reference an `imgIContainer`
+ // with the image is needed.
@ochameau Mozilla member
ochameau added a note

It might be usefull to add some links about this.
Like platform code that highlight these interfaces:
http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530

@ZER0
ZER0 added a note

That doesn't really explain why a nsISupportsInterfacePointer is needed to makes the clipboard's data available for the others application. It is something I figured out. Or do you mean I should add something like "To understand the copy action on platform side, see: http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530" ?

@ochameau Mozilla member

Actually, as there is not documentation on this, C++ code can be considered as a usefull resource to explain why we are doing this.
To me, this piece of code tells that we have to use nsISupportsInterfacePointer in setTransferData.
This C++ code is a pure equivalent of your JS code.
You could have figured this out by reading it, but the hard part is about how to find such piece of code :p
(I found it by searching for setTransferData usages)

@ZER0
ZER0 added a note

You can't figure out reading the C++ code what I explained in the comment. I didn't see anything in the C++ code that said that if you "Store directly the input stream makes the cliboard's data available for Firefox but not to the others application or to the OS".
Actually, if you never doing that by yourself but just "copied" the C++ you will never figure it out.
And that's why I didn't see the value to add this link to "explain why": the link didn't explain anything about the comment I wrote.

@ochameau Mozilla member

Oh what's going on there. I'm trying to help you looking for information and give additional material to document your code.
Don't be too picky about review comments. I want to improve comments about this method, not just one sentence in this comment. The C++ code does exactly what you describe in your second sentence Therefore, ansISupportsInterfacePointerobject that reference animgIContainerwith the image is needed., and what you code does.

@ZER0
ZER0 added a note

Sorry if there is some misunderstanding, but I'm not picky. I just think that put that link "as is" in the comment is wrong.

As you said, the code I wrote and C++ code doing the same. The difference is that in the C++ code there is no explanation about what's happening – why that process is needed – where in my code I explicitly said that.
The C++ code use a nsISupportsInterfacePointer but it doesn't explain why. So, to me, is totally useless: it's basically the same code, as you said, but looking at it you can't figure out why these steps are needed.

So, to me it's wrong put this link in this comment as is because if I read that comment I would expect that the link will bring to a code that explain what is described above, but it doesn't.

Notice that I'm not against to put that link, I just think that put the link in that context is misleading. As I suggested the link could be added in a different context – same comment, but with a sentence that define the link contents better, something like "To understand the copy action on platform side, see: http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530".

Or maybe put that link before line 136. What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ochameau ochameau commented on the diff
packages/addon-kit/lib/clipboard.js
((9 lines not shown))
let options = {
data: aData,
datatype: aDataType || "text"
};
+
+ // If `aDataType` is not given or if it's "image", the data is parsed as
+ // data URL to detect a better datatype
+ if (!aDataType || aDataType === "image") {
+ try {
+ let dataURL = new DataURL(aData);
+
+ options.datatype = dataURL.mimeType;
+ options.data = dataURL.data;
+ }
+ catch (e if e.name === "URIError") {
+ // Not a valid Data URL
@ochameau Mozilla member
ochameau added a note

Shouldn't we throw here, expect when aDataType is null?

@ZER0
ZER0 added a note

No, this is just an attempt to detect the data type if omitted – or if "image" is given. If we are not to able to auto detect the datatype, we proceed with the defaults and the information we have.

@ochameau Mozilla member
ochameau added a note

Let me rephrase it with an example. If I do clipboard.set("data:buggy-uri", "image"), I could receive this meaningfull URIError exception instead of a random exception in following code. I think that this code is going to throw Unable to decode data given in a valid image..

It's expected to ignore the exception when the data type isn't specified, but I don't think it is valuable to ignore it when we explicitely pass "image".

@ZER0
ZER0 added a note

But that's the point, your example is perfectly valid. If you pass an image's data that you can't decode in a valid image, you will get an exception. So there is no error about that. I'm not sure I'm following you. In the way the code is now, we can have:

clipboard.set("data:text/html,<b>foo</b>");

as well, that is consistent now that we can pass a data URL, that is equivalent to have:

clipboard.set("<b>foo</b>", "html");

Of course if I have something like:

cliboard.set("data:buggy-uri");

It will ends to set that data as plain text.

I do not see the advantage to throw an exception for images at this point, if it's throw in any case, and give to us the ability to detect the data type in general.
Of course I could throw the exception only if aDataType is equals to "image", but I don't see the point to add another check here and a new exception if this scenario is in any case trapped later.

@ochameau Mozilla member
ochameau added a note

If you are sure that a meaningfull exception is thrown on clipboard.set("data:buggy-uri", "image"), then that's fine.
It is just a weak assumption that some code will later throw a nice error message whereas we know that it is going to fail right here.

@ZER0
ZER0 added a note

I think that "Unable to decode data given in a valid image." is quite understandable for clipboard.set("data:buggy-uri", "image"). It's not a weak assumption that some code will later throw a exception later, I design the code explicitly in that way in order to avoid to add unnecessary complexity.

@ochameau Mozilla member

It is ok, but less explicit than URIError: Malformed Data URL: data:buggy-uri. I let you judge your own code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ochameau ochameau commented on an outdated diff
packages/addon-kit/lib/clipboard.js
((17 lines not shown))
+ if (image instanceof Ci.nsISupportsInterfacePointer)
+ image = image.data;
+
+ if (image instanceof Ci.imgIContainer)
+ image = imageTools.encodeImage(image, flavor);
+
+ if (image instanceof Ci.nsIInputStream) {
+ let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].
+ createInstance(Ci.nsIBinaryInputStream);
+
+ binaryStream.setInputStream(image);
+
+ dataURL.data = binaryStream.readBytes(binaryStream.available());
+
+ data = dataURL.toString();
+ } else
@ochameau Mozilla member
ochameau added a note

if/else shouldn't be on the same line than the brace:

}
else
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ochameau ochameau commented on an outdated diff
packages/addon-kit/lib/clipboard.js
@@ -185,6 +241,38 @@ exports.get = function(aDataType) {
case "text/html":
data = data.value.QueryInterface(Ci.nsISupportsString).data;
break;
+ case "image/png":
+ let dataURL = new DataURL();
+
+ dataURL.mimeType = flavor;
+ dataURL.base64 = true;
+
+ let image = data.value;
+
+ // Due to the differences in how the images in the clipboard could be
+ // stored, the checks below are needed. The clipboard could be already
+ // provide the image as byte streams, but also as pointer, or as
+ // image container.
@ochameau Mozilla member
ochameau added a note

I would rather say:
Due to the differences in how images could be stored in the clipboard, ...
be seems to be useless here: The clipboard could already provide.
There is two spaces after also.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ochameau ochameau commented on an outdated diff
packages/api-utils/lib/url.js
((11 lines not shown))
+ * data.
+ */
+const DataURL = Class({
+
+ get base64 () {
+ return "base64" in this.parameters;
+ },
+
+ set base64 (value) {
+ if (value)
+ this.parameters["base64"] = "";
+ else
+ delete this.parameters["base64"];
+ },
+ /**
+ * Initialize the Data URL object. If a uri is give, it will be parsed.
@ochameau Mozilla member
ochameau added a note

give -> given

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ochameau ochameau commented on an outdated diff
packages/api-utils/lib/url.js
((18 lines not shown))
+
+ set base64 (value) {
+ if (value)
+ this.parameters["base64"] = "";
+ else
+ delete this.parameters["base64"];
+ },
+ /**
+ * Initialize the Data URL object. If a uri is give, it will be parsed.
+ *
+ * @param {String} [uri] The uri to parse
+ *
+ * @throws {URIError} if the Data URL is malformed
+ */
+ initialize: function(uri) {
+ // Due to the bug 751834 is not possible document and define these
@ochameau Mozilla member
ochameau added a note

due to the bug -> due to bug
is not -> it is not

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ochameau ochameau commented on an outdated diff
packages/api-utils/lib/url.js
((43 lines not shown))
+ * The MIME type of the data. By default is empty, that accordingly to RFC
+ * is equivalent to "text/plain"
+ */
+ this.mimeType = "";
+
+ /**
+ * The string that represent the data in the Data URL
+ */
+ this.data = "";
+
+ if (!uri)
+ return;
+
+ uri = String(uri);
+
+ let matches = uri.match(/^data:([^,]*),([\s\S]*)/i);
@ochameau Mozilla member
ochameau added a note

Is there any reason to use \s\S instead of .?
I'd rewrite it to: /^data:([^,]*),(.*)$/i

@ZER0
ZER0 added a note

I usually prefer something like \s\S, that is commonly used in the community, to indicate "everything" when I don't want to provide a full regexp pattern that match the specs. The dot is generic enough, but doesn't really means "everything".
However I do not have any hard feeling about it, so if you prefer the dot in that case I can replace it.

@ochameau Mozilla member

We should'nt use something because it is commonly used but on purpose!!
Can a data URI contain \n \r \u2028 or \u2029 ?
If yes, comment that; if not, use . and do not forget to add $ at end of regexp in any case.

@ZER0
ZER0 added a note

I use something when I think is right. I prefer using an explicity "everything" when I do not care about the what I should actually have in the structure in accordance to the specs. Otherwise I prefer a regexp that match the specs. To me, find in a regexp something like [\s\S] tells immediately that I want everything. Found a dot could be not exactly "everything".

Because to me this is not really an issue, but more a matter of past experience and personal taste, I already modified that regexp in the last commit I did the other day.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ochameau ochameau commented on an outdated diff
packages/api-utils/lib/url.js
((91 lines not shown))
+ */
+ toString : function() {
+ let parametersList = [];
+
+ for (let name in this.parameters) {
+ let encodedParameter = encodeURIComponent(name);
+ let value = this.parameters[name];
+
+ if (value)
+ encodedParameter += "=" + encodeURIComponent(value);
+
+ parametersList.push(encodedParameter);
+ }
+
+ if (parametersList.length > 0)
+ parametersList.unshift("");
@ochameau Mozilla member
ochameau added a note

A simple comment about the necessity to start with ; would be welcomed.

@ZER0
ZER0 added a note

Not sure what you mean. It's like write a comment why the parameters in the querystring needs to start with "?"…

@ochameau Mozilla member
ochameau added a note

By reading your code we can see what it does but not why.
Why are you adding an empty string in this parametersList variable?
I suggested you to write the response to this question in a comment ;)

@ZER0
ZER0 added a note

because is in the specs? ;)

@ochameau Mozilla member
ochameau added a note

// Add an empty string in order to start with a ';' on join call.
It may be obvious for you as you wrote the code and you already know what are next actions,
but when you are reading the code for the first time, you first read this and don't really understand why would you add an empty string.
Part of the review is to improve readability of the code so that it is easier to understand for people that join the project.

@ZER0
ZER0 added a note

But this is not explain the necessity to start with ;, right? That's because I wasn't following you, I didn't understand which kind of comment I could put there to explain "the necessity".

If it's enough to you, I can just add a comment like the one you suggested.

@ochameau Mozilla member
ochameau added a note

Sorry if it wasn't clear enough, but yes, that's just about that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ochameau ochameau commented on an outdated diff
packages/api-utils/tests/test-url.js
((39 lines not shown))
+
+ test.assertEqual(dataURL.toString(), "data:text/html;charset=US-ASCII,%3Ch1%3EHello!%3C%2Fh1%3E");
+}
+
+exports.testDataURLparseBase64 = function (test) {
+ const { DataURL } = url;
+ const { base64Decode } = require("api-utils/utils/data");
+
+ let base64Data = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAQ0lEQVRYhe3OwQkAIBTD0Oyqg7idbqUr9B9EhBRyLY8F+0akEyBAgIBvAI1eCuaIEiBAgAABzwH50sNqAgQIEPAYcABJQw5EXdmcNgAAAABJRU5ErkJggg==";
+ let dataURL = new DataURL("data:image/png;base64," + base64Data);
+
+ test.assertEqual(dataURL.base64, true, "base64 is true for base64 encoded data uri")
+ test.assertEqual(dataURL.data, base64Decode(base64Data), "data is properly decoded")
+ test.assertEqual(dataURL.mimeType, "image/png", "mimeType is set properly")
+ test.assertEqual(Object.keys(dataURL.parameters).length, 1, "one parameters specified");
+
@ochameau Mozilla member
ochameau added a note

Can you add a test against this parameter value?

@ZER0
ZER0 added a note

In this specific case, the fact that dataURL.base64 is true and dataURL.parameters.length is 1 means that the parameters value has to be "base64", so it will be redundant.

But if you think it should be present, I can easily add that test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ochameau ochameau commented on an outdated diff
packages/api-utils/docs/url.md
((4 lines not shown))
+<api name="DataURL">
+@class
+<api name="DataURL">
+@constructor
+ The DataURL constructor creates an object that represents a data: URL,
+ verifying that the provided string is a valid data: URL in the process.
+
+@param uri {string}
+ A string to be parsed as Data URL. If is not a valid URI, this constructor
+ will throw an exception.
+</api>
+
+<api name="mimeType">
+@property {string}
+ The MIME type of the data. By default is empty, that accordingly to RFC2397
+ is equivalent to `text/plain`
@ochameau Mozilla member
ochameau added a note

By saing that I would expect it to become text/plain if it isn't set. Same thing for parameters.
I haven't read the RFC but it sounds like: if the data URI doesn't specify a mimeType, then the mimeType is text/plain.

@ZER0
ZER0 added a note

If you think is confusing, I can just remove these comments. I wanted just highlight that an empty mime type / parameters is allowed, but has a specific meaning for the browser.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ochameau ochameau commented on an outdated diff
packages/api-utils/lib/cuddlefish.js
@@ -384,6 +384,8 @@ const Loader = iced(function Loader(options) {
Cu: Cu,
Cr: Cr,
Cm: Cm,
+ atob: atob,
+ btoa: btoa,
@ochameau Mozilla member
ochameau added a note

I don't think chrome module is a goog placeholder for these as it doesn't give any priviledges and has nothing to do with existing attributes exposed by chrome. There may not be any ideal solution for this, but I'd prefer:

  • exposing them in require('@packaging')
  • a new specific pseudo module require("@base64")

And for these two options, this hack would only be used by api-utils/utils/data module.
Or expose atob and btoa to all modules by adding it to !globals.js ?

@Gozala What's your thoughts on this topic ?

@Gozala Mozilla member
Gozala added a note

@ochameau In fact I have suggested @ZER0 to put atob / btoa into chrome module but expose them to end user through some other utility module. So you won't write let { atob, btoa } = require('chrome') in your code instead you would do let { atob } = require('some/utilitiy/module'). I don't think @packaging is a good placeholder for that specially since we serialize / parse it as JSON in diff places.

@Gozala Mozilla member
Gozala added a note

Although, I just realized that in some case we do require('cuddlefish') and in such cases there will be no atob / btoa so this way is also no good. Something that may work and be a best option is probably a module in a following form:

// Loaded as JSM
if (~String(this).indexOf('BackstagePass')) {
  EXPORTED_SYMBOLS = [ 'atob, btoa' ];
} 
// Loaded as CommonJS module
else {
   const { Cu } = require('chrome');
   const { atob, btoa } = Cu.import(module.uri);
   exports.atob = atob;
   exports.btoa = btoa;
}
@Gozala Mozilla member
Gozala added a note

I don't know if patch above actually works, but it gives an idea I think.

@ochameau Mozilla member
ochameau added a note

Oh so the original hack is to get a reference from a JSM!
What about just doing this in api-utils/utils/data ? (and just this module!)
const { atob, btoa } = Cu.import("resource://gre/modules/Services.jsm");
Services.jsm is here to stay and is already loaded by Firefox.
And its implementation is really simple and won't mess with these methods:
http://mxr.mozilla.org/mozilla-central/source/toolkit/content/Services.jsm

Do we have a platform bug opened in order to expose them in all sandboxes?
It sounds like a really simple feature!!

@ZER0
ZER0 added a note

My first thought was add btoa and atob as globals, it was @Golaza that suggested to add them in "chrome" module instead.

I'm open to any of the options you have listed.

@ZER0
ZER0 added a note

As we discussed in IRC, we agreed on:

  1. Remove atob and btoa from chrome module
  2. Create a new api-utils/base64 module that exposes the base64 encode/decode functions
  3. Update the old api-utils/utils/data to notify to the developers they should use the new module instead
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ochameau ochameau commented on an outdated diff
packages/api-utils/tests/test-url.js
((36 lines not shown))
+ test.assertEqual(dataURL.mimeType, "text/html", "mimeType is set properly")
+ test.assertEqual(Object.keys(dataURL.parameters).length, 1, "one parameters specified");
+ test.assertEqual(dataURL.parameters["charset"], "US-ASCII", "charset parsed");
+
+ test.assertEqual(dataURL.toString(), "data:text/html;charset=US-ASCII,%3Ch1%3EHello!%3C%2Fh1%3E");
+}
+
+exports.testDataURLparseBase64 = function (test) {
+ const { DataURL } = url;
+ const { decode } = require("./base64");
+
+ let base64Data = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAQ0lEQVRYhe3OwQkAIBTD0Oyqg7idbqUr9B9EhBRyLY8F+0akEyBAgIBvAI1eCuaIEiBAgAABzwH50sNqAgQIEPAYcABJQw5EXdmcNgAAAABJRU5ErkJggg==";
+ let dataURL = new DataURL("data:image/png;base64," + base64Data);
+
+ test.assertEqual(dataURL.base64, true, "base64 is true for base64 encoded data uri")
+ test.assertEqual(dataURL.data, decode(base64Data), "data is properly decoded")
@ochameau Mozilla member

It would be better to check against a static value. It would allow to throw if something goes wrong with decode.
As we are using decode both internally and in test, we may have a bad base64 data without noticying it.

@ZER0
ZER0 added a note

Ok. I won't use a png then, but just a string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ZER0 ZER0 merged commit 3330c3e into mozilla:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 20, 2012
  1. @ZER0
  2. @ZER0

    Updated `base64Encode` and `base64Decode` methods to user `btoa` and …

    ZER0 committed
    …`atob` from `chrome` module.
  3. @ZER0

    Added `DataURL` object

    ZER0 committed
  4. @ZER0
Commits on Mar 26, 2012
  1. @ZER0
Commits on Apr 2, 2012
  1. @ZER0
Commits on Apr 5, 2012
  1. @ZER0

    Added `btoa` and `atob` to our MessageManager implementation to refle…

    ZER0 committed
    …ct the globals of `ContentFrameMessageManage` in Fennec.
Commits on Apr 6, 2012
  1. @ZER0
  2. @ZER0

    Fixed Test case

    ZER0 committed
  3. @ZER0

    Updated documentation.

    ZER0 committed
Commits on May 3, 2012
  1. @ZER0

    Merge remote-tracking branch 'upstream/master' into bug586047

    ZER0 committed
    Conflicts:
    	packages/addon-kit/lib/clipboard.js
    	packages/api-utils/lib/cuddlefish.js
    	packages/api-utils/lib/message-manager.js
Commits on May 4, 2012
  1. @ZER0
Commits on May 9, 2012
  1. @ZER0
Commits on May 31, 2012
  1. @ZER0
  2. @ZER0

    Addressed the review's comments

    ZER0 committed
This page is out of date. Refresh to see the latest.
View
38 packages/addon-kit/docs/clipboard.md
@@ -12,29 +12,59 @@ The following types are supported:
* `text` (plain text)
* `html` (a string of HTML)
+* `image` (a base-64 encoded png)
If no data type is provided, then the module will detect it for you.
+Currently `image`'s type doesn't support transparency on Windows.
+
Examples
--------
Set and get the contents of the clipboard.
- let clipboard = require("clipboard");
+ var clipboard = require("clipboard");
clipboard.set("Lorem ipsum dolor sit amet");
- let contents = clipboard.get();
+ var contents = clipboard.get();
Set the clipboard contents to some HTML.
- let clipboard = require("clipboard");
+ var clipboard = require("clipboard");
clipboard.set("<blink>Lorem ipsum dolor sit amet</blink>", "html");
+
If the clipboard contains HTML content, open it in a new tab.
- let clipboard = require("clipboard");
+ var clipboard = require("clipboard");
if (clipboard.currentFlavors.indexOf("html") != -1)
require("tabs").open("data:text/html," + clipboard.get("html"));
+Set the clipboard contents to an image.
+
+ var clipboard = require("clipboard");
+ clipboard.set("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA" +
+ "AABzenr0AAAASUlEQVRYhe3O0QkAIAwD0eyqe3Q993AQ3cBSUKpygfsNTy" +
+ "N5ugbQpK0BAADgP0BRDWXWlwEAAAAAgPsA3rzDaAAAAHgPcGrpgAnzQ2FG" +
+ "bWRR9AAAAABJRU5ErkJggg%3D%3D");
+
+If the clipboard contains an image, open it in a new tab.
+
+ var clipboard = require("clipboard");
+ if (clipboard.currentFlavors.indexOf("image") != -1)
+ require("tabs").open(clipboard.get());
+
+As noted before, data type can be easily omitted for images.
+
+If the intention is set the clipboard to a data URL as string and not as image,
+it can be easily done specifying a different flavor, like `text`.
+
+ var clipboard = require("clipboard");
+
+ clipboard.set("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA" +
+ "AABzenr0AAAASUlEQVRYhe3O0QkAIAwD0eyqe3Q993AQ3cBSUKpygfsNTy" +
+ "N5ugbQpK0BAADgP0BRDWXWlwEAAAAAgPsA3rzDaAAAAHgPcGrpgAnzQ2FG" +
+ "bWRR9AAAAABJRU5ErkJggg%3D%3D", "text");
+
<api name="set">
@function
Replace the contents of the user's clipboard with the provided data.
View
129 packages/addon-kit/lib/clipboard.js
@@ -6,22 +6,23 @@
"use strict";
-const {Cc,Ci} = require("chrome");
+const { Cc, Ci } = require("chrome");
+const { DataURL } = require("api-utils/url");
const errors = require("api-utils/errors");
const apiUtils = require("api-utils/api-utils");
-
/*
While these data flavors resemble Internet media types, they do
no directly map to them.
*/
const kAllowableFlavors = [
"text/unicode",
- "text/html"
+ "text/html",
+ "image/png"
/* CURRENTLY UNSUPPORTED FLAVORS
"text/plain",
- "image/png",
"image/jpg",
- "image/gif"
+ "image/jpeg",
+ "image/gif",
"text/x-moz-text-internal",
"AOLMAIL",
"application/x-moz-file",
@@ -45,9 +46,8 @@ Jetpack API druid.
*/
const kFlavorMap = [
{ short: "text", long: "text/unicode" },
- { short: "html", long: "text/html" }
- // Images are currently unsupported.
- //{ short: "image", long: "image/png" },
+ { short: "html", long: "text/html" },
+ { short: "image", long: "image/png" }
];
let clipboardService = Cc["@mozilla.org/widget/clipboard;1"].
@@ -56,12 +56,30 @@ let clipboardService = Cc["@mozilla.org/widget/clipboard;1"].
let clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].
getService(Ci.nsIClipboardHelper);
+let imageTools = Cc["@mozilla.org/image/tools;1"].
+ getService(Ci.imgITools);
exports.set = function(aData, aDataType) {
+
let options = {
data: aData,
datatype: aDataType || "text"
};
+
+ // If `aDataType` is not given or if it's "image", the data is parsed as
+ // data URL to detect a better datatype
+ if (!aDataType || aDataType === "image") {
+ try {
+ let dataURL = new DataURL(aData);
+
+ options.datatype = dataURL.mimeType;
+ options.data = dataURL.data;
+ }
+ catch (e if e.name === "URIError") {
+ // Not a valid Data URL
@ochameau Mozilla member
ochameau added a note

Shouldn't we throw here, expect when aDataType is null?

@ZER0
ZER0 added a note

No, this is just an attempt to detect the data type if omitted – or if "image" is given. If we are not to able to auto detect the datatype, we proceed with the defaults and the information we have.

@ochameau Mozilla member
ochameau added a note

Let me rephrase it with an example. If I do clipboard.set("data:buggy-uri", "image"), I could receive this meaningfull URIError exception instead of a random exception in following code. I think that this code is going to throw Unable to decode data given in a valid image..

It's expected to ignore the exception when the data type isn't specified, but I don't think it is valuable to ignore it when we explicitely pass "image".

@ZER0
ZER0 added a note

But that's the point, your example is perfectly valid. If you pass an image's data that you can't decode in a valid image, you will get an exception. So there is no error about that. I'm not sure I'm following you. In the way the code is now, we can have:

clipboard.set("data:text/html,<b>foo</b>");

as well, that is consistent now that we can pass a data URL, that is equivalent to have:

clipboard.set("<b>foo</b>", "html");

Of course if I have something like:

cliboard.set("data:buggy-uri");

It will ends to set that data as plain text.

I do not see the advantage to throw an exception for images at this point, if it's throw in any case, and give to us the ability to detect the data type in general.
Of course I could throw the exception only if aDataType is equals to "image", but I don't see the point to add another check here and a new exception if this scenario is in any case trapped later.

@ochameau Mozilla member
ochameau added a note

If you are sure that a meaningfull exception is thrown on clipboard.set("data:buggy-uri", "image"), then that's fine.
It is just a weak assumption that some code will later throw a nice error message whereas we know that it is going to fail right here.

@ZER0
ZER0 added a note

I think that "Unable to decode data given in a valid image." is quite understandable for clipboard.set("data:buggy-uri", "image"). It's not a weak assumption that some code will later throw a exception later, I design the code explicitly in that way in order to avoid to add unnecessary complexity.

@ochameau Mozilla member

It is ok, but less explicit than URIError: Malformed Data URL: data:buggy-uri. I let you judge your own code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ }
+ }
+
options = apiUtils.validateOptions(options, {
data: {
is: ["string"]
@@ -71,10 +89,10 @@ exports.set = function(aData, aDataType) {
}
});
- var flavor = fromJetpackFlavor(options.datatype);
+ let flavor = fromJetpackFlavor(options.datatype);
if (!flavor)
- throw new Error("Invalid flavor");
+ throw new Error("Invalid flavor for " + options.datatype);
// Additional checks for using the simple case
if (flavor == "text/unicode") {
@@ -87,7 +105,7 @@ exports.set = function(aData, aDataType) {
var xferable = Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
if (!xferable)
- throw new Error("Couldn't set the clipboard due to an internal error " +
+ throw new Error("Couldn't set the clipboard due to an internal error " +
"(couldn't create a Transferable object).");
switch (flavor) {
@@ -114,7 +132,40 @@ exports.set = function(aData, aDataType) {
xferable.setTransferData("text/unicode", str, str.data.length * 2);
}
break;
- // TODO: images!
+
+ // Set images to the clipboard is not straightforward, to have an idea how
+ // it works on platform side, see:
+ // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530
+ case "image/png":
+ let image = options.data;
+
+ let container = {};
+
+ try {
+ let input = Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(Ci.nsIStringInputStream);
+
+ input.setData(image, image.length);
+
+ imageTools.decodeImageData(input, flavor, container);
+ }
+ catch (e) {
+ throw new Error("Unable to decode data given in a valid image.");
+ }
+
+ // Store directly the input stream makes the cliboard's data available
+ // for Firefox but not to the others application or to the OS. Therefore,
+ // a `nsISupportsInterfacePointer` object that reference an `imgIContainer`
+ // with the image is needed.
@ochameau Mozilla member
ochameau added a note

It might be usefull to add some links about this.
Like platform code that highlight these interfaces:
http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530

@ZER0
ZER0 added a note

That doesn't really explain why a nsISupportsInterfacePointer is needed to makes the clipboard's data available for the others application. It is something I figured out. Or do you mean I should add something like "To understand the copy action on platform side, see: http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530" ?

@ochameau Mozilla member

Actually, as there is not documentation on this, C++ code can be considered as a usefull resource to explain why we are doing this.
To me, this piece of code tells that we have to use nsISupportsInterfacePointer in setTransferData.
This C++ code is a pure equivalent of your JS code.
You could have figured this out by reading it, but the hard part is about how to find such piece of code :p
(I found it by searching for setTransferData usages)

@ZER0
ZER0 added a note

You can't figure out reading the C++ code what I explained in the comment. I didn't see anything in the C++ code that said that if you "Store directly the input stream makes the cliboard's data available for Firefox but not to the others application or to the OS".
Actually, if you never doing that by yourself but just "copied" the C++ you will never figure it out.
And that's why I didn't see the value to add this link to "explain why": the link didn't explain anything about the comment I wrote.

@ochameau Mozilla member

Oh what's going on there. I'm trying to help you looking for information and give additional material to document your code.
Don't be too picky about review comments. I want to improve comments about this method, not just one sentence in this comment. The C++ code does exactly what you describe in your second sentence Therefore, ansISupportsInterfacePointerobject that reference animgIContainerwith the image is needed., and what you code does.

@ZER0
ZER0 added a note

Sorry if there is some misunderstanding, but I'm not picky. I just think that put that link "as is" in the comment is wrong.

As you said, the code I wrote and C++ code doing the same. The difference is that in the C++ code there is no explanation about what's happening – why that process is needed – where in my code I explicitly said that.
The C++ code use a nsISupportsInterfacePointer but it doesn't explain why. So, to me, is totally useless: it's basically the same code, as you said, but looking at it you can't figure out why these steps are needed.

So, to me it's wrong put this link in this comment as is because if I read that comment I would expect that the link will bring to a code that explain what is described above, but it doesn't.

Notice that I'm not against to put that link, I just think that put the link in that context is misleading. As I suggested the link could be added in a different context – same comment, but with a sentence that define the link contents better, something like "To understand the copy action on platform side, see: http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530".

Or maybe put that link before line 136. What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ var imgPtr = Cc["@mozilla.org/supports-interface-pointer;1"].
+ createInstance(Ci.nsISupportsInterfacePointer);
+
+ imgPtr.data = container.value;
+
+ xferable.addDataFlavor(flavor);
+ xferable.setTransferData(flavor, imgPtr, -1);
+
+ break;
default:
throw new Error("Unable to handle the flavor " + flavor + ".");
}
@@ -135,8 +186,17 @@ exports.set = function(aData, aDataType) {
exports.get = function(aDataType) {
let options = {
- datatype: aDataType || "text"
+ datatype: aDataType
};
+
+ // Figure out the best data type for the clipboard's data, if omitted
+ if (!aDataType) {
+ if (~currentFlavors().indexOf("image"))
+ options.datatype = "image";
+ else
+ options.datatype = "text";
+ }
+
options = apiUtils.validateOptions(options, {
datatype: {
is: ["string"]
@@ -146,7 +206,7 @@ exports.get = function(aDataType) {
var xferable = Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
if (!xferable)
- throw new Error("Couldn't set the clipboard due to an internal error " +
+ throw new Error("Couldn't set the clipboard due to an internal error " +
"(couldn't create a Transferable object).");
var flavor = fromJetpackFlavor(options.datatype);
@@ -154,12 +214,11 @@ exports.get = function(aDataType) {
// Ensure that the user hasn't requested a flavor that we don't support.
if (!flavor)
throw new Error("Getting the clipboard with the flavor '" + flavor +
- "' is > not supported.");
+ "' is not supported.");
// TODO: Check for matching flavor first? Probably not worth it.
xferable.addDataFlavor(flavor);
-
// Get the data into our transferable.
clipboardService.getData(
xferable,
@@ -185,6 +244,38 @@ exports.get = function(aDataType) {
case "text/html":
data = data.value.QueryInterface(Ci.nsISupportsString).data;
break;
+ case "image/png":
+ let dataURL = new DataURL();
+
+ dataURL.mimeType = flavor;
+ dataURL.base64 = true;
+
+ let image = data.value;
+
+ // Due to the differences in how images could be stored in the clipboard
+ // the checks below are needed. The clipboard could already provide the
+ // image as byte streams, but also as pointer, or as image container.
+ // If it's not possible obtain a byte stream, the function returns `null`.
+ if (image instanceof Ci.nsISupportsInterfacePointer)
+ image = image.data;
+
+ if (image instanceof Ci.imgIContainer)
+ image = imageTools.encodeImage(image, flavor);
+
+ if (image instanceof Ci.nsIInputStream) {
+ let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].
+ createInstance(Ci.nsIBinaryInputStream);
+
+ binaryStream.setInputStream(image);
+
+ dataURL.data = binaryStream.readBytes(binaryStream.available());
+
+ data = dataURL.toString();
+ }
+ else
+ data = null;
+
+ break;
default:
data = null;
}
@@ -192,7 +283,7 @@ exports.get = function(aDataType) {
return data;
};
-Object.defineProperty(exports, "currentFlavors", { get: function() {
+function currentFlavors() {
// Loop over kAllowableFlavors, calling hasDataMatchingFlavors for each.
// This doesn't seem like the most efficient way, but we can't get
// confirmation for specific flavors any other way. This is supposed to be
@@ -208,7 +299,9 @@ Object.defineProperty(exports, "currentFlavors", { get: function() {
currentFlavors.push(toJetpackFlavor(flavor));
}
return currentFlavors;
-}});
+};
+
+Object.defineProperty(exports, "currentFlavors", { get : currentFlavors });
// SUPPORT FUNCTIONS ////////////////////////////////////////////////////////
View
150 packages/addon-kit/tests/test-clipboard.js
@@ -2,6 +2,103 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Cc, Ci } = require("chrome");
+
+const imageTools = Cc["@mozilla.org/image/tools;1"].
+ getService(Ci.imgITools);
+
+const io = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+
+const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA" +
+ "AABzenr0AAAASUlEQVRYhe3O0QkAIAwD0eyqe3Q993AQ3cBSUKpygfsNTy" +
+ "N5ugbQpK0BAADgP0BRDWXWlwEAAAAAgPsA3rzDaAAAAHgPcGrpgAnzQ2FG" +
+ "bWRR9AAAAABJRU5ErkJggg%3D%3D";
+
+const base64jpeg = "data:image/jpeg;base64,%2F9j%2F4AAQSkZJRgABAQAAAQABAAD%2F" +
+ "2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCg" +
+ "sOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD%2F2wBDAQMDAwQDBAgEBAgQCw" +
+ "kLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ" +
+ "EBAQEBAQEBD%2FwAARCAAgACADAREAAhEBAxEB%2F8QAHwAAAQUBAQEBAQ" +
+ "EAAAAAAAAAAAECAwQFBgcICQoL%2F8QAtRAAAgEDAwIEAwUFBAQAAAF9AQ" +
+ "IDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRol" +
+ "JicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eX" +
+ "qDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJ" +
+ "ytLT1NXW19jZ2uHi4%2BTl5ufo6erx8vP09fb3%2BPn6%2F8QAHwEAAwEB" +
+ "AQEBAQEBAQAAAAAAAAECAwQFBgcICQoL%2F8QAtREAAgECBAQDBAcFBAQA" +
+ "AQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNO" +
+ "El8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0" +
+ "dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6ws" +
+ "PExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3%2BPn6%2F9oADAMB" +
+ "AAIRAxEAPwD5Kr8kP9CwoA5f4m%2F8iRqX%2FbH%2FANHJXr5F%2FwAjCn" +
+ "8%2F%2FSWfnnir%2FwAkji%2F%2B4f8A6dgeD1%2BiH8bn1BX5If6FmFqW" +
+ "pXtveyQwzbUXGBtB7D2r9l4U4UyjMsoo4rFUeacua75pLaUktFJLZH5NxN" +
+ "xNmmX5pVw2Gq8sI8tlyxe8U3q03uzD8S3dxqOi3NneSeZDJs3LgDOHBHI5" +
+ "6gV%2BkcG%2BH%2FDmJzuhSq4e8XzfbqfyS%2FvH5rx1xTm2MyDEUa1W8X" +
+ "yXXLFbTi%2BkThv7B0r%2FAJ9f%2FH2%2Fxr90%2FwCIVcI%2F9An%2FAJ" +
+ "Uq%2FwDyZ%2FO%2F16v%2FADfgv8j0r%2FhZvgj%2FAKDf%2FktN%2FwDE" +
+ "V%2Fnr%2FYWYf8%2B%2Fxj%2Fmf3R%2FxFXhH%2FoL%2FwDKdX%2F5Azrv" +
+ "xLouo3D3lne%2BZDJja3luM4GDwRnqDX9LeH%2FBud4nhzD1aVC8Xz%2Fa" +
+ "h%2Fz8l%2FePx%2FinjrIMZm1WtRxF4vls%2BSa2jFdYlDUdRsp7OSKKbc" +
+ "7YwNpHce1fqfCvCub5bm9HFYqjywjzXfNF7xklopN7s%2BC4l4lyvMMrq4" +
+ "fD1bzfLZcsltJPqktkYlfsZ%2BUnBV%2FnufVnXaD%2FAMgqD%2FgX%2Fo" +
+ "Rr%2BxvCr%2FkkcJ%2F3E%2F8ATsz5%2FHfx5fL8kX6%2FQjkCgD%2F%2F" +
+ "2Q%3D%3D";
+
+const canvasHTML = "data:text/html," + encodeURIComponent(
+ "<html>\
+ <body>\
+ <canvas width='32' height='32'></canvas>\
+ </body>\
+ </html>"
+);
+
+function comparePixelImages(imageA, imageB, callback) {
+ let tabs = require("tabs");
+
+ tabs.open({
+ url: canvasHTML,
+
+ onReady: function onReady(tab) {
+ let worker = tab.attach({
+ contentScript: "new " + function() {
+ let canvas = document.querySelector("canvas");
+ let context = canvas.getContext("2d");
+
+ self.port.on("draw-image", function(imageURI) {
+ let img = new Image();
+
+ img.onload = function() {
+ context.drawImage(this, 0, 0);
+
+ let pixels = Array.join(context.getImageData(0, 0, 32, 32).data);
+ self.port.emit("image-pixels", pixels);
+ }
+
+ img.src = imageURI;
+ });
+ }
+ });
+
+ let compared = "";
+
+ worker.port.on("image-pixels", function (pixels) {
+ if (!compared) {
+ compared = pixels;
+ this.emit("draw-image", imageB);
+ } else {
+ callback(compared === pixels);
+ tab.close()
+ }
+ });
+
+ worker.port.emit("draw-image", imageA);
+ }
+ });
+}
+
// Test the typical use case, setting & getting with no flavors specified
exports.testWithNoFlavor = function(test) {
@@ -61,4 +158,57 @@ exports.testNotInFlavor = function(test) {
// If there's nothing on the clipboard with this flavor, should return null
test.assertEqual(clip.get(flavor), null);
};
+
+exports.testSetImage = function(test) {
+ var clip = require("clipboard");
+ var flavor = "image";
+ var fullFlavor = "image/png";
+
+ test.assert(clip.set(base64png, flavor), "clipboard set");
+ test.assertEqual(clip.currentFlavors[0], flavor, "flavor is set");
+};
+
+exports.testGetImage = function(test) {
+ test.waitUntilDone();
+
+ var clip = require("clipboard");
+
+ clip.set(base64png, "image");
+
+ var contents = clip.get();
+
+ comparePixelImages(base64png, contents, function (areEquals) {
+ test.assert(areEquals,
+ "Image gets from clipboard equals to image sets to the clipboard");
+
+ test.done();
+ });
+}
+
+exports.testSetImageTypeNotSupported = function(test) {
+ var clip = require("clipboard");
+ var flavor = "image";
+
+ test.assertRaises(function () {
+ clip.set(base64jpeg, flavor);
+ }, "Invalid flavor for image/jpeg");
+
+};
+
+// Notice that `imageTools.decodeImageData`, used by `clipboard.set` method for
+// images, write directly to the javascript console the error in case the image
+// is corrupt, even if the error is catched.
+//
+// See: http://mxr.mozilla.org/mozilla-central/source/image/src/Decoder.cpp#136
+exports.testSetImageTypeWrongData = function(test) {
+ var clip = require("clipboard");
+ var flavor = "image";
+
+ var wrongPNG = "data:image/png" + base64jpeg.substr(15);
+
+ test.assertRaises(function () {
+ clip.set(wrongPNG, flavor);
+ }, "Unable to decode data given in a valid image.");
+};
+
// TODO: Test error cases.
View
50 packages/api-utils/docs/base64.md
@@ -0,0 +1,50 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+The module provides data encoding and decoding using Base64 algorithms.
+
+##Example
+
+ var base64 = require("api-utils/base64");
+
+ var encodedData = base64.encode("Hello, World");
+ var decodedData = base64.decode(encodedData);
+
+##Unicode Strings
+
+In order to `encode` and `decode` properly Unicode strings, the `charset`
+parameter needs to be set to `"utf-8"`:
+
+ var base64 = require("api-utils/base64");
+
+ var encodedData = base64.encode(unicodeString, "utf-8");
+ var decodedData = base64.decode(encodedData, "utf-8");
+
+<api name="encode">
+@function
+ Creates a base-64 encoded ASCII string from a string of binary data.
+
+@param data {string}
+ The data to encode
+@param [charset] {string}
+ The charset of the string to encode (optional).
+ The only accepted value is `"utf-8"`.
+
+@returns {string}
+ The encoded string
+</api>
+
+<api name="decode">
+@function
+ Decodes a string of data which has been encoded using base-64 encoding.
+
+@param data {string}
+ The encoded data
+@param [charset] {string}
+ The charset of the string to encode (optional).
+ The only accepted value is `"utf-8"`.
+
+@returns {string}
+ The decoded string
+</api>
View
42 packages/api-utils/docs/url.md
@@ -83,3 +83,45 @@ The `url` module provides functionality for the parsing and retrieving of URLs.
The converted URL as a string.
</api>
+<api name="DataURL">
+@class
+<api name="DataURL">
+@constructor
+ The DataURL constructor creates an object that represents a data: URL,
+ verifying that the provided string is a valid data: URL in the process.
+
+@param uri {string}
+ A string to be parsed as Data URL. If is not a valid URI, this constructor
+ will throw an exception.
+</api>
+
+<api name="mimeType">
+@property {string}
+ The MIME type of the data. By default is an empty string.
+</api>
+
+<api name="parameters">
+@property {object}
+ An hashmap that contains the parameters of the Data URL. By default is
+ an empty object.
+</api>
+
+<api name="base64">
+@property {boolean}
+ Defines the encoding for the value in `data` property.
+</api>
+
+<api name="data">
+@property {string}
+ The string that contains the data in the Data URL.
+ If the `uri` given to the constructor contains `base64` parameter, this string is decoded.
+</api>
+
+<api name="toString">
+@method
+ Returns a string representation of the Data URL.
+ If `base64` is `true`, the `data` property is encoded using base-64 encoding.
+@returns {string}
+ The URL as a string.
+</api>
+</api>
View
37 packages/api-utils/lib/base64.js
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Cu } = require("chrome");
+
+// If an object is not given as second argument, the JavaScript Module scope is
+// returned, so we can obtain from it the `atob` and `btoa` functions
+const { atob, btoa } = Cu.import("resource://gre/modules/Services.jsm");
+
+function isUTF8(charset) {
+ let type = typeof charset;
+
+ if (type === "undefined")
+ return false;
+
+ if (type === "string" && charset.toLowerCase() === "utf-8")
+ return true;
+
+ throw new Error("The charset argument can be only 'utf-8'");
+}
+
+exports.decode = function (data, charset) {
+ if (isUTF8(charset))
+ return decodeURIComponent(escape(atob(data)))
+
+ return atob(data);
+}
+
+exports.encode = function (data, charset) {
+ if (isUTF8(charset))
+ return btoa(unescape(encodeURIComponent(data)))
+
+ return btoa(data);
+}
View
121 packages/api-utils/lib/url.js
@@ -4,7 +4,10 @@
"use strict";
-const {Cc,Ci,Cr} = require("chrome");
+const { Cc, Ci, Cr } = require("chrome");
+
+const { Class } = require("./heritage");
+const base64 = require("./base64");
var ios = Cc['@mozilla.org/network/io-service;1']
.getService(Ci.nsIIOService);
@@ -111,3 +114,119 @@ function URL(url, base) {
URL.prototype = Object.create(String.prototype);
exports.URL = URL;
+
+/**
+ * Parse and serialize a Data URL.
+ *
+ * See: http://tools.ietf.org/html/rfc2397
+ *
+ * Note: Could be extended in the future to decode / encode automatically binary
+ * data.
+ */
+const DataURL = Class({
+
+ get base64 () {
+ return "base64" in this.parameters;
+ },
+
+ set base64 (value) {
+ if (value)
+ this.parameters["base64"] = "";
+ else
+ delete this.parameters["base64"];
+ },
+ /**
+ * Initialize the Data URL object. If a uri is given, it will be parsed.
+ *
+ * @param {String} [uri] The uri to parse
+ *
+ * @throws {URIError} if the Data URL is malformed
+ */
+ initialize: function(uri) {
+ // Due to bug 751834 it is not possible document and define these
+ // properties in the prototype.
+
+ /**
+ * An hashmap that contains the parameters of the Data URL. By default is
+ * empty, that accordingly to RFC is equivalent to {"charset" : "US-ASCII"}
+ */
+ this.parameters = {};
+
+ /**
+ * The MIME type of the data. By default is empty, that accordingly to RFC
+ * is equivalent to "text/plain"
+ */
+ this.mimeType = "";
+
+ /**
+ * The string that represent the data in the Data URL
+ */
+ this.data = "";
+
+ if (!uri)
+ return;
+
+ uri = String(uri);
+
+ let matches = uri.match(/^data:([^,]*),(.*)$/i);
+
+ if (!matches)
+ throw new URIError("Malformed Data URL: " + uri);
+
+ let mediaType = matches[1].trim();
+
+ this.data = decodeURIComponent(matches[2].trim());
+
+ if (!mediaType)
+ return;
+
+ let parametersList = mediaType.split(";");
+
+ this.mimeType = parametersList.shift().trim();
+
+ for (let parameter, i = 0; parameter = parametersList[i++];) {
+ let pairs = parameter.split("=");
+ let name = pairs[0].trim();
+ let value = pairs.length > 1 ? decodeURIComponent(pairs[1].trim()) : "";
+
+ this.parameters[name] = value;
+ }
+
+ if (this.base64)
+ this.data = base64.decode(this.data);
+
+ },
+
+ /**
+ * Returns the object as a valid Data URL string
+ *
+ * @returns {String} The Data URL
+ */
+ toString : function() {
+ let parametersList = [];
+
+ for (let name in this.parameters) {
+ let encodedParameter = encodeURIComponent(name);
+ let value = this.parameters[name];
+
+ if (value)
+ encodedParameter += "=" + encodeURIComponent(value);
+
+ parametersList.push(encodedParameter);
+ }
+
+ // If there is at least a parameter, add an empty string in order
+ // to start with a `;` on join call.
+ if (parametersList.length > 0)
+ parametersList.unshift("");
+
+ let data = this.base64 ? base64.encode(this.data) : this.data;
+
+ return "data:" +
+ this.mimeType +
+ parametersList.join(";") + "," +
+ encodeURIComponent(data);
+ }
+});
+
+exports.DataURL = DataURL;
View
23 packages/api-utils/lib/utils/data.js
@@ -5,10 +5,10 @@
"use strict";
const { Cc, Ci, Cu } = require("chrome");
+const base64 = require("../base64");
+
const IOService = Cc["@mozilla.org/network/io-service;1"].
getService(Ci.nsIIOService);
-const AppShellService = Cc["@mozilla.org/appshell/appShellService;1"].
- getService(Ci.nsIAppShellService);
const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm");
const FaviconService = Cc["@mozilla.org/browser/favicon-service;1"].
@@ -34,7 +34,7 @@ exports.getFaviconURIForLocation = function getFaviconURIForLocation(uri) {
catch(e) {
if (!DEF_FAVICON) {
DEF_FAVICON = PNG_B64 +
- base64Encode(getChromeURIContent(DEF_FAVICON_URI));
+ base64.encode(getChromeURIContent(DEF_FAVICON_URI));
}
return DEF_FAVICON;
}
@@ -49,7 +49,7 @@ function getChromeURIContent(chromeURI) {
let channel = IOService.newChannel(chromeURI, null, null);
let input = channel.open();
let stream = Cc["@mozilla.org/binaryinputstream;1"].
- createInstance(Ci.nsIBinaryInputStream);
+ createInstance(Ci.nsIBinaryInputStream);
stream.setInputStream(input);
let content = stream.readBytes(input.available());
stream.close();
@@ -61,11 +61,18 @@ exports.getChromeURIContent = getChromeURIContent;
/**
* Creates a base-64 encoded ASCII string from a string of binary data.
*/
-function base64Encode(data) AppShellService.hiddenDOMWindow.btoa(String(data));
-exports.base64Encode = base64Encode;
+exports.base64Encode = function base64Encode(data) {
+ console.warn('require("api-utils/utils/data").base64Encode is deprecated, ' +
+ 'please use require("api-utils/base64").encode instead');
+ return base64.encode(data);
+}
/**
* Decodes a string of data which has been encoded using base-64 encoding.
*/
-function base64Decode(data) AppShellService.hiddenDOMWindow.atob(String(data));
-exports.base64Decode = base64Decode;
+exports.base64Decode = function base64Decode(data) {
+ console.warn('require("api-utils/utils/data").base64Dencode is deprecated, ' +
+ 'please use require("api-utils/base64").decode instead');
+
+ return base64.decode(data);
+}
View
75 packages/api-utils/tests/test-base64.js
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const base64 = require("api-utils/base64");
+
+const text = "Awesome!";
+const b64text = "QXdlc29tZSE=";
+
+const utf8text = "✓ à la mode";
+const b64utf8text = "4pyTIMOgIGxhIG1vZGU=";
+
+exports["test base64.encode"] = function (assert) {
+ assert.equal(base64.encode(text), b64text, "encode correctly")
+}
+
+exports["test base64.decode"] = function (assert) {
+ assert.equal(base64.decode(b64text), text, "decode correctly")
+}
+
+exports["test base64.encode Unicode"] = function (assert) {
+
+ assert.equal(base64.encode(utf8text, "utf-8"), b64utf8text,
+ "encode correctly Unicode strings.")
+}
+
+exports["test base64.decode Unicode"] = function (assert) {
+
+ assert.equal(base64.decode(b64utf8text, "utf-8"), utf8text,
+ "decode correctly Unicode strings.")
+}
+
+exports["test base64.encode with wrong charset"] = function (assert) {
+
+ assert.throws(function() {
+ base64.encode(utf8text, "utf-16");
+ }, "The charset argument can be only 'utf-8'");
+
+ assert.throws(function() {
+ base64.encode(utf8text, "");
+ }, "The charset argument can be only 'utf-8'");
+
+ assert.throws(function() {
+ base64.encode(utf8text, 8);
+ }, "The charset argument can be only 'utf-8'");
+
+}
+
+exports["test base64.decode with wrong charset"] = function (assert) {
+
+ assert.throws(function() {
+ base64.decode(utf8text, "utf-16");
+ }, "The charset argument can be only 'utf-8'");
+
+ assert.throws(function() {
+ base64.decode(utf8text, "");
+ }, "The charset argument can be only 'utf-8'");
+
+ assert.throws(function() {
+ base64.decode(utf8text, 8);
+ }, "The charset argument can be only 'utf-8'");
+
+}
+
+exports["test encode/decode Unicode without utf-8 as charset"] = function (assert) {
+
+ assert.notEqual(base64.decode(base64.encode(utf8text)), utf8text,
+ "Unicode strings needs 'utf-8' charset"
+ );
+
+}
+
+require("test").run(exports);
View
59 packages/api-utils/tests/test-url.js
@@ -147,11 +147,11 @@ exports.testURL = function(test) {
"h:foo",
"toString should roundtrip");
// test relative + base
- test.assertEqual(URL("mypath", "http://foo").toString(),
+ test.assertEqual(URL("mypath", "http://foo").toString(),
"http://foo/mypath",
"relative URL resolved to base");
// test relative + no base
- test.assertRaises(function() URL("path").toString(),
+ test.assertRaises(function() URL("path").toString(),
"malformed URI: path",
"no base for relative URI should throw");
@@ -198,3 +198,58 @@ exports.testStringInterface = function(test) {
test.assertEqual(a.trimRight(), EM.trimRight(), "trimRight on URL works.");
test.assertEqual(a.trimLeft(), EM.trimLeft(), "trimLeft on URL works.");
}
+
+exports.testDataURLwithouthURI = function (test) {
+ const { DataURL } = url;
+
+ let dataURL = new DataURL();
+
+ test.assertEqual(dataURL.base64, false, "base64 is false for empty uri")
+ test.assertEqual(dataURL.data, "", "data is an empty string for empty uri")
+ test.assertEqual(dataURL.mimeType, "", "mimeType is an empty string for empty uri")
+ test.assertEqual(Object.keys(dataURL.parameters).length, 0, "parameters is an empty object for empty uri");
+
+ test.assertEqual(dataURL.toString(), "data:,");
+}
+
+exports.testDataURLwithMalformedURI = function (test) {
+ const { DataURL } = url;
+
+ test.assertRaises(function() {
+ let dataURL = new DataURL("http://www.mozilla.com/");
+ },
+ "Malformed Data URL: http://www.mozilla.com/",
+ "DataURL raises an exception for malformed data uri"
+ );
+}
+
+exports.testDataURLparse = function (test) {
+ const { DataURL } = url;
+
+ let dataURL = new DataURL("data:text/html;charset=US-ASCII,%3Ch1%3EHello!%3C%2Fh1%3E");
+
+ test.assertEqual(dataURL.base64, false, "base64 is false for non base64 data uri")
+ test.assertEqual(dataURL.data, "<h1>Hello!</h1>", "data is properly decoded")
+ test.assertEqual(dataURL.mimeType, "text/html", "mimeType is set properly")
+ test.assertEqual(Object.keys(dataURL.parameters).length, 1, "one parameters specified");
+ test.assertEqual(dataURL.parameters["charset"], "US-ASCII", "charset parsed");
+
+ test.assertEqual(dataURL.toString(), "data:text/html;charset=US-ASCII,%3Ch1%3EHello!%3C%2Fh1%3E");
+}
+
+exports.testDataURLparseBase64 = function (test) {
+ const { DataURL } = url;
+ const { decode } = require("./base64");
+
+ let text = "Awesome!";
+ let b64text = "QXdlc29tZSE=";
+ let dataURL = new DataURL("data:text/plain;base64," + b64text);
+
+ test.assertEqual(dataURL.base64, true, "base64 is true for base64 encoded data uri")
+ test.assertEqual(dataURL.data, text, "data is properly decoded")
+ test.assertEqual(dataURL.mimeType, "text/plain", "mimeType is set properly")
+ test.assertEqual(Object.keys(dataURL.parameters).length, 1, "one parameters specified");
+ test.assertEqual(dataURL.parameters["base64"], "", "parameter set without value");
+
+ test.assertEqual(dataURL.toString(), "data:text/plain;base64," + encodeURIComponent(b64text));
+}
Something went wrong with that request. Please try again.