WPSU (Web Page Screenshot Utility) をこれまで作ってきたが、だんだんブラウザの実装に近くなってきた。このまま進んでいくと時間がかかる割には出来の悪いブラウザしかできない気もしてきた。そもそも Webページのスクリーンショットを撮るのに普段使っているSafariやFireFoxとは別にわざわざ他のソフトを立ち上げるのは面倒じゃないか?面倒なものはいずれは使われなくなるだろう。開発面でも様々なページを表示できるブラウザを作るとなると単純に WebView を使うだけではだめで、さまざまな細かい点に気を配る必要ある(特にセキュリティ関連は手が抜けないので負担が高い)。結局スクリーンショットそのものよりもブラウザの実装に時間がかかってしまっているのが現状。
そんなわけでここで一旦立ち止まってアプリの方向性を考え直す事にした。
ただ独立したアプリが難しいとなると残りは既存ブラウザのプラグイン化しかない。
とりあえずはプラグインでそんなことが可能かを調べてみる。また同等の機能を持つソフトが無いか調べてみる(実際みかけたことがある)。
2009年10月1日木曜日
WPSU再考
2009年9月22日火曜日
WPSU(16) - WebKitで新規ウィンドウを開く(Documentベースアプリへ移行)
WPSU はシングルウィンドウのアプリとして作成しているため、新規ウィンドウを開くようなリンクを押しても反応しない。
Safari など普通のブラウザと同様にリンクが開くようにする。このあたりは自分で実装するよりは標準のフレームワークを使った方が楽にできる。今までのプロジェクトを一旦捨てて新しいプロジェクトへソースコードを載せ替える。
最初のテンプレートで document-based application を選択する。
後は旧プロジェクトから必要なソースコードをコピーしてプロジェクトへ加える。従来 WebController で行っていた処理は MyDocument へ載せ変えた。
その上で、リンクが押されたときの処理を追加する。
まず Interface Builder を使い、WebViewの UIDelegate を File's Owner(すなわち MyDocument)へ設定する。
次に -[WebUIDelegate webView:createWebViewWithRequest:] を MyDocumentに実装する。
MyDocument.m
- (WebView *)webView:(WebView *)sender createWebViewWithRequest:(NSURLRequest *)request
{
MyDocument* document = [[NSDocumentController sharedDocumentController] openUntitledDocumentOfType:@"DocumentType" display:YES];
[[[document webView] mainFrame] loadRequest:request];
return [document webView];
}
新規にドキュメントを作成し、その上に配置された WebView でリクエストを処理させる。
実行してみよう。

出た。
- - - -
JavaScriptを ON にするとエラーが出て強制終了してしまった。
どうも JSを使うブログパーツが原因で何か問題が起きているようだ。
2009年8月17日月曜日
WPSU(14) - WebKitでクッキーを自前でハンドリングする #4 NSHTTPCookieを調べる
#タイトルが長くなってきた
さてクッキーを自前でハンドリングするにしても用意されているクラスが使えればそれにこしたことが無い。Foudation.framework には幸いなことに NSHTTPCookie クラスが用意されている。
NSHTTPCookie Class Reference
これが使えそうかどうか調査してみよう。
リファレンスを眺めると3つのインスタンス作成メソッドが用意されていた。
このうち、+[NSHTTPCookie cookiesWithResponseHeaderFields:forURL:] を試してみる。
cookiesWithResponseHeaderFields:forURL:
デリゲートメソッドに仕掛ける。
サーバから受け取ったレスポンス内のヘッダ情報を渡してみた。
AppController.m
- (void)webView:(WebView *)sender
resource:(id)identifier
didReceiveResponse:(NSURLResponse *)response
fromDataSource:(WebDataSource *)dataSource
{
NSDictionary* headers = [(NSHTTPURLResponse*)response allHeaderFields];
NSURL* url = [response URL];
NSArray* cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers forURL:url];
NSLog(@"---------------");
NSLog(@"%@", url);
NSLog(@"%@", cookies);
}
実行してみよう。Googleへ行き、非ログイン状態で「ログイン」リンクを押してみた。

するとサーバからクッキーが送られてきた。先ほどのメソッドでうまく取り込めているようだ。
2009-08-17 21:14:58.569 WebPageScreenshotUtility[2160:10b] ---------------
2009-08-17 21:14:58.621 WebPageScreenshotUtility[2160:10b] https://www.google.com/accounts/Login?hl=ja&continue=http://www.google.co.jp/
2009-08-17 21:14:58.640 WebPageScreenshotUtility[2160:10b] (
<NSHTTPCookie
version:0
name:@"GoogleAccountsLocale_session"
value:@"ja"
expiresDate:@"(null)"
created:@"272175298.569016"
sessionOnly:TRUE
domain:@"www.google.com"
path:@"/accounts"
secure:FALSE
comment:@"(null)"
commentURL:@"(null)"
portList:(null)>,
<NSHTTPCookie
version:0
name:@"GALX"
value:@"fo7Y_pW13-M"
expiresDate:@"(null)"
created:@"272175298.569091"
sessionOnly:TRUE
domain:@"www.google.com"
path:@"/accounts"
secure:TRUE
comment:@"(null)"
commentURL:@"(null)"
portList:(null)>
)
※読みやすい様に改行を入れてある
- - - - -
おお、これは便利。ヘッダ内のCookieの解析はこれが使えそうだ。
2009年8月12日水曜日
WebKit を使ったアプリケーションはクッキーを共有する
WPSU を作っていて気になったことがある。
WPSU を使って Googleを開くとログインしたはずが無いのに、ログイン状態で表示される。
(右上にメールアドレスが記載)
普段 Safari でこのアカウントを使っているのだが、
もしやクッキーは WebKitを使ったアプリケーションで共有されている?
Safariを開きログイン状態であることを確認した後、試しに WPSUでログアウトしてみると Safariの方でもログアウト状態になっていた。おおなんてこった。これってまずくないのか?
WebKitにはクッキーを扱うクラスは見つからなかった。Foundation.framework を調べると NSHTTPCookieStorage というクラスが見つかった。キャッシュの時と同様 Foundation.framework で管理されているようだ。
ADC - NSHTTPCookieStorage Class Reference
Overview には、はっきりと "a singleton object (shared instance)" と書かれている。
サンプルを作って動作を見てみる。
サンプル:CookieStudy.zip
-[NSHTTPCookieStorage cookies]の結果をデバッガコンソールへ吐き出してみよう。
コードはこんな感じ。
@implementation AppController
- (void)awakeFromNib
{
NSHTTPCookieStorage* cookie_storage =
[NSHTTPCookieStorage sharedHTTPCookieStorage];
NSArray* cookies = [cookie_storage cookies];
NSLog(@"%@", cookies);
}
@end
実行するとでるわでるわ、すべてのクッキーが表示された。

表示内容はこんな感じ。
<NSHTTPCookie version:0 name:@"__utmz" value:@"1981754.1243.1.1.utmcsr=google.co.jp/"
expiresDate:@"2010-01-13 00:40:03 +0900"
created:@"2692303.4789"
sessionOnly:FALSE
domain:@".deep.csi.com"
path:@"/"
secure:FALSE
comment:@"(null)"
commentURL:@"(null)"
portList:(null)>
※一部値を改変
- - - -
悪意のある WebKitアプリケーションがクッキー情報を好きにできるってことか。
と思ったりもしたが、
この件に限らず悪意のあるプログラムを走らせたら何でもできるという点で特別問題視する事でもないか。
ただ異なるブラウザ間でログイン状態が共有されるのはあまり気持ちのよい状況ではないな。
2009年8月7日金曜日
WPSU(11) - UserAgent を変える(Safariを名乗る?)
WebKitで作った簡易ブラウザで Gmail へアクセスすると「Gmail には完全にサポートされたブラウザのご利用をおすすめします。」と表示され、ユーザインターフェイスが Safariで見るそれとは違った少ししょぼい?ものなる。
UserAgentで判断しているのだろうとあたりをつけて、まずは Safariと簡易ブラウザの UserAgentを調べてみた。
Safari 3.2.3
"Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_7; ja-jp)
AppleWebKit/525.28.3 (KHTML, like Gecko) Version/3.2.3 Safari/525.28.3"
WebKit簡易ブラウザ
"Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_7; ja-jp)
AppleWebKit/525.28.3 (KHTML, like Gecko)"
前半は同じだが Safari の場合は最後にバージョン番号とアプリ名/ビルド番号が追加されている。
WebKit のドキュメントには Spoofing という項目が設けられていて UserAgent についての話題が取り上げられている。ここで紹介されていたメソッドを使い試してみよう。
WebKit Objective-C Programming Guide - Spoofing
まずは - [WebView setCustomUserAgent:] から。
試しに適当な文字列を設定してみる。
[_web_view setCustomUserAgent:@"CustomUserAgent"];
するとサーバ側では UserAgentに指定文字列がそのまま出てきた。
"CustomUserAgent"
次に - [WebView setApplicationNameForUserAgent:] を試す。
[_web_view setApplicationNameForUserAgent:@"Sample"];
すると WebKit標準のUserAgentの最後に指定文字列をものが出てきた。
"Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_7; ja-jp)
AppleWebKit/525.28.3 (KHTML, like Gecko) Sample"
それでは setApplicationNameForUserAgent: を使い Safariを偽装してみよう。
コード:
[_web_view setApplicationNameForUserAgent:@"Version/3.2.3 Safari/525.28.3"];
サーバ側確認:
"Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_7; ja-jp)
AppleWebKit/525.28.3 (KHTML, like Gecko) Version/3.2.3 Safari/525.28.3"
Gmail へアクセスしてみる。

Safariと同じ画面が出た。Gmailでは「標準HTML画面」と呼ばれているもののようだ。
画面読み込み中に右下に下記のメッセージが表示される。

試しに Safariで「簡易HTML形式」を選んだところ、簡易ブラウザで見たのと同じ画面が表示された。

Gmailでは、UserAgentからブラウザ情報を得てそれを元に HTML5/CSS 3 の対応状況を判断し、画面の表示を変えているようだ。
なお簡易ブラウザでは Gmailの表示が完了した数秒後にクラッシュしてしまった。以下、ログ
Debugger() was called!
The Debugger has exited due to signal 2 (SIGINT).The Debugger has exited due to signal 2 (SIGINT).
これはなんだ?
2009年8月6日木曜日
WPSU(10) - キャプチャ画像にスクロールバーを含めない
- [WebFrameView setAllowsScrolling:] での制御はどうやってもうまく行かなかった。
仕方が無いので、スクロールバーが出ないようにビューの大きさを調整してみる。色々試行錯誤したところ 幅10px, 縦15px 大きくするとスクロールバーが出なくなった。
rect.size.width += 10.0;
rect.size.height += 15.0;
その結果、横はちょうど良い大きさだが、縦は若干(数px)余白ができるようになった。
今回の結果:

調整しない場合の結果:

スクロールバーの幅や高さによってこの値が決まると思われるので、スクロールバーの大きさが変わるとこの数値は変わる可能性がある。
が、機能実現にはこれで十分なので安易だがこれでいこう。
(一番良いのはスクロールバーの制御だが、これは地道に調べていく)。
2009年8月5日水曜日
WPSU(9) - WebDocumentViewにスクロールバーを表示しない
キャプチャした画像をよく見るとスクロールバーがついている。
これをなくすのに -[WebFrameView setAllowsScrolling:]を使う。
WebFrameView Class Reference
setAllowsScrolling:
するとキャプチャ画像からスクロールバーが消えた。
ただ何故かもとに戻らない。一旦スクロールバーを消すとその後に表示設定(YES)を行っても、スクロールバーが2度と表示されなくなってしまう。
むう。どうすればいいのか。
2009年8月2日日曜日
WPSU(7) - 入力されたURLページを表示する
テキストフィールドへ入力された URL を使い Webページを表示させる。表示のためのボタンを別途用意するのではなく、テキストフィールドで returnキーが押されたらページを表示するようにしよう。
returnキーを処理するためにまず NSTextFieldのサブクラスを用意する。
URLTextField.h
@class WebController;
@interface URLTextField : NSTextField {
IBOutlet WebController* _web_controller;
}
@end
InterfaceBuilder で既に置いてあった NSTextFieldのクラスを URLTextFieldへ置き換える。またアウトレットに WebControllerを接続しておく。

URL入力後に returnキーが押されると- [NSTextField textDidEndEditing:]が呼ばれるので、ここでページ読み込みのメソッドを呼出す。
URLTextField.m
@implementation URLTextField
- (void)textDidEndEditing:(NSNotification *)notification
{
[_web_controller loadPageWithURLString:[self stringValue]];
}@end
ページ呼出しはこんな感じ。まだエラー処理はない。
WebController.m
- (void)loadPageWithURLString:(NSString*)url_string
{
[[_web_view mainFrame] loadRequest:
[NSURLRequest requestWithURL:[NSURL URLWithString:url_string]]];
}
実行しよう。テキストフィールドへ Googleのページを入力し returnキーを押してみる。

出た。
2009年8月1日土曜日
WPSU(6) - ウィンドウを2つ重ねる(解説)
前回のコードの解説。
まず WebViewを載せる前面のウィンドウのクラスを定義する。WebWindowと名付けた。
WebWindow.h
@interface WebWindow : NSWindow {
}
@end
実装は初期化コードおよび key window になるための -[canBecomeKeyWindow] の実装。
WebWindow.m
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithContentRect:frame
styleMask:NSUtilityWindowMask
backing:NSBackingStoreBuffered
defer:NO];
if (self) {
[self setDisplaysWhenScreenProfileChanges:YES];
[self setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces];
}
return self;
}
- (BOOL)canBecomeKeyWindow
{
return YES;
}
Interface Builder ではベースとなる後ろのウィンドウだけを定義しておく。ここに戻るや進むのボタン、URLテキストフィールドなどを配置しておく。また WebWindowを配置するエリアを表すためにカスタムビューを配置しておく。あらかじめ Interface Builder で位置や大きさを設定しておくと、実行時に WebWindowの大きさや位置を簡単に決める事ができる。

WebViewは前面の WebWindowに載せるのでここには追加しない。ただアウトレットやアクションを Interface Builderで設定できると便利なのでインスタンス化だけしておく。

これらを使って必要な配線を済ませておく。

2つのウィンドウと WebView を統合する役割を担うのが WebController。ここで行う処理は:
・2つのウィンドウの結びつけ
・WebWindowへ WebViewを貼付ける
・NSProgressIndicatorの処理
NSProgressIndicatorは以前説明したので省略。
まずヘッダ。必要なインスタンス変数が定義されている。ほとんどがアウトレットで先ほどの InterfaceBuilderによる配線で準備される。
WebController.h
@class WebWindow;
@interface WebController : NSObject {
IBOutlet NSProgressIndicator* _progress_indicator;
IBOutlet WebView* _web_view;
IBOutlet NSWindow* _main_window;
IBOutlet NSView* _background_view;
WebWindow* _web_window;
}
@end
次に実装。Nib読み込み後に初期化する。
WebController.m
- (void)awakeFromNib
{
:
_web_window = [[WebWindow alloc] initWithFrame:[self webWindowFrame]];
[_web_window makeKeyAndOrderFront:nil];
[_web_window setContentView:_web_view];
[_main_window addChildWindow:_web_window ordered:NSWindowAbove];
:
}
まず WebWindowを生成する。続いて WebViewをコンテンツビューとして張りつける。
そして最後に -[NSWindow addChildWindow:ordered:] を使って2つのウィンドウを紐づける。紐付けによってベースのウィンドウの移動に合わせて WebWindowも位置関係を保ったまま移動するようになる。
WebWindowの位置と大きさは用意しておいたカスタムビューから取得する。
- (NSRect)webWindowFrame
{
NSRect frame = [_background_view frame];
frame.origin = [_main_window convertBaseToScreen:frame.origin];
return frame;
}
カスタムビューから得られる座標系は、そのビューが置かれているウィンドウ内のものになる。WebWindowはスクリーン座標系を使うので - [NSWindow convertBaseScreen:] で座標変換してやる。
最後にデリゲートを使ってベースのウィンドウのサイズが変わった時に WebWindowの大きさも変わる様にしておく。
// Main Window Delegate
- (void)windowDidResize:(NSNotification *)notification
{
[_web_window setFrame:[self webWindowFrame] display:YES];
}
これで2つのウィンドウがあたかも1つのウィンドウのように振る舞うようになる。

(おわり)
2009年7月31日金曜日
WPSU(5) - ウィンドウを2つ重ねる
以前、WebViewのキャプチャ検証を行った時と同じ方式を採用する。
参考:
webKit検証(26) - Webウィンドウを重ねる(4)
※上記は検証結果。さかのぼっていくと経緯がわかる。
この方式は2つのウィンドウを作り、手前のウィンドウに WebViewを載せておく。
キャプチャ時にはこのウィンドウの表示位置を画面外に移動しサイズを十分に大きくする。その上でキャプチャを行い画像ファイルを生成する。
ソース:WebPageScreenshotUtility-01.zip
実行してみる。
見た目は前回と変わらない。
- - - -
詳しい解説はまた明日。
2009年7月30日木曜日
WPSU(4) - 読み込み中を表示する(NSProgressIndicatorを回す)
Webページを読み込んでいる状態がわかるように NSProgressIndicator を使って状態を表示する。
Cocoaはやっぱり!
インターネットにアクセスしよう
Web Kit #5 : ページタイトルとURLを表示
によれば Cocoa Bindings を使い Interface Builder で WebView と NSProgressIndicator をバインドするだけで良いと書いてある。
試してみたが NSProgressIndicator のバインド先一覧に WebView が出てこない。
ネットで探してみると、どうも Interface Builder 3 から仕様が変わったらしい。
Cocoa-dev
WebView does not expose bindings in IB 3
仕方が無いのでバインドをあきらめ、実装コードを書く事にする。
Cocoaの日々
WebKit検証(2) - プログレスバー
以前、自分で書いたコードを参考にしつつコードを書く。
こんな感じ。
WebController.h
@interface WebController : NSObject {
IBOutlet NSProgressIndicator* _progress_indicator;
}
@end
コントローラを一つ用意し、InterfaceBuilder でアウトレットに NSProgressIndicator を接続しておく。
WebController.m
#import "WebController.h"
#import <WebKit/WebKit.h>
@implementation WebController
- (void)awakeFromNib
{
NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:@selector(progressStarted:)
name:WebViewProgressStartedNotification
object:nil];
[nc addObserver:self
selector:@selector(progressFinished:)
name:WebViewProgressFinishedNotification
object:nil];
}
- (void)progressStarted:(NSNotification *)notification
{
[_progress_indicator startAnimation:self];
}
- (void)progressFinished:(NSNotification *)notification
{
[_progress_indicator stopAnimation:self];
}
@end
実行してみよう。

回った。
2009年7月26日日曜日
2009年7月23日木曜日
2009年7月22日水曜日
WPSU(1) Webページのキャプチャ(スクリーンショット)開発開始 - まずは名前とコンセプトを決める
SimpleCapの修正が一段落したので、そろそろ Webページのキャプチャ(スクリーンショット)ソフトの開発に取りかかる。
名前は SimpleCapに合わせて WebCap にしたかったが、Windows用で既に同じ名前で同じ機能のソフトが存在した。SimpleCap-Web とか SimpleCapForWeb とか SimpleCapにひっかけたネーミングも考えたが、その場合デザインや機能で何らかの関連性を SimpleCapに持たせる必要がある。また混同されて誤解を招く危険性?もある。
基本的に名が体を表すようにしたいのだが、いかんせん既に存在する名前が多い。
WebShot
WebScreenshot
WebCapture
:
...どうせ説明調の名前にするんだったらいっその事長い名前にしてみよう。
"WebPageScreenshotUtility"
Google では完全一致はなかった(近い使われ方はあったが)。
これで行こう。
ただ長過ぎるので今後は略称として WPSU と呼ぶ。
コンセプトは SimpleCap 同様にシンプル=無駄がなく、使いやすいものを目指す。最初のバージョンでは単純なスクリーンショットの作成にしぼって開発を進める。以下、ラフ仕様。
・通常のCocoaアプリケーションの形態を取る。
・簡易ブラウザ機能を持ち、見ているページのスクリーンショットをワンボタンで取る事ができる
・複数ウィンドウ、タブ機能は持たない
・スクロールが必要な長いページも全体のスクリーンショットを取る事ができる
・ブログ等向けに縮小機能(サムネイル)を持つ
・外部アプリケーションの起動ができる(メーラーやアップローダ等との連携)
・簡易ビューアを持つ(SimpleCapViewerのようなもの)
・画像形式が選択できる(PNG, JPEG, GIF, PDF, WebArchive)
これらは最初のアイディアなので作っていく途中で必要があれば(良い方向に)変えていく。
- - - -
11月の完成を目指す。