前回のエントリーの後日談をライブ間たっぷりに書いていくぜ! 誤字脱字の修正を除いて、書きっぱなしの推敲なしで最後まで。
前回からこれまでの流れ
なんだか次のURLを参照すれば良いのではとWassrで聞いた。
function load_binary_resource(url) {
var req = new XMLHttpRequest();
req.open('GET', url, false);
//XHR binary charset opt by Marcus Granado 2006 [http://mgran.blogspot.com]
req.overrideMimeType('text/plain; charset=x-user-defined');
req.send(null);
if (req.status != 200) return '';
return req.responseText;
}
前回のエントリーの最後の方で書いた通り、Content-TypeやAccept-Charsetの指定で解決するのではと考えていたので、次のようにすればいいと思い込んでしまった。
GM_xmlhttpRequest(
{
"method":"GET",
"url":url,
"onload":callbackForSuccess,
"onerror":callbackForError,
"headers":{
"Content-Type":"text/plain",
"Accept-Charset":"x-user-defined"
"If-Unmodified-Since":lastModified,
"Range":"bytes="+from+"-"+To
}
}
);
結果はやっぱり文字化けした。でも、そのあとでblogへのコメントとはてなブックマークのコメントに同じような指摘があった。「あれ?うまくいかなかったのになぁ...」と思いつつもう一回MDCを確認した。あああ、もしかして「overrideMimeType」ってメソッドが重要ってこと?
じゃあ調べてみよう
MDCのサンプルを見ると、「overrideMimeType」はXMLHttpRequestのメソッドとして準備されている。前回はGreasemonkeyで実装しようとしたので、GM_xmlhttpRequestにI/Fが準備されていないか調べてみる。
GM_xmlhttpRequest(
{
"method":"GET",
"url":data.url,
"onload":callback,
"onerror":callback,
"overrideMimeType":"text/plain; charset=x-user-defined",
"headers":{
"If-Unmodified-Since":data.lastModified,
"Range":"bytes="+range+"-"+range
}
}
);
GM_xmlhttpRequest(
{
"method":"HEAD",
"url":data.url,
"onload":loadHeaderMethod,
"onerror":loadHeaderMethod,
"overrideMimeType":"text/plain; charset=x-user-defined"
}
);
ダメだった。何でだろう。調査中。GM_xmlhttpRequestの書き方がまずいのかな。
22:07
しまったなぁ。ハードル上げ過ぎた。戦略練り直す。ここから更新時間を入れることにする。
22:54
う〜〜〜ん。無理。中断。再開は今日かもしれないし明日かもしれない。ちょっと頭冷やす。
23:43
再開。単純なcodeから原因を突き止めて行くことにする。
// ==UserScript==
// @name getFileByDataScheme
// @namespace http://www.kanasansoft.com/
// @include *
// ==/UserScript==
(
function(){
var base64=function(original){
var table=(
function(s){
var len=s.length;
var hash={};
for(var i=0;i<len;i++){
var bit=i.toString(2);
var bit="000000".slice(0,-bit.length%6)+bit;
hash[bit]=s.charAt(i);
}
return hash;
}
)("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
var len=original.length;
var bits8=[];
if(original instanceof Array){
for(var i=0;i<len;i++){
var bit=original[i].charCodeAt(0).toString(2);
bit="00000000".slice(0,-bit.length%8)+bit;
Array.prototype.push.apply(bits8,bit.match(/.{8}/g));
}
}else{
for(var i=0;i<len;i++){
var bit=original.charCodeAt(i).toString(2);
bit="00000000".slice(0,-bit.length%8)+bit;
Array.prototype.push.apply(bits8,bit.match(/.{8}/g));
}
}
var len=bits8.length;
var bits6=[];
for(var i=0;i<len;i+=3){
var bit=bits8.slice(i,i+3).join("");
bit+="000000".slice(0,-bit.length%6);
Array.prototype.push.apply(bits6,bit.match(/.{6}/g));
}
var len=bits6.length;
var base64=[];
for(var i=0;i<len;i++){
base64.push(table[bits6[i]]);
}
Array.prototype.push.apply(base64,["=","=","=","="].slice(0,-base64.length%4));
return base64.join("");
}
var parseHTTPHeader=function(responseHeader){
var headers=responseHeader.split("\n");
var len=headers.length;
var parsing=[];
for(var i=0;i<len;i++){
if(/^$/.test(headers[i])){
}else if(/^[\x09\x20]/.test(headers[i])){
if(parsing.length==0){
throw "SyntaxError:HTTPHeader (first line) "+headers[i];
}
parsing[parsing.length-1]+="\n"+headers[i];
}else{
parsing.push(headers[i]);
}
}
var len=parsing.length;
var parsed={};
for(var i=0;i<len;i++){
var pair=parsing[i].split(": ",2);
if(pair.length!=2){
throw "SyntaxError:HTTPHeader (format) "+parsing[i];
}
if(pair[0] in parsed){
throw "SyntaxError:HTTPHeader (repetition) "+pair[0];
}
parsed[pair[0]]=pair[1];
}
return parsed;
}
var loadDataMethod=function(res){
var headers=parseHTTPHeader(res.responseHeaders);
if(res.status!=200){
throw "RequestError"+
":status="+res.status+
":statusText="+res.statusText+
":responseHeaders="+res.responseHeaders;
}
var contentType=("Content-Type" in headers)?
headers["Content-Type"]:
"application/octet−stream";
var dataScheme="data:"+contentType+
";base64,"+base64(res.responseText);
data.callback(dataScheme);
}
var getData=function(url){
data.url=url;
GM_xmlhttpRequest(
{
"method":"GET",
"url":data.url,
"onload":loadDataMethod,
"onerror":loadDataMethod,
"overrideMimeType":"text/plain; charset=x-user-defined"
}
);
}
var data={};
var getFileByDataScheme=function(url,callback){
data.callback=callback;
getData(url);
}
if(window.self==window.top){
var url=prompt("image url","");
if(url==null||url==""){
return;
}
var handler=function(url,callback){
return function(){
getFileByDataScheme(url,callback);
}
}
var callback=function(dataScheme){
var elem=document.createElement("img");
elem.src=dataScheme;
document.body.appendChild(elem);
}
window.addEventListener(
"load",
handler(url,callback),
false
);
}
}
)();
結果
//グリモンで出力されたDataScheme
data:image/png;base64,94lQTkcNChoKAAAADUlIRFIAAAAQAAAAEAIDAAAAYvedF/fyAAAAAXNSR0IA9673zhz36QAAAAxQTFRFAC1n98z3zPf/AAAA95n3mff/99VVX/f6AAAAAXRSTlMAQPfm99hmAAAAAWJLR0QA94gFHUgAAAAJcEhZcwAACxMAAAsTAQD3mvecGAAAAAd0SU1FB/fYAwwPCTX3s/fi98n33QAAAENJREFUCPfXY2BgamBgYPfgTAAS96oP94D3xHQg95dpHveQ9+B+CRT3434aAffkXg0DSkwNBQr3qveGAgU5QQQT94j3yzQNKPfB9/0CRPeA9/QC940CAFYZDvfC99z3r3b31QAAAABJRU5E965CYPeC
//[http://software.hixie.ch/utilities/cgi/data/data]で変換
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAAAXNSR0IArs4c6QAAAAxQTFRFAC1nzMz%2FAAAAmZn%2F1VVf%2BgAAAAF0Uk5TAEDm2GYAAAABYktHRACIBR1IAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH2AMMDwk1s%2BLJ3QAAAENJREFUCNdjYGBqYGBg4EwAEqoPgMR0IJdpHpDgfgkU434aAeReDQNKTA0FCqqGAgU5QQQTiMs0DSjB%2FQJEgPQCjQIAVhkOwtyvdtUAAAAASUVORK5CYII%3D
似ても似つかない悲惨な結果に...。昨日のグリモンでは似たような結果だったのになんでだろう。画像を変えたからかな。また、変えてみるかなぁ。
00:22
上の画像で「console.log(res.responseText);」してみたら「�」が大量に出力されてた。やっぱりRangeで分割する必要があるのかな。
23:23
Twitterで色々教えてもらった。取得した文字列の上位bitを削らなければいけない、Base64変換の変わりにbtoa関数を使えばよい、GlitchMonkeyというグリモンが参照になるかも、とのこと。JavaScirptは文字列をUTF-16で扱うんだった。「text/plain; charset=x-user-defined」で取得した文字列は1byteだけども、client側で認識される時にはUTF-16になるなって2bytesとなってしまうってことだよね。0x00-0xFFの文字列が0x0000-0x00FFとなってしまうので上位bitを削除しないとbinaryとしてはおかしなことになると...。あと、btoa関数、動作がよくわからなかったのでBase64関数を新規に作ったんだけど...。btoaでいいのか...。native関数の方が速いのはもちろんわかっているんだけど、このままでやってみる。もし、正常に動作すれば古いブラウザでも動くはずなので二兎を追える。最後のGlitchMonkeyは画像を操作するuser scriptみたい。必要に応じて追っかけてみる。
04/15 00:01
認識間違ってた。GlitchMonkeyのsourceが無茶苦茶重要。
function base64encode(data) {
return btoa(data.replace(/[\u0100-\uffff]/g, function(c) {
return String.fromCharCode(c.charCodeAt(0) & 0xff);
}));
}
変更前
var dataScheme="data:"+contentType+
";base64,"+base64(res.responseText);
変更後
var resText=res.responseText.replace(
/[\u0100-\uffff]/g,
function(c){
return String.fromCharCode(c.charCodeAt(0)&0xff);
}
)
var dataScheme="data:"+contentType+
";base64,"+base64(resText);
00:16
うをぉおおおおお〜〜〜〜〜!!! 動いた!!! すげぇ!!! とりあえず動いたグリモンをはる。
// ==UserScript==
// @name getFileByDataScheme
// @namespace http://www.kanasansoft.com/
// @include *
// ==/UserScript==
(
function(){
var base64=function(original){
var table=(
function(s){
var len=s.length;
var hash={};
for(var i=0;i<len;i++){
var bit=i.toString(2);
var bit="000000".slice(0,-bit.length%6)+bit;
hash[bit]=s.charAt(i);
}
return hash;
}
)("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
var len=original.length;
var bits8=[];
if(original instanceof Array){
for(var i=0;i<len;i++){
var bit=original[i].charCodeAt(0).toString(2);
bit="00000000".slice(0,-bit.length%8)+bit;
Array.prototype.push.apply(bits8,bit.match(/.{8}/g));
}
}else{
for(var i=0;i<len;i++){
var bit=original.charCodeAt(i).toString(2);
bit="00000000".slice(0,-bit.length%8)+bit;
Array.prototype.push.apply(bits8,bit.match(/.{8}/g));
}
}
var len=bits8.length;
var bits6=[];
for(var i=0;i<len;i+=3){
var bit=bits8.slice(i,i+3).join("");
bit+="000000".slice(0,-bit.length%6);
Array.prototype.push.apply(bits6,bit.match(/.{6}/g));
}
var len=bits6.length;
var base64=[];
for(var i=0;i<len;i++){
base64.push(table[bits6[i]]);
}
Array.prototype.push.apply(base64,["=","=","=","="].slice(0,-base64.length%4));
return base64.join("");
}
var parseHTTPHeader=function(responseHeader){
var headers=responseHeader.split("\n");
var len=headers.length;
var parsing=[];
for(var i=0;i<len;i++){
if(/^$/.test(headers[i])){
}else if(/^[\x09\x20]/.test(headers[i])){
if(parsing.length==0){
throw "SyntaxError:HTTPHeader (first line) "+headers[i];
}
parsing[parsing.length-1]+="\n"+headers[i];
}else{
parsing.push(headers[i]);
}
}
var len=parsing.length;
var parsed={};
for(var i=0;i<len;i++){
var pair=parsing[i].split(": ",2);
if(pair.length!=2){
throw "SyntaxError:HTTPHeader (format) "+parsing[i];
}
if(pair[0] in parsed){
throw "SyntaxError:HTTPHeader (repetition) "+pair[0];
}
parsed[pair[0]]=pair[1];
}
return parsed;
}
var loadDataMethod=function(res){
var headers=parseHTTPHeader(res.responseHeaders);
if(res.status!=200){
throw "RequestError"+
":status="+res.status+
":statusText="+res.statusText+
":responseHeaders="+res.responseHeaders;
}
var contentType=("Content-Type" in headers)?
headers["Content-Type"]:
"application/octet−stream";
var resText=res.responseText.replace(
/[\u0100-\uffff]/g,
function(c){
return String.fromCharCode(c.charCodeAt(0)&0xff);
}
)
var dataScheme="data:"+contentType+
";base64,"+base64(resText);
data.callback(dataScheme);
}
var getData=function(url){
data.url=url;
GM_xmlhttpRequest(
{
"method":"GET",
"url":data.url,
"onload":loadDataMethod,
"onerror":loadDataMethod,
"overrideMimeType":"text/plain; charset=x-user-defined"
}
);
}
var data={};
var getFileByDataScheme=function(url,callback){
data.callback=callback;
getData(url);
}
if(window.self==window.top){
var url=prompt("image url","");
if(url==null||url==""){
return;
}
var handler=function(url,callback){
return function(){
getFileByDataScheme(url,callback);
}
}
var callback=function(dataScheme){
var elem=document.createElement("img");
elem.src=dataScheme;
document.body.appendChild(elem);
}
window.addEventListener(
"load",
handler(url,callback),
false
);
}
}
)();