SQLの窓

2017年02月06日


WebRTC による WEBカメラ表示を canvas にコピーして画像に変換し、サーバへアップロードする

資料ページ

Taking still photos with WebRTC
2016/3末時点のWebRTCブラウザ対応状況まとめ

※ デモページは、WEBアプリ用のテンプレートを Bootstrap を使用してスマホ対応で作成しています

▼ デモページ

※ この画像では、カメラが無い場合の代替の動画を使用しています

MediaDevices - Web API インターフェイス | MDN
Taking still photos with WebRTC - Web APIs | MDN



▼ 初回( Google Chrome )


ソースコード

カメラを使うにしても、動画を使うにしても VIDEO 要素が使用されます。そこから、いったん canvas へコピーして、base64 で表現された画像に変換します。

アップロード時は、FormData を使い、データはバイナリに変換してアップロードします。(最大3枚までアップロード可能にしています)

アップロードは ajax で行われるので、ページが書き換わる事はありません。結果として返される json は、$_FILES の内容をそのまま返しています(メッセージを追加しています)

<!DOCTYPE html>
<html lang="ja">
<head>
<meta content="width=device-width initial-scale=1.0 minimum-scale=1.0 maximum-scale=1.0 user-scalable=no" name="viewport">
<meta charset="utf-8">
<title>カメラ撮影とアップロード</title>

<!-- jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<!-- jQuery UI -->
<link id="link" rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/themes/base/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha/css/bootstrap.css">
<!-- jQuery.mmenu -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.mmenu/5.5.3/core/js/jquery.mmenu.min.all.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jQuery.mmenu/5.5.3/core/css/jquery.mmenu.all.css">
<!-- toastr -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.js"></script>

<link rel="stylesheet" href="std/mmenu.css">
<link rel="stylesheet" href="std/basic.css">

<style>
.fields {
	width: 85px;
	font-size: 12px;
	vertical-align: middle!important;
}

legend {
	font-size: 18px;
	padding-left: 6px;
}

/* 画像表示用 */
#row2 {
	vertical-align: top!important;
}

/* カメラ用 */
#camera {
	width: 400px;
	height: 300px;
	object-fit: fill;
}
#canvas {
	/* display: none; */
}
</style>

<script>
jQuery.isMobile = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()));
toastr.options={"closeButton":false,"debug":false,"newestOnTop":false,"progressBar":false,"positionClass":"toast-bottom-center","preventDuplicates":false,"onclick":null,"showDuration":"300","hideDuration":"1000","timeOut":"3000","extendedTimeOut":"1000","showEasing":"swing","hideEasing":"linear","showMethod":"fadeIn","hideMethod":"fadeOut"};
if ( !$.isMobile ) {
	toastr.options.positionClass = "toast-top-center";
}
var datepicker_option = {
	dateFormat: 'yy/mm/dd',
	dayNamesMin: ['日', '月', '火', '水', '木', '金', '土'],
	monthNames:  ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
	showMonthAfterYear: true,
	yearSuffix: '年',
	changeYear: true,
	showAnim: 'fadeIn',
	yearRange: "c-70:c"
}
$(function(){
	var curlink = $("#title").text();
	$("#title").html("<a href=\"./\" style=\"color:#fff\">" + curlink + "</a>");
});
var options = {
	row1 : { title : "カメラ" },
	row2 : { title : ""  },
	row3 : { title : "画像一覧<br>(ロード順)"  },
	row4 : { title : ""  },
	row_last : { title :"メッセージ" },
	error : function(message){
		$("#row_last").next().text( message );
		toastr.error(message);
	},
	info : function(message){
		$("#row_last").next().text( message );
		toastr.success(message);
	},
	cerror : function( message ){
		message =  message + "<br>代替として動画を表示します"
		$("#row_last").next().html( message );
		toastr.error( message );
		$("#camera")
		.prop({ 
			"loop" : true, "muted" : true, "controls" : true,
			"src" : "mp4/freebies_018_win.mp4"
		})
		.css("border", "solid 1px #000");
	}
};

// *************************************
// カメラ用データ
// *************************************
var camera;
var canvas;
var copy_count = 0;

$(function(){

	// 1) options による行とフィールドの設定
	// 2) Bootstrap 用 form-control クラスの追加
	$(".fields").each(function(){
		if ( options[ $(this).prop("id") ] ) {
			$(this).html( options[ $(this).prop("id") ].title );
			// 個別 css
			if ( options[ $(this).prop("id") ].css ) {
				$(this).next().find("input,select").css( options[ $(this).prop("id") ].css );
			}
			// 入力チェック用属性
			if ( options[ $(this).prop("id") ].attr ) {
				$(this).next().find("input,select").attr( options[ $(this).prop("id") ].attr );
			}
		}
		$(this).next().find("input,select").addClass("form-control");
	});
	// スマホでロード時の処理のチラつき防止用
	$("#wrapper").css("visibility","visible"); 

	// 初期フォーカス
	setTimeout( function(){$('#row1_fld').focus();}, 100 );

	// video 内 camera
	camera = $("#camera").get(0);

	// *************************************
	// 最新 API
	// *************************************
	if ( navigator.mediaDevices ) {
		console.log("navigator.mediaDevices");

		if ( $.isMobile ) {
			$("#camera").css("width","100%");
		}
		navigator.mediaDevices.getUserMedia({video: true})
		.then(function(stream){
			camera.src = window.URL.createObjectURL(stream);
		})
		.catch(function(err){
			// ブラウザで使用を拒否した場合等( 動画で代替 )
			options.cerror(err.name);
		});

	}
	// *************************************
	// 旧 API
	// *************************************
	else {
		console.log("navigator.getUserMedia");

		// 旧 WebRTCチェック用
		var api = [
			"webkitGetUserMedia", "mozGetUserMedia","msGetUserMedia"
		]
		$.each(api,function(idx){
			if (navigator.getUserMedia = navigator.getUserMedia || navigator[api[idx]]) {
				return false;
			}
		});
		// WebRTC 使用可能
		if ( navigator.getUserMedia ) {
			if ( $.isMobile ) {
				$("#camera").css("width","100%");
			}
			// カメラの表示
			navigator.getUserMedia({video: true}, 
				function(stream) {
					camera.src = window.URL.createObjectURL(stream);
				},
				function(err){
					// ブラウザで使用を拒否した場合等( 動画で代替 )
					options.cerror(err.name);
				}
			);	
		}
		else {
			// WebRTC 使用不可( 動画で代替 )
			options.cerror("WebRTC を使用できません");
		}
	}

	// *************************************
	// canvas にコピーして画像に変換
	// *************************************
	$("#copy").on( "click", function(){

		copy_count++;
		if ( copy_count > 3 ) {
			options.error("撮影は3枚までです");
			return false;
		}

		canvas = $("#canvas").get(0);
		var ctx = canvas.getContext('2d');

		ctx.drawImage(camera, 0, 0, canvas.width, canvas.height);

		$("<img>").appendTo("#images")
		.prop( {"src": canvas.toDataURL("image/jpeg"), "id": "image"+ copy_count } )
		.css( {"width": "100px", "margin": "10px" } );


	});

	// *************************************
	// アップロード処理
	// *************************************
	$("#frm").submit( function(event){
		// 本来の送信処理はキャンセル
		event.preventDefault();

		if ( $("#images").html() == "" ) {
			options.error("アップロードする画像ファイルを作成して下さい");
			return;
		}

		$("fieldset").eq(0).prop("disabled", true);

		// エラーメッセージエリアをクリア
		$(".error").next().text( "" );

		// 結果の表示エリアを全てクリア
		$("#result").html( "" );


		// **************************************
		// ファイルのアップロード
		// **************************************
		console.log("アップロード処理開始");

		var formData = new FormData();

		// テストの為、約100K の制限
		formData.append("MAX_FILE_SIZE", 100000);

		var file_cnt = 0;

		$("#images img").each( function() {

			var base64 = $(this).prop("src");
			var bin = atob(base64.split(',')[1]);
			var buffer = new Uint8Array(bin.length);
			for (var i = 0; i < bin.length; i++) {
				buffer[i] = bin.charCodeAt(i);
			}
			var blob = new Blob([buffer.buffer], {type: "image/jpeg"});

			file_cnt++;
			var file_name = (new Date()).getTime();
			formData.append("image"+file_cnt, blob, file_name +"_"+file_cnt+".jpg");

		});

		formData.append("FILE_COUNT", file_cnt );

		$.ajax({
			url: "./upload.php",
			type: "POST",
			data: formData,
			processData: false,  // jQuery がデータを処理しないよう指定
			contentType: false   // jQuery が contentType を設定しないよう指定
		})
		.done(function( data, textStatus ){
			console.log( "status:" + textStatus );
			console.log( "data:" + JSON.stringify(data, null, "    ") );
			options.info("アップロード処理が完了しました");

			// アップロード結果の表示
			$.each(data, function(idx,image){

				if ( image.error != 0 ) {
					$("#result").append("<tr><td><span id=\"result" +idx+"\"></span><b style='color:red'>" + image.name+ " : " + image.result +"</b></td></tr>");
				}
				else {
					$("#result").append("<tr><td><span id=\"result" +idx+"\"></span>" + image.name + " : " + image.result +"</td></tr>");
				}

				$( "#result"+idx ).append($("#"+idx).clone());

			});

			$("#images").html("");
			copy_count = 0;
		})
		.fail(function(jqXHR, textStatus, errorThrown ){
			console.log( "status:" + textStatus );
			console.log( "errorThrown:" + errorThrown );
			options.info("アップロードに失敗しました");
		})
		.always(function() {

			// 操作不可を解除
			$("fieldset").eq(0).prop("disabled", false);
		})
		;

	} );

	// **************************************
	// mmenu
	// **************************************
	$("#mmenu_left").mmenu({
		navbar: {
			title: "メニュー"
		},
		offCanvas: {
			position  : "left",
			zposition : "next"
		}
	});


});

</script>
</head>
<body>

<div id="wrapper">
<script>
// スマホでロード時の処理のチラつき防止用
//$("#wrapper").css( "visibility", "hidden" );
</script>

	<div id="head">
		<a id="hamburger" href="#mmenu_left">
	<span class="top-bar"></span>
	<span class="middle-bar"></span>
	<span class="bottom-bar"></span>
</a>
		<div id="title">カメラ撮影とアップロード</div>
	</div>

	<div id="body">
		<form id="frm" class="form-inline">

			<fieldset>
				<legend>アップロード</legend>
				<table class="table table-condensed">
			
					<tr>
						<td class="fields" id="row1"></td>
						<td>
							<video
								id="camera"
								autoplay></video>
							<canvas
								id="canvas"
								width="400"
								height="300"></canvas>								
						</td>
					</tr>

					<tr>
						<td class="fields" id="row2"></td>
						<td>
							<input id="copy" type="button" class="btn btn-primary btn-sm" value="撮影">
						</td>
					</tr>

					<tr>
						<td class="fields" id="row3"></td>
						<td>
							<div id="images"></div>
						</td>
					</tr>

					<tr>
						<td class="fields" id="row4"></td>
						<td>
							<input id="action" type="submit" class="btn btn-primary btn-sm" value="送信">
						</td>
					</tr>

					<tr>
						<td class="fields error" id="row_last"></td>
						<td></td>
					</tr>

				</table>

			</fieldset>

			<fieldset>
				<legend>結果</legend>
				<table id="result" class="table table-condensed">


				</table>

			</fieldset>

		</form>
	</div>

	<div id="comment">
	ようこそ jQuery + Bootstrap(css) + mmenu + WebRTC(カメラ) + FormData + PHP<br><a href="https://www.studio-lab01.com/freebies.html" target="_blank">素材提供:らぼわん</a> / カメラが無い場合の動画素材	</div>

</div>


<div id="mmenu_left">
<ul>
	<li class="mm_user_title">ページ選択</li>
	<li><a class="mm_link_left" href="#" onclick="location='index.php';void(0)">リセット</a></li>
	<li><a class="mm_link_left" 
			href="http://getbootstrap.com/css/"
			onclick="location='index.php';void(0)"
			target="_blank"
		>Bootstrap(css)</a></li>
	<li><a class="mm_link_left"
			href="http://api.jquery.com/"
			onclick="location='index.php';void(0)"
			target="_blank"
		>jQuery ドキュメント</a></li>

	<li><a class="mm_link_left"
			href="https://developer.mozilla.org/ja/docs/Web/Guide/Using_FormData_Objects"
			onclick="location='index.php';void(0)"
			target="_blank"
		>FormData オブジェクトの利用 / MDN</a></li>

	<li><a class="mm_link_left"
			href="https://developer.mozilla.org/ja/docs/Web/API/MediaDevices"
			onclick="location='index.php';void(0)"
			target="_blank"
		>MediaDevices (MDN)</a></li>

	<li><a class="mm_link_left"
			href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Taking_still_photos"
			onclick="location='index.php';void(0)"
			target="_blank"
		>Taking still photos with WebRTC(英文)</a></li>


</ul>
</div>


</body>
</html>



upload.php

※ アップロードした画像の保存部分はコメントにしています
<?php
// キャラクタセット
// *************************************
header( "Content-Type: application/json; charset=utf-8" );
// *************************************
// キャッシュ無効
// *************************************
session_cache_limiter('nocache');
session_start();

// ファイルを移動するフォルダ
$target_folder = "./images/";


$cnt = $_POST["FILE_COUNT"] + 0;

for( $i = 0; $i < $cnt; $i++ ) {

	$image_target = "image".($i+1);

	if ( $_FILES[$image_target]["error"] == 0 ) {

		// *************************************
		// 1) 画像フォーマットの取得
		// *************************************
		$type_string = image_type_to_mime_type( exif_imagetype( $_FILES[$image_target]['tmp_name'] ) );

		// *************************************
		// 2) オリジナルファイル名の取得
		// *************************************
		$file = explode(".", $_FILES[$image_target]['name']);

		// *************************************
		// 3) 日本語ファイル名対応
		// *************************************
		$file_name = urlencode( $file[0] );

		// *************************************
		// 4) 保存ファイル名を作成
		//   a) 拡張子決定
		//   b) uniqid() でファイル目をユニーク
		// *************************************
		$target = "";
		if ( $type_string == "image/jpeg" ) {
			$target = uniqid() . "_{$file_name}.jpg";
		}
		if ( $type_string == "image/gif" ) {
			$target = uniqid() . "_{$file_name}.gif";
		}
		if ( $type_string == "image/png" ) {
			$target = uniqid() . "_{$file_name}.png";
		}
		if ( $target == "" ) {
			$_FILES["image"]["result"][] = "アップロードできないフォーマットです";
		}
		else {
			// *************************************
			// アップロードファイルの保存
			// *************************************
//			if ( @move_uploaded_file( $_FILES[$image_target]['tmp_name'], $target_folder . $target ) ) {
				$_FILES[$image_target]["result"] = "アップロードに成功しました";
//			}
//			else {
				// なんらかの環境エラー
//				$_FILES[$image_target]["result"] = "アップロードに失敗しました";
//			}
			
		}
	}
	else {
		switch($_FILES[$image_target]["error"]){
			case 1:
				$_FILES[$image_target]["result"] = "php.ini の upload_max_filesize ディレクティブの値を超えています";
				break;
			case 2:
				$_FILES[$image_target]["result"] = "HTML フォームで指定された MAX_FILE_SIZE を超えています";
				break;
			case 3:
				$_FILES[$image_target]["result"] = "一部のみしかアップロードされていません";
				break;
			case 4:
				$_FILES[$image_target]["result"] = "アップロードされませんでした";
				break;
			case 6:
				$_FILES[$image_target]["result"] = "テンポラリフォルダがありません";
				break;
			case 7:
				$_FILES[$image_target]["result"] = "ディスクへの書き込みに失敗しました";
				break;
			case 8:
				$_FILES[$image_target]["result"] = "PHP の拡張モジュールがファイルのアップロードを中止しました";
				break;
			default:
				$_FILES[$image_target]["result"] = "不明なエラーです";
		}
		
	}

}


print json_encode($_FILES)


?>


関連する記事

WebRTC による WEBカメラ表示



posted by lightbox at 2017-02-06 18:18 | Comment(0) | API | このブログの読者になる | 更新情報をチェックする

2017年02月01日


RICOHのデジタルフルカラー複合機でスキャンしたPDFを自分のPCに保存する

RICOH MP C6004/C5504/C4504/C3504/C3004 : デジタルフルカラー複合機

目標は、ドメインで管理されているネットワークのPC への PDF の保存です。

その前にもっと簡単そうな、スキャンしたPDFをメールに添付して送るテストをしましたが、ごく普通のメールクライアントの設定で使用可能でした。



この段階でプリンタのタッチパネルの操作に慣れ、PC のブラウザからIPアドレスでアクセス可能な情報+操作ページのどこにどんな情報があるかに慣れました。

メールの場合は、いったんプリンタ内の共有フォルダに保存してから、ブラウザからの操作でメール送信を行います。また、メールを送る場合は、必ず送信者を指定する必要があるので、それ用のアドレス登録をしておきます(送信時のSMTPのユーザとパスワードはシステムで一つだけで、登録するのはユーザ毎の情報になります)。このアドレスは、PC の共有に対して PDF を送る時にも使用します。実運用としては、スキャン毎に共有アドレスとユーザとパスワードを毎回入力するのは無理がありますので必須です。

ブラウザからは、システムログの表示を見る事ができますが、ここにはエラーが起きた時にログが追加されるのでテストには重要な情報です。

アドレスは、直接入力が可能なので、その時はタッチパネルにエラーが表示されますが、登録済みのアドレスを使用した場合は、問題があればシステムログにのみ情報が出力されてタッチパネルは正常終了になります。

ドメイン + シマンテック + Windows Firewall

ドメインでは無く、シマンテックでなければ何も問題なく動作すると思います。但し、デジタルフルカラー複合機のデフォルトのワークグループは、『WORKGROUP』です。

まず、ドメインで管理されているので、デジタルフルカラー複合機のワークグループを変更する必要があります。この変更は、ブラウザより管理者としてログインして行いました。導入直後のユーザ名はすごくポピュラーなあれです。そしてパスワードは入っていません。

この変更後、コマンドプロンプトから net view を実行すると、プリンタのエントリが表示されます。

この後は、シマンテックとWindows Firewall を無効にして、自分の PC に共有を作成して保存のテストを行います。デジタルフルカラー複合機からPC の共有を参照して保存するので、ログインするユーザをアクセス許可に登録して、書き込み可にしておく必要があります。そして、セキュリティ管理上面倒な everyone は削除しておきます。

このユーザのログイン情報をデジタルフルカラー複合機に登録する際は、ユーザは ドメイン名\ユーザ名 で登録する必要があるので注意して下さい。

シマンテック の設定

重要なキーワードは SMB プロトコルです。そもそも、デジタルフルカラー複合機のネットワークの設定(ワークグループ)が SMB というカテゴリで設定されます。シマンテック の設定ではあまり気にする必要はありませんが、ポートとしては 137、138、139 という3つのポートを知っておく必要があります。(445 も知識としては重要です)

で、結局シマンテックはちょっとバグっぽいのですが、『ネットワーク脅威防止の設定』の『Microsoft Windows ネットワーク』タブにある、『ネットワーク上の他のユーザとファイルプリンタを共有する』にきちんとチェックが入るようにすれば良いです。

自分の環境では、このチェックが通常のチェックでは無く、部分的に有効な場合に使われる四角いチェックになっていました。なので、一度チェックを完全に外してから再度チェックすると問題無く動作しました。これに関してはネット上で同様の記事がありましたが、それでもダメな場合は、例のポートの受信を許可するようにルールを作成する必要があるでしょう。

Windows Firewall の設定

Windows Firewall では、137、138、139 の許可設定が、ファイアウォールのプロファイル毎に存在していました。なので、ドメイン用が無効になっていたので、有効にします(ポート毎に3つエントリがあるので、プロファイル毎で全部で9つあります)。これで動作するはずですが、面倒なら Firewall は一時的に止めてしまってもいいかもしれません。(時間のかかるアナログ作業を優先したほうがいい場合もあります)

※ 445 のエントリは SMB とコメントが書かれてありますが、そちらは通常は無視していいはずです。




タグ:SMB
posted by lightbox at 2017-02-01 20:38 | Comment(0) | Windows7 | このブログの読者になる | 更新情報をチェックする

2017年01月27日


Android Studio で、javamail を gradle に書いて添付ファイル付きメール送信を行う

数年前までは、Google Code の 2009 年ライブラリを使用して Android 用のメール送信を行っていたのですが、

参照 ➡ javamail-android + AsyncTask でメール送信を行う為のテンプレート

今回改めて調べてみると、オリジナルの javamail 側で Android の対応がなされていました。gradle に記述するだけで使用可能です。

▼ 今回使用したもの
apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "23.0.3"
    defaultConfig {
        applicationId "january17.lightbox.mailapplication"
        minSdkVersion 19
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    packagingOptions {
        pickFirst 'META-INF/LICENSE.txt' // picks the JavaMail license file
    }
}

repositories {
    jcenter()
    maven {
        url "https://maven.java.net/content/groups/public/"
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:22.2.1'
    compile 'com.sun.mail:android-mail:1.5.5'
    compile 'com.sun.mail:android-activation:1.5.5'
    testCompile 'junit:junit:4.12'
}


さらに、今回は添付ファイルを付加する為に調べてみると、MimeBodyPart クラスの attachFile メソッドに、ファイルのパスを渡すだけで全てライブラリが実行してくれる事が解りました。さっそくオリジナルのソースコードを読んでみると、古くからあるサンプルの FileDataSource を使った内容(Google code バージョンでも同等の処理をしていました)であり、さらに Google code バージョンには無い contentType と encoding を指定できる attachFile メソッドも存在していました。

添付ファイル用 AndroidSendmail クラス
public class AndroidSendmail {

	private String server;
	private String port;
	private String userid;
	private String password;
	private String username;

	// ************************************
	// コンストラクタ
	// ************************************
	public AndroidSendmail(
		String server,
		String port,
		String userid,
		String password,
		String username) {

		this.server = server;
		this.port = port;
		this.userid = userid;
		this.password = password;
		this.username = username;
	}

	// ************************************
	// AsyncTask の onPostExecute から外部イベントとして
	// 呼び出す為のインターフェイス
	// ************************************
	public interface SendMailed {
		public void onMailResult(String result);
	}

	// ************************************
	// メール送信
	// ************************************
	public void SendMail(String to, String from, String subject, String body,String file, final SendMailed sm) {

		new AsyncTask<String, Void, String>() {

			// ************************************
			// 非同期処理
			// ************************************
			@Override
			protected String doInBackground(String... params) {

				Log.i("lightbox","開始");

				String result_string = "";
				try {
					// ************************************
					// プロパティオブジェクトを作成
					// プロパティオブジェクトは、extends Hashtable(連想配列)
					// ************************************
					Properties props = new Properties();

					// ************************************
					// * 連想配列に送信用サーバのアドレスをセット
					// ************************************
//					props.put("mail.smtp.host","smtp.mail.yahoo.co.jp");	// Yahoo
//					props.put("mail.smtp.host","smtp.live.com");	// Microsoft
					props.put("mail.smtp.host", server);
					props.put("mail.smtp.port", port);
					props.put("mail.smtp.auth", "true" );	// SMTP 認証を行う

					// ************************************
					// SSL関連設定
					// ************************************
					if ( port.equals("465") ) {
						props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
						props.put("mail.smtp.socketFactory.fallback", "false");
						props.put("mail.smtp.socketFactory.port", port);
					}

					// ************************************
					// 暗号化
					// ************************************
					if ( port.equals("587") ) {
						props.put("mail.smtp.starttls.enable", "true");
					}


					// ************************************
					// メール用のセッションを作成
					// ************************************
					SimpleAuthenticator sa =
						new SimpleAuthenticator(userid, password);
					Session MailSession =
						Session.getInstance( props, sa );

					// ************************************
					// メール用のメッセージオブジェクトを作成
					// ************************************
					MimeMessage msg = new MimeMessage(MailSession);

					// ************************************
					// 宛先
					// ( 第一引数では、CC や BCC を指定できます。)
					// ************************************
					msg.setRecipients(
						Message.RecipientType.TO,
						new Address[] { new InternetAddress( params[0], "日本語相手先", "ISO-2022-JP" ) }
					);

					// ************************************
					// 送信者
					// ************************************
					msg.setFrom(
						new InternetAddress( params[1], username, "ISO-2022-JP" )
					);

					// ************************************
					// 件名
					// ************************************
					msg.setSubject( params[2], "ISO-2022-JP" );

					// ************************************
					// 本文(テキスト)
					// ************************************
					MimeBodyPart mbp1 = new MimeBodyPart();
					mbp1.setText(params[3], "ISO-2022-JP");

					// ************************************
					// 添付ファイル
					// ************************************
					MimeBodyPart mbp2 = new MimeBodyPart();
					mbp2.attachFile(params[4]);

					// ************************************
					// マルチパート作成
					// ************************************
					Multipart mp = new MimeMultipart();
					mp.addBodyPart(mbp1);
					mp.addBodyPart(mbp2);

					msg.setContent(mp);

					// ************************************
					// 送信
					// ************************************
					Transport.send( msg );

					result_string = "メールの送信を完了しました";

				}
				catch( Exception e ) {
					e.printStackTrace();
					result_string = "メールの送信に失敗しました";
				}

				Log.i("lightbox","終了");

				return result_string;
			}

			// ************************************
			// 非同期処理終了後の処理( 画面へのアクセスが可能 )
			// ************************************
			@Override
			protected void onPostExecute(String result) {
				// 引数のインターフェイス内のメソッドを呼び出す
				sm.onMailResult(result);
			}

		}.execute(to, from, subject, body,file);

	}

	// ************************************
	// 認証用のプライベートクラス
	// ************************************
	private class SimpleAuthenticator extends Authenticator {

		private String user_string = null;
		private String pass_string = null;

		public SimpleAuthenticator( String user_s, String pass_s ) {
			super();
			user_string = user_s;
			pass_string = pass_s;
		}

		protected PasswordAuthentication getPasswordAuthentication(){
			return new PasswordAuthentication( this.user_string, this.pass_string );
		}
	}
}

添付するファイルは、ギャラリーから取得する画像を使ってテストしました。昨今、Android では、ギャラリーから取得する為の intent のパラメータが過去バージョンから変わっており、取得する uri も 直接のパスでは無い為、作法にのっとってデーターベースより必要な情報を収集しています。

MainActivity

※ ここでは、MIME と 画像の表示名を DB から取得していますが、使用していません
※ Gmail を使用する場合は、安全性の低いアプリの許可を『有効』にする 必要があります。
public class MainActivity extends AppCompatActivity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		Button button = (Button) MainActivity.this.findViewById(R.id.button);
		button.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {

				// ギャラリーの呼び出し
				Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
				intent.addCategory(Intent.CATEGORY_OPENABLE);
				intent.setType("image/*");
				startActivityForResult(intent, 10);        // 10 は任意

			}
		});
	}

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		super.onActivityResult(requestCode, resultCode, data);

		if (requestCode == 10 && resultCode == Activity.RESULT_OK) {
			if (data != null) {
				Uri uri = data.getData();

				String id = DocumentsContract.getDocumentId(uri);
				Log.i("lightbox", "id文字列:" + id);

				// カラムリストの指定
				String[] proj = {
					MediaStore.Images.Media.DATA,
					MediaStore.Images.Media.MIME_TYPE,
					MediaStore.Images.Media.DISPLAY_NAME
				};
				String selection = "_id=?";
				String[] args = new String[]{id.split(":")[1]};

				// 目的データのカーソルを取得
				ContentResolver contentResolver = getContentResolver();
				Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, proj, selection, args, null);
				cursor.moveToFirst();    // 読み出し

				// データを取得
				String result = cursor.getString(0);
				String mime = cursor.getString(1);
				String name = cursor.getString(2);
				Log.i("lightbox", "Path:" + result);
				cursor.close();


				AndroidSendmail as = new AndroidSendmail(
					"smtp.gmail.com", // サーバ( 例 : "smtp.mail.yahoo.co.jp" or "smtp.live.com" )
					"465", // 465 または 587
					"アカウント", // アカウント
					"パスワード", // パスワード
					"日本語ユーザ名"
				);

				as.SendMail(
					"宛先メールアドレス",
					"自分のメールアドレス",
					"件名",
					"本文一行目\n本文二行目",
					result, // ファイルへのパス
					new AndroidSendmail.SendMailed() {
						@Override
						public void onMailResult(String result) {
							Log.i("lightbox", result);
						}
					}
				);

			}
		}
	}
}


上のコードでは、Android API 19 以上で利用できる、getDocumentId を使用していますが、それより古いバージョンでは 呼び出し時に ACTION_GET_CONTENT を使用して、取得できる uri の パスの一番最後の数値を _id に使用して取得できました。
※ API 18(4.3.1) と API 15(4.0.3) を エミュレータで確認しました。
Uri uri = data.getData();

Log.i("lightbox", "id文字列:" + uri.toString());

// カラムリストの指定
String[] proj = {
	MediaStore.Images.Media.DATA,
	MediaStore.Images.Media.MIME_TYPE,
	MediaStore.Images.Media.DISPLAY_NAME
};
String selection = "_id=?";
String[] path = (uri.toString()).split("/");
String[] args = new String[]{path[path.length-1]};


Manifest.xml

※ INTERNET と READ_EXTERNAL_STORAGE が必要です
<?xml version="1.0" encoding="utf-8"?>
<manifest package="january17.lightbox.mailapplication"
          xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

最後に

マルチパートの内容を WEB上の Outlook から送って比べてみました。Microsoft は、異常に多くの情報を持っていましたが、一般的なデータは javamail の setText(本文, "ISO-2022-JP") と attachFile(ファイルのパス) で問題無いようでした。(ヘッダ部分は通常の WEB メールで確認できます)
▼ Microsoft()
--_002_PS1PR01MB0667CE2FF5CECC981B23A8CF81710PS1PR01MB0667apcp_
Content-Type: text/plain; charset="iso-2022-jp"
Content-Transfer-Encoding: quoted-printable

=1B$BK\J80l9TL\=1B(B
=1B$BK\J8Fs9TL\=1B(B

--_002_PS1PR01MB0667CE2FF5CECC981B23A8CF81710PS1PR01MB0667apcp_
Content-Type: image/jpeg; name="2013-01-26T01-26-00_0.jpg"
Content-Description: 2013-01-26T01-26-00_0.jpg
Content-Disposition: attachment; filename="2013-01-26T01-26-00_0.jpg";
	size=431072; creation-date="Fri, 20 Jan 2017 16:29:33 GMT";
	modification-date="Fri, 20 Jan 2017 16:29:33 GMT"
Content-Transfer-Encoding: base64

/9j/4AAQSkZJRgABAQAAAQABAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQA
AAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAGYKADAAQAAAABAAAEyAAAAAD/2wBD
AAIBAQIBAQICAQICAgICAwUDAwMDAwYEBAMFBwYHBwcGBgYHCAsJBwgKCAYGCQ0JCgsLDAwMBwkN

▼ JavaMail for Android
------=_Part_0_1107074864.1484932123775
Content-Type: text/plain; charset=ISO-2022-JP
Content-Transfer-Encoding: quoted-printable

=1B$BK\J80l9TL\=1B(B
=1B$BK\J8Fs9TL\=1B(B

------=_Part_0_1107074864.1484932123775
Content-Type: image/jpeg; name=KIMG0178.JPG
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=KIMG0178.JPG

/9j/4TjERXhpZgAATU0AKgAAAAgACgEPAAIAAAAIAAAAhgEQAAIAAAAGAAAAjgESAAMAAAABAAMA
AAEaAAUAAAABAAAAlAEbAAUAAAABAAAAnAEoAAMAAAABAAIAAAExAAIAAAALAAAApAEyAAIAAAAU
AAAAsAITAAMAAAABAAEAAIdpAAQAAAABAAAAxAAABEpLWU9DRVJBADQwNEtDAAAAAEgAAAABAAAA



posted by lightbox at 2017-01-27 21:29 | Comment(0) | Android Studio 2017 | このブログの読者になる | 更新情報をチェックする

Google Chrome の Flash 完全排除に対応 : JavaScript でクリップボードに文字列をコピーする Clipboard.js の使用方法と注意事項

世の中の一般的な使用方法は、オリジナルサイトの概要と大差無いようですが、実際本当に使いたい環境を持っている人にとっては説明不十分です。

バージョン 1.5.4 までと 1.5.5 からでは仕様がすこし違う

問題は、element.focus(); ですが、これですと環境によってはコピー対象となるエレメントの位置までスクロールしてしまうので(実際このブログではしてしまった)ので、良く解らずに 1.5.4 以前を使用している場合は、バージョンアップしないほうがいいです。また、以下のソースコードからも解るように、focus() が実行されるのは、INPUT か TEXTAREA か その他の要素で contenteditable 属性を持っている場合だけなので、普通に DIV ならば focus() が実行される事はありません。
/*!
 * clipboard.js v1.5.12
 * https://zenorocha.github.io/clipboard.js
 *
 * Licensed MIT c Zeno Rocha
 */
    if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
        element.focus();
        element.setSelectionRange(0, element.value.length);

        selectedText = element.value;
    }
    else {
        if (element.hasAttribute('contenteditable')) {
            element.focus();
        }

        var selection = window.getSelection();
        var range = document.createRange();

        range.selectNodeContents(element);
        selection.removeAllRanges();
        selection.addRange(range);

        selectedText = selection.toString();
    }

    return selectedText;
この SyntaxHighlighter の右上のツールバーにある copy ボタンは、Clipboard.js を組み込んで実装しました。

実装

このような事をふまえて一般的な実装は以下のようになります
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.5.12/clipboard.min.js"></script>
<script>
$(function(){
	var clipboard = new Clipboard('.clipboard_element');
	clipboard.on('success', function(e) {
	    alert("クリップボードにコピーしました");
	});
	$(".action_btn").on("click", function(){
		var work = prompt("入力値をクリップボードにセット")
		$("#clipboard").text( work );
	})
});
</script>
<div class="clipboard_element" data-clipboard-target="#clipboard">
	<button type="button" class="action_btn">実行</button>
</div>

<div id="clipboard" style='position:absolute;left:-1000px;width:900px;white-space:pre-wrap;word-wrap:break-word;'></div>
このソースコードには、いくつか重要な部分が存在します。

注意事項

1) ページにひとつだけ、クリップボード転送用の要素を DIV で作成する
   (body 要素直下でいいと思います)

2) DIV 内に改行やスペースを反映させる為に white-space:pre を設定する

3) ページ上から隠す為に、position:absolute;left:-1000px を設定する
   (クリップボートへコピーする為に選択する必要があるので、非表示では動作しません)
   転送したテキストがこちら側へ表示しないように width:900px と word-wrap:break-word を指定する

4) クリックイベントは、ボタンである必要は無く、どのような要素でも良い

5) ボタンをクリックした時に必要な文字列を DIV に転送した後、DIV のクリックイベントが発生
実際の Clipboard.js のイベントは、ページ上のクリックイベントが全て終了してから発生するので、この順序である必要は無いのですが、仕様変更等あった場合にそなえてこの順序が良いと思います。


追記

ここで行っているような転送用のエリアを作成した処理を半自動でやるのがどうやら Advanced Options のあたりのようなのですが、ソース読むといろいろ scroll とかやってるので、環境によってはやはり問題がでるかもしれません。


Google、ChromeでのFlashブロックをさらに推進、12月にHTML5をデフォルトに

関連する記事

clipboard.js のコピーさせるテキストを自由にダイナミックに渡す方法は、Advanced Usage の text です。



posted by lightbox at 2017-01-27 19:40 | Comment(0) | JavaScript ライブラリ | このブログの読者になる | 更新情報をチェックする

2017年01月24日


WebRTC による WEBカメラ表示

資料ページ

Taking still photos with WebRTC
2016/3末時点のWebRTCブラウザ対応状況まとめ

※ デモページは、WEBアプリ用のテンプレートを Bootstrap を使用してスマホ対応で作成しています

▼ デモページ


MediaDevices - Web API インターフェイス | MDN
Taking still photos with WebRTC - Web APIs | MDN



▼ 初回( Google Chrome )


▼ カメラが無い場合


ソースコード

当初、古い API を実装していたのですが、いろいろ調べるうちに新しい API に変わっていた事に気がついて、両方のコードが実装されています。
WebRTC の処理部分は 119行 〜 183行です。

※ 処理画面部分は 220行 〜 256行
※ 画面構築しているテンプレート部分は、64行 〜 86行 と 98行 〜 116行

<!DOCTYPE html>
<html lang="ja">
<head>
<meta content="width=device-width initial-scale=1.0 minimum-scale=1.0 maximum-scale=1.0 user-scalable=no" name="viewport">
<meta charset="utf-8">
<title>カメラ表示</title>

<script src="//ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<link id="link" rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/themes/base/jquery-ui.css">
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha/css/bootstrap.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.mmenu/5.5.3/core/js/jquery.mmenu.min.all.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jQuery.mmenu/5.5.3/core/css/jquery.mmenu.all.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.js"></script>

<link rel="stylesheet" href="../std/mmenu.css">
<link rel="stylesheet" href="../std/basic.css">

<style>
.fields {
	width: 85px;
	font-size: 12px;
	vertical-align: middle!important;
}

legend {
	font-size: 18px;
	padding-left: 6px;
}

/* カメラ用 */
#camera {
	width: 400px;
	height: 300px;
	object-fit: fill;
}

.table-responsive td, .table-responsive th {
	white-space: nowrap;
}
</style>

<script>
jQuery.isMobile = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()));
toastr.options={"closeButton":false,"debug":false,"newestOnTop":false,"progressBar":false,"positionClass":"toast-bottom-center","preventDuplicates":false,"onclick":null,"showDuration":"300","hideDuration":"1000","timeOut":"3000","extendedTimeOut":"1000","showEasing":"swing","hideEasing":"linear","showMethod":"fadeIn","hideMethod":"fadeOut"};
if ( !$.isMobile ) {
	toastr.options.positionClass = "toast-top-center";
}
var datepicker_option = {
	dateFormat: 'yy/mm/dd',
	dayNamesMin: ['日', '月', '火', '水', '木', '金', '土'],
	monthNames:  ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
	showMonthAfterYear: true,
	yearSuffix: '年',
	changeYear: true,
	showAnim: 'fadeIn',
	yearRange: "c-70:c"
}
$(function(){
	var curlink = $("#title").text();
	$("#title").html("<a href=\"./\" style=\"color:#fff\">" + curlink + "</a>");
});
var options = {
	row1 : { title : "カメラ" },
	row_last : { title :"メッセージ" },
	error : function(message){
		$("#row_last").next().text( message );
		toastr.error(message);
	},
	info : function(message){
		$("#row_last").next().text( message );
		toastr.success(message);
	},
	cerror : function( message ){
		message =  message + "<br>代替として動画を表示します"
		$("#row_last").next().html( message );
		toastr.error( message );
		$("#camera")
		.prop({ 
			"loop" : true, "muted" : true, "controls" : true,
			"src" : "https://lightbox.sakura.ne.jp/demo/mp4/waterfall-free-video9.mp4"
		})
		.css("border", "solid 1px #000");
	}
};

// *************************************
// カメラ用データ
// *************************************
var camera;
var canvas;

$(function(){

	// 1) options による行とフィールドの設定
	// 2) Bootstrap 用 form-control クラスの追加
	$(".fields").each(function(){
		if ( options[ $(this).prop("id") ] ) {
			$(this).html( options[ $(this).prop("id") ].title );
			// 個別 css
			if ( options[ $(this).prop("id") ].css ) {
				$(this).next().find("input,select").css( options[ $(this).prop("id") ].css );
			}
			// 入力チェック用属性
			if ( options[ $(this).prop("id") ].attr ) {
				$(this).next().find("input,select").attr( options[ $(this).prop("id") ].attr );
			}
		}
		$(this).next().find("input,select").addClass("form-control");
	});
	// スマホでロード時の処理のチラつき防止用
	$("#wrapper").css("visibility","visible"); 

	// 初期フォーカス(row1_fld があれば機能します)
	setTimeout( function(){$('#row1_fld').focus();}, 100 );

	// video 内 camera
	camera = $("#camera").get(0);

	// *************************************
	// 最新 API
	// *************************************
	if ( navigator.mediaDevices ) {
		if ( $.isMobile ) {
			$("#camera").css("width","100%");
		}
		navigator.mediaDevices.getUserMedia({video: true})
		.then(function(stream){
			camera.src = window.URL.createObjectURL(stream);
		})
		.catch(function(err){
			// ブラウザで使用を拒否した場合等( 動画で代替 )
			options.cerror(err.name);
		});

		// デバイス一覧
		$("#result").append("<tr><th>label</th><th>deviceId</th><th>kind</th></tr>");
		navigator.mediaDevices.enumerateDevices()
		.then(function(devices) {
			devices.forEach(function(device) {
				$("#result").append("<tr><td>" + device.label + "</td><td>" + device.deviceId +"</td><td>"+ device.kind + "</td></tr>");
			});
		})
		.catch(function(err) {
			options.error(err.name + ": " + err.message);
		});

	}
	// *************************************
	// 旧 API
	// *************************************
	else {
		// 旧 WebRTCチェック用
		var api = [
			"webkitGetUserMedia", "mozGetUserMedia","msGetUserMedia"
		]
		$.each(api,function(idx){
			if (navigator.getUserMedia = navigator.getUserMedia || navigator[api[idx]]) {
				return false;
			}
		});
		// WebRTC 使用可能
		if ( navigator.getUserMedia ) {
			if ( $.isMobile ) {
				$("#camera").css("width","100%");
			}
			// カメラの表示
			navigator.getUserMedia({video: true}, 
				function(stream) {
					camera.src = window.URL.createObjectURL(stream);
				},
				function(err){
					// ブラウザで使用を拒否した場合等( 動画で代替 )
					options.cerror(err.name);
				}
			);	
		}
		else {
			// WebRTC 使用不可( 動画で代替 )
			options.cerror("WebRTC を使用できません");
		}
	}

	// **************************************
	// mmenu
	// **************************************
	$("#mmenu_left").mmenu({
		navbar: {
			title: "メニュー"
		},
		offCanvas: {
			position  : "left",
			zposition : "next"
		}
	});


});

</script>
</head>
<body>

<div id="wrapper">
<script>
// スマホでロード時の処理のチラつき防止用
$("#wrapper").css( "visibility", "hidden" );
</script>

	<div id="head">
		<a id="hamburger" href="#mmenu_left">
	<span class="top-bar"></span>
	<span class="middle-bar"></span>
	<span class="bottom-bar"></span>
</a>
		<div id="title">カメラ表示</div>
	</div>

	<div id="body">
		<form id="frm" class="form-inline">

			<fieldset>
				<legend></legend>
				<table class="table table-condensed">
			
					<tr>
						<td class="fields" id="row1"></td>
						<td>
							<video
								id="camera"
								autoplay></video>
						</td>
					</tr>

					<tr>
						<td class="fields error" id="row_last"></td>
						<td></td>
					</tr>

				</table>

			</fieldset>

			<fieldset>
				<legend>結果</legend>
				<div class="table-responsive">
					<table id="result" class="table table-condensed">

					</table>
				</div>

			</fieldset>

		</form>
	</div>

	<div id="comment">
	ようこそ jQuery + Bootstrap(css) + mmenu + WebRTC(カメラ)	</div>

</div>


<div id="mmenu_left">
<ul>
	<li class="mm_user_title">ページ選択</li>
	<li><a class="mm_link_left" href="#" onclick="location='index.php';void(0)">リセット</a></li>
	<li><a class="mm_link_left" 
			href="http://getbootstrap.com/css/"
			onclick="location='index.php';void(0)"
			target="_blank"
		>Bootstrap(css)</a></li>
	<li><a class="mm_link_left"
			href="http://api.jquery.com/"
			onclick="location='index.php';void(0)"
			target="_blank"
		>jQuery ドキュメント</a></li>

	<li><a class="mm_link_left"
			href="https://developer.mozilla.org/ja/docs/Web/API/MediaDevices"
			onclick="location='index.php';void(0)"
			target="_blank"
		>MediaDevices (MDN)</a></li>


</ul>
</div>


</body>
</html>


関連する記事

WebRTC による WEBカメラ表示を canvas にコピーして画像に変換し、サーバへアップロードする



posted by lightbox at 2017-01-24 15:09 | Comment(0) | API | このブログの読者になる | 更新情報をチェックする
container 終わり

フリーフォントで簡単ロゴ作成
フリーフォントでボタン素材作成
フリーフォントで吹き出し画像作成
フリーフォントではんこ画像作成
ほぼ自由に利用できるフリーフォント
フリーフォントの書体見本とサンプル
画像を大きく見る為のウインドウを開くボタンの作成

Android SDK ポケットリファレンス
改訂版 Webデザイナーのための jQuery入門
今すぐ使えるかんたん ホームページ HTML&CSS入門
CSS ドロップシャドウの参考デモ
PHP正規表現チェッカー
Google Hosted Libraries
cdnjs
BUTTONS (CSS でボタン)
イラストAC
ぱくたそ
写真素材 足成
フリーフォント一覧
utf8 文字ツール
右サイド 終わり
base 終わり