ノードを、いくつかの固まりごとに処理する

たとえば、XML形式の住所録データがあるとする。そこから宛名ラベル印刷用のデータを作る(住所録データ→XSL-FO→PDFといった形で印刷可能なデータにするとか)。宛名ラベル印刷用紙は用紙1枚につき10宛先をできるとする。そうなると、住所録データ内の住所データ(氏名、住所などなどを子としてもつ要素とする)を、10ずつ扱うとかしたい。
これは手続き型言語のように単純な繰り返し構文(C言語のforみたいなの)があれば話は簡単なのだけれども、そういうのはない。xsl:for-eachの使い方を工夫して実現する必要がある。10ずつに区切ったノードの先頭(Character[position() mod 10 = 1] または Character[(position() - 1) mod 10 = 0])をxsl:for-eachで処理するようにして、そのノード自身と、あとに続く兄弟ノード(最大9個)を処理するといったふうにやるとよい。
住所録データじゃないけど、下にマンガの登場人物データベース(しごく、単純なもの)の登場人物要素(Character要素)を、いくつかの固まりで処理するサンプルを書いた。HTMLとして出力し、各データは表形式で出力される。
表の列数(あとのサンプルだと列数も)を無理にあわせるために、ちょっとめんどうな処理を入れている部分があり、サンプルとしては冗長だけど、そのへんはご容赦のほどを。

もっと良いやり方があれば、コメント欄などでお教えください。

"sample.xml"
元データとなる登場人物データベース。
"sample1.xsl"
変数 range の大きさをひとかたまりとしてCharacter要素を扱う。
"sample2.xsl"
"sample1.xsl"を、ちょっと複雑にしたもの。変数 columnSize x 変数 rowSize の表を作る(= 変数 columnSize x 変数 rowSize の大きさをひとかたまりとしてCharacter要素を扱う。)

XSLTは、.NET Framework(System.Xml.Xsl.XslCompiledTransform クラス。PowerShellから利用する)とSAXONで処理した。

こちらの動作環境は以下のもの。

OS
Windows XP SP3 x86
PowerShell
PowerShell Ver. 2.0
Java
Java 6 Update 22
SAXON
SAXON-HE 9.3.0.4 (公式サイト:The SAXON XSLT and XQuery Processor

各種ファイル

"sample.xml"
<?xml version="1.0" encoding="UTF-8"?>
<Characters>
	<!-- けいおん! -->
	<Character>
		<FamilyName>平沢</FamilyName>
		<FirstName></FirstName>
		<Race>人間</Race>
		<Sex></Sex>
		<Title>けいおん!</Title>
	</Character>
	<Character>
		<FamilyName>秋山</FamilyName>
		<FirstName></FirstName>
		<Race>人間</Race>
		<Sex></Sex>
		<Title>けいおん!</Title>
	</Character>
	<Character>
		<FamilyName>田井中</FamilyName>
		<FirstName></FirstName>
		<Race>人間</Race>
		<Sex></Sex>
		<Title>けいおん!</Title>
	</Character>
	<Character>
		<FamilyName>琴吹</FamilyName>
		<FirstName></FirstName>
		<Race>人間</Race>
		<Sex></Sex>
		<Title>けいおん!</Title>
	</Character>
	<Character>
		<FamilyName>中野</FamilyName>
		<FirstName></FirstName>
		<Race>人間</Race>
		<Sex></Sex>
		<Title>けいおん!</Title>
	</Character>
	<Character>
		<FamilyName>山中</FamilyName>
		<FirstName>さわ子</FirstName>
		<Race>人間</Race>
		<Sex></Sex>
		<Title>けいおん!</Title>
	</Character>

	<!-- セキレイ -->
	<Character>
		<FamilyName>佐橋</FamilyName>
		<FirstName>皆人</FirstName>
		<Sex></Sex>
		<Race>人間</Race>
		<Title>セキレイ</Title>
	</Character>
	<Character>
		<FirstName></FirstName>
		<Race>セキレイ</Race>
		<Sex></Sex>
		<Title>セキレイ</Title>
	</Character>
	<Character>
		<FirstName>草野</FirstName>
		<Race>セキレイ</Race>
		<Sex></Sex>
		<Title>セキレイ</Title>
	</Character>
	<Character>
		<FirstName></FirstName>
		<Race>セキレイ</Race>
		<Sex></Sex>
		<Title>セキレイ</Title>
	</Character>
	<Character>
		<FirstName>月海</FirstName>
		<Race>セキレイ</Race>
		<Sex></Sex>
		<Title>セキレイ</Title>
	</Character>
	<Character>
		<FirstName>風花</FirstName>
		<Race>セキレイ</Race>
		<Sex></Sex>
		<Title>セキレイ</Title>
	</Character>
	<Character>
		<FirstName></FirstName>
		<Race>セキレイ</Race>
		<Sex></Sex>
		<Title>セキレイ</Title>
	</Character>
</Characters>
"sample1.xsl"
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
	<xsl:output method="html"
		encoding="UTF-8"
		indent="yes"
		doctype-public="-//W3C//DTD HTML 4.01 Transitional//EN"
		doctype-system="http://www.w3.org/TR/html4/loose.dtd" />

	<xsl:variable name="range" select="4"/>

	<xsl:template match="/">
		<html>
			<head>
				<title>いろいろな作品の登場人物</title>
				<style>
					<![CDATA[div { margin-bottom: 8px; }
					table { margin: 0px; padding: 0px; border-collapse: collapse; }
					tr { margin: 0px; padding: 0px; }
					td { margin: 0px; padding: 0px; width: 180px; height: 180px; border: solid 1px black; }
					td div { margin: 0px; padding: 4px; }
					td dl { margin: 0; }
					td dl dt { margin: 0 0 2px 0; width: 5em; float:left; }
					td dl dd { margin: 0 0 2px 5em; border-bottom: solid 1px black; }
					td.pattern1 { background: #FFFFFF;}
					td.pattern2 { background: #DDDDFF;}]]>
				</style>
			</head>
			<body>
				<xsl:apply-templates />
			</body>
		</html>
	</xsl:template>

	<xsl:template match="Characters">
		<xsl:for-each select="Character[(position() - 1) mod $range = 0]">
			<div>
				<table>
					<tr>
						<!-- カレントノードと、カレントノードに続く兄弟ノード。 -->
						<xsl:apply-templates select=".|following-sibling::Character[position() &lt; $range]" />
						<!-- テーブル内のセル数が $range になるようにする。足りない文、空のセルを追加する。 -->
						<xsl:call-template name="emptyCell">
							<!-- 0〜($range-1) -->
							<xsl:with-param name="restNodeCount" select="$range - count(following-sibling::Character[position() &lt; $range]) - 1" />
						</xsl:call-template>
					</tr>
				</table>
			</div>
		</xsl:for-each>
	</xsl:template>

	<!-- Character 要素をテーブルのセルに。 -->
	<!-- 最大 $range 個の Character 要素のノードセットを処理する。 -->
	<xsl:template match="Character">
		<td>
			<xsl:attribute name="class">
			<xsl:choose>
				<xsl:when test="position() mod 2 = 1">pattern1</xsl:when>
				<xsl:otherwise>pattern2</xsl:otherwise>
			</xsl:choose>
			</xsl:attribute>
			<div>
				<dl>
					<dt>氏名</dt>
					<dd><xsl:if test="count(FamilyName)>0"><xsl:value-of select="FamilyName" /><xsl:text> </xsl:text></xsl:if><xsl:value-of select="FirstName" /></dd>
					<dt>種族</dt>
					<dd><xsl:value-of select="Race" /></dd>
					<dt>性別</dt>
					<dd><xsl:value-of select="Sex" /></dd>
					<dt>作品</dt>
					<dd><xsl:value-of select="Title" /></dd>
				</dl>
			</div>
		</td>
	</xsl:template>

	<!-- テーブルのセル数を $range の数にあわせるために、空のセルを追加する。 -->
	<xsl:template name="emptyCell">
		<xsl:param name="restNodeCount" />

		<xsl:if test="$restNodeCount &gt;= 1">
			<td>
				<xsl:attribute name="class">
				<xsl:choose>
					<xsl:when test="$restNodeCount mod 2 = ($range mod 2)">pattern1</xsl:when>
					<xsl:otherwise>pattern2</xsl:otherwise>
				</xsl:choose>
				</xsl:attribute>
				<div></div>
			</td>

			<!-- 再帰呼び出し。 -->
			<xsl:call-template name="emptyCell">
				<xsl:with-param name="restNodeCount" select="$restNodeCount - 1" />
			</xsl:call-template>
		</xsl:if>
	</xsl:template>
</xsl:stylesheet>

"sample1.xsl"を処理するPowerShellスクリプト

try {
	$xslPath = ".\sample1.xsl";
	$xmlPath = ".\sample.xml";
	$htmlPath = ".\sample1result_a.html";

	$xslt = new-object System.Xml.Xsl.XslCompiledTransform;
	$xslt.Load($xslPath);
	$xslt.Transform($xmlPath, $htmlPath);
	write "無事終了した。";
	exit 0;
} catch {
	write-error $_;
	exit -1;
}

"sample1.xsl"をSAXONで処理するコマンド。SAXON-HEのjarファイルの位置は c:\opt\saxon\saxon9he.jar と想定している。

java -cp c:\opt\saxon\saxon9he.jar net.sf.saxon.Transform -t -strip:all "-s:.\sample.xml" "-xsl:.\sample1.xsl" "-o:.\sample1result_b.html"
"sample2.xsl"
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
	<xsl:output method="html"
		encoding="UTF-8"
		indent="yes"
		doctype-public="-//W3C//DTD HTML 4.01 Transitional//EN"
		doctype-system="http://www.w3.org/TR/html4/loose.dtd" />

	<xsl:variable name="rowSize" select="2" />
	<xsl:variable name="columnSize" select="3" />
	<xsl:variable name="range" select="$rowSize * $columnSize" />

	<xsl:template match="/">
		<html>
			<head>
				<title>いろいろな作品の登場人物</title>
				<style>
					<![CDATA[div { margin-bottom: 8px; }
					table { margin: 0px; padding: 0px; border-collapse: collapse; }
					tr { margin: 0px; padding: 0px; }
					td { margin: 0px; padding: 0px; width: 180px; height: 180px; border: solid 1px black; }
					td div { margin: 0px; padding: 4px; }
					td dl { margin: 0; }
					td dl dt { margin: 0 0 2px 0; width: 5em; float:left; }
					td dl dd { margin: 0 0 2px 5em; border-bottom: solid 1px black; }
					td.pattern1 { background: #FFFFFF;}
					td.pattern2 { background: #DDDDFF;}]]>
				</style>
			</head>
			<body>
				<xsl:apply-templates />
			</body>
		</html>
	</xsl:template>

	<xsl:template match="Characters">
		<!-- 表。rowSize x columnSize -->
		<xsl:for-each select="Character[(position() - 1) mod $range = 0]">
			<div>
				<table>
					<!-- 行。columnSize -->
					<xsl:for-each select=".|following-sibling::Character[(position() &lt; $range) and (position() mod $columnSize = 0)]">
						<tr>
							<!-- カレントノードと、カレントノードに続く兄弟ノード。 -->
							<xsl:apply-templates select=".|following-sibling::Character[position() &lt; $columnSize]">
								<xsl:with-param name="rowNum" select="position()" />
							</xsl:apply-templates>
							<!-- テーブル内のセル数が $columnSize になるようにする。足りない文、空のセルを追加する。 -->
							<xsl:call-template name="emptyCell">
								<!-- 0〜$columnSize -->
								<xsl:with-param name="rowNum" select="position()" />
								<xsl:with-param name="restNodeCount" select="$columnSize - count(following-sibling::Character[position() &lt; $columnSize]) - 1" />
							</xsl:call-template>
						</tr>
					</xsl:for-each>
					<!-- 必要なら数あわせの行を追加する。 -->
					<xsl:call-template name="emptyRow">
						<!-- 0〜($rowSize - 1) -->
						<xsl:with-param name="restRowCount" select="$rowSize - ceiling((count(following-sibling::Character[position() &lt; $range]) + 1) div $columnSize)" />
					</xsl:call-template>
				</table>
			</div>
		</xsl:for-each>
	</xsl:template>

	<!-- Character 要素をテーブルのセルに。 -->
	<!-- 最大 $columnSize 個の Character 要素のノードセットを処理する。 -->
	<xsl:template match="Character">
		<xsl:param name="rowNum" />

		<xsl:comment><xsl:value-of select="$rowNum"/></xsl:comment>
		<td>
			<xsl:attribute name="class">
			<xsl:choose>
				<xsl:when test="position() mod 2 = ($rowNum mod 2)">pattern1</xsl:when>
				<xsl:otherwise>pattern2</xsl:otherwise>
			</xsl:choose>
			</xsl:attribute>
			<div>
				<dl>
					<dt>氏名</dt>
					<dd><xsl:if test="count(FamilyName)>0"><xsl:value-of select="FamilyName" /><xsl:text> </xsl:text></xsl:if><xsl:value-of select="FirstName" /></dd>
					<dt>種族</dt>
					<dd><xsl:value-of select="Race" /></dd>
					<dt>性別</dt>
					<dd><xsl:value-of select="Sex" /></dd>
					<dt>作品</dt>
					<dd><xsl:value-of select="Title" /></dd>
				</dl>
			</div>
		</td>
	</xsl:template>

	<!-- テーブルの列数を $rowSize の数にあわせるために、空の行を追加する。 -->
	<xsl:template name="emptyRow">
		<xsl:param name="restRowCount" />

		<xsl:if test="$restRowCount &gt;= 1">
			<tr>
				<xsl:call-template name="emptyCell">
					<xsl:with-param name="rowNum" select="$rowSize - $restRowCount + 1" />
					<xsl:with-param name="restNodeCount" select="$columnSize" />
				</xsl:call-template>
			</tr>

			<!-- 再帰呼び出し。 -->
			<xsl:call-template name="emptyRow">
				<xsl:with-param name="restRowCount" select="$restRowCount - 1" />
			</xsl:call-template>
		</xsl:if>
	</xsl:template>

	<!-- テーブルのセル数を $columnSize の数にあわせるために、空のセルを追加する。 -->
	<xsl:template name="emptyCell">
		<xsl:param name="rowNum" />
		<xsl:param name="restNodeCount" />

		<xsl:if test="$restNodeCount &gt;= 1">
			<td>
				<xsl:attribute name="class">
				<xsl:choose>
					<xsl:when test="$restNodeCount mod 2 = (((($rowNum - 1) mod 2) + $columnSize) mod 2)">pattern1</xsl:when>
					<xsl:otherwise>pattern2</xsl:otherwise>
				</xsl:choose>
				</xsl:attribute>
				<div></div>
			</td>

			<!-- 再帰呼び出し。 -->
			<xsl:call-template name="emptyCell">
				<xsl:with-param name="rowNum" select="$rowNum" />
				<xsl:with-param name="restNodeCount" select="$restNodeCount - 1" />
			</xsl:call-template>
		</xsl:if>
	</xsl:template>
</xsl:stylesheet>

"sample2.xsl"を処理するPowerShellスクリプト

try {
	$xslPath = ".\sample2.xsl";
	$xmlPath = ".\sample.xml";
	$htmlPath = ".\sample2result_a.html";

	$xslt = new-object System.Xml.Xsl.XslCompiledTransform;
	$xslt.Load($xslPath);
	$xslt.Transform($xmlPath, $htmlPath);
	write "無事終了した。";
	exit 0;
} catch {
	write-error $_;
	exit -1;
}

"sample2.xsl"をSAXONで処理するコマンド。SAXON-HEのjarファイルの位置は c:\opt\saxon\saxon9he.jar と想定している。

java -cp c:\opt\saxon\saxon9he.jar net.sf.saxon.Transform -t -strip:all "-s:.\sample.xml" "-xsl:.\sample2.xsl" "-o:.\sample2result_b.html"

Illustrator と svg 要素の viewBox 属性

本記事中に出てくるアプリケーションのヴァージョン。

Illustrator
Illustrator CS2/CS4。
Firefox
3.6.12。
Apache Batik
1.7。

svg 要素の viewBox 属性で、ユーザー単位を設定できるわけだけれども、レンダラーによっておかしな振る舞いをすることがある。
IllustratorSVG ファイルを配置すると線幅の解釈を間違う。stroke-width に単位をつけなかったら*1、 viewBox を考慮せず、point として扱う。

下の SVG データはユーザー単位として"mm"になるように viewBox 属性を設定している。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="50mm" height="50mm" viewBox="0 0 50 50">
	<!-- 図01 -->
	<rect x="0" y="0" width="50" height="50" fill="#B8D8EF"/>
	<rect x="10" y="10" width="30" height="30" stroke="#000000" stroke-width="4" fill="#FFFFFF"/>
</svg>

Firefox など*2で上の内容の SVG ファイルを開くと二つ目の rect 要素の線幅を 4mm として扱ってくれる。Illustrator だと 4pt として扱う……。x 属性や y 属性、width 属性、height 属性などは問題ない。stroke-width 属性だけがおかしい*3Illustrator はインチやポイント(とくにこっち?)を基本にして動作しているんだろう。だから、"mm"という単位への対応が弱く、ポイントで処理してしまう箇所があるのだろうか?

上の SVG データを "mm" 単位から "pt" 単位にしてみたものが、下の SVG データである。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="141.7322834645669pt" height="141.7322834645669pt" viewBox="0 0 141.7322834645669 141.7322834645669">
	<!-- 図02 -->
	<!-- 50mm ≒ 141.7322834645669pt、10mm ≒ 28.34645669291339pt、30mm ≒ 85.03937007874016pt、4mm ≒ 11.33858267716536pt。 -->
	<rect x="0" y="0" width="141.7322834645669" height="141.7322834645669" fill="#B8D8EF"/>
	<rect x="28.34645669291339" y="28.34645669291339" width="85.03937007874016" height="85.03937007874016" stroke="#000000" stroke-width="11.33858267716536" fill="#FFFFFF"/>
</svg>

こちらは、まあ、望むとおりの図が Illustrator に配置される。Illustrator で利用したい SVG ファイルはポイント単位で作ってやる必要がありそうだ*4
つか、Illustrator がはき出す SVG ファイル見てみたら、単位ついてない。んで、その数値と Illustrator 上で描画される図の、線の長さだの幅だの見てると、SVG ファイル中の数値は全部ポイント換算だな……。Adobe のテックノートに [SVG ファイル取込時に [ラスター品質] の dpi の値を大きくすると画像が小さく取り込まれる] なんてのもあった。Illustrator 10 以降、svg 要素の width、height 属性は数値だけ、"pt"と単位をつけてはき出さなくなったようだ……。そりゃ、FrameMaker も "px" 単位で読み込む。その仕様で正しいもの。んで、"px"単位で図を扱ってるんだから、dpi高めたら図は小さくなるよね……。別にIllustratorが扱う SVG ファイルがポイント単位以外考えて無くてもいい。いいけど、単位をなんも出力しないってのは他のアプリケーションが困るから(実際、自社のFremaMaker使っている人が困っている)、それはやめなよ……。ほんとに、Illustrator 向け SVG ファイルの単位は、ポイント以外は考えないほうがよさそうだ。

もひとつ実験してみる。Illustrator だと下をポイント単位で読み込む(図02と図03が同じになる)。ほかのレンダラーだと"px"単位で読み込む(図02と図03が別物になる)。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="141.7322834645669" height="141.7322834645669" viewBox="0 0 141.7322834645669 141.7322834645669">
	<!-- 図03 -->
	<!-- 50mm ≒ 141.7322834645669pt、10mm ≒ 28.34645669291339pt、30mm ≒ 85.03937007874016pt、4mm ≒ 11.33858267716536pt。 -->
	<rect x="0" y="0" width="141.7322834645669" height="141.7322834645669" fill="#B8D8EF"/>
	<rect x="28.34645669291339" y="28.34645669291339" width="85.03937007874016" height="85.03937007874016" stroke="#000000" stroke-width="11.33858267716536" fill="#FFFFFF"/>
</svg>

*1:viewBox 属性でユーザー単位を指定しているときに stroke-width に単位をつけると、当然のことながら、けったいなことになるので、ユーザー単位を指定しているときには、ふつうはつけない(はず)。

*2:ほかには Apache Batik でPDFとして出力して確認してみた。

*3:もっと調べれば、他にもおかしなものは見つかるかもしれない。自分の見た範囲では、stroke-width の指定を正しく扱えなかった。

*4:続く説明でもいろいろ書いてるけど、Illustrator では、SVG ファイルはポイント単位以外を考えるのはよしたほうがよさそう。

QueryDosDevice()の第1引数NULL

QueryDosDevice() の第1引数に NULL を与えると、既存の MS-DOSバイス名をすべて得られる。

下は MSDN より引用したもの。

このパラメータには、NULL を指定することができます。その場合、QueryDosDevice 関数は既存の MS-DOSバイス名をすべて列挙したリストを、lpTargetPath パラメータが示すバッファに格納します。

[QueryDosDevice 関数]

んで、やってみた(ワイド文字版を使った)。失敗。GetLastError() で確認すると ERROR_INSUFFICIENT_BUFFER が返ってきている。情報の書き込み先配列の大きさは 10K にしてたのだけど……。
そんなに情報書くの?とか思いつつも、配列サイズを64Kにしてみたら、成功したよ。なんか、書き込まれてた文字数調べたら、1万字以上あった。
文字列を表示してみると、NT namespaceの"Global??"以下のものやらなにやら(あまり細かいことはわからん)が、ごっそりと列挙されている。……それで1万字以上書くわけか。
QueryDosDevice()って"C:"とか"COM1"とかそんなのしか渡せないと思ってたけど、ちがうのだな。"Volume{*GUID*}"みたいな文字列も渡せる(*GUID*の部分は、7eebeecd-f371-46e9-83f6-a09178edb56c みたいな文字列ね。)。

ためしに"C:"に割り当てられているボリュームを表す"Volume{*GUID*}"の文字列を渡したら、"C:"にマッピングされているデバイス名が返ってきた。

wchar_t wbuf[1024];
QueryDosDeviceW(L"Volume{*GUID*}", wbuf, 1024);

と、

wchar_t wbuf[1024];
QueryDosDeviceW(L"C:", wbuf, 1024);

の結果が同じもの*1

参考ページ

//msdn.microsoft.com/ja-jp/library/cc429649.aspx" title="QueryDosDevice 関数" target="_blank">[QueryDosDevice 関数]:QueryDosDevice()のヘルプ情報。MSDN内ページ。
//msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx" title="Naming Files, Paths, and Namespaces (Windows)" target="_blank">[Naming Files, Paths, and Namespaces (Windows)]:ファイルパスだのNT Namespaceだのなんだの。自分もあまりよくわかってない。きちんと読んでおきたい。
//msdn.microsoft.com/en-us/library/cc542456(v=VS.85).aspx" title="Displaying Volume Paths (Windows)" target="_blank">[Displaying Volume Paths (Windows)]:QueryDosDevice() が使われている。QueryDosDevice() の動作を知るための最初のサンプルにするには、ちょっと複雑だけど、いいサンプルだと思う。

*1:"*GUID*"の部分はシステムによって異なるだろうから、[WinObj]なりで調べてみてください。

Unicodeパス対応版 "高速 sha1sum / md5sum"

高速 sha1sum / md5sumで公開されているソフトウェアを、Unicodeパスを扱うように(逆にSJISでのパスは扱えなくなった)修正したものを公開した。
Unicodeパス対応 sha1sum / md5sum

PowerShell と ZipPackage

本記事を試した環境。

Windows
Windows 7 x64/Windows Server 2008 x64/Windows XP x86
PowerShell
PowerShell 2.0
.NET Framework
3.5 SP1以降がインストールされている
Visual Studio
Visual Studio 2005

PowerShell の対話型インターフェイスの AppDomain は情報が不完全なようで*1、そのために一部処理が失敗することがある。その一つが ZipPackage 関連クラスのストリーム処理。下に成功するサンプルと失敗するサンプルを書く。

まずは成功するもの。なお、OPC準拠のファイルを作るためにある ZipPackage を、下のように単なるzip書庫を作るために使うのは NG だと思うが、単純なサンプルということで、目をつぶって欲しい。

[VOID][System.Reflection.Assembly]::Load("WindowsBase, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

$s = "実験用文字列";

$package = [System.IO.Packaging.ZipPackage]::Open("test01.zip", [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite);

$packagePart = $package.CreatePart("/hoge.txt", [System.Net.Mime.MediaTypeNames+Text]::Plain);
$stream = $packagePart.GetStream();
$writer = new-object System.IO.StreamWriter($stream, [System.Text.Encoding]::Unicode);
$writer.WriteLine($s);
$writer.Close();
$stream.Close();

$package.Flush();
$package.Close();

下は失敗する。サンプルとしては、成功したら上よりもかなり大きなファイルが作られるもの。成功する環境もあるかもしれないけど、ZipPackagePart オブジェクト($packagePart)のストリームに流し込むデータを増やせば失敗すると思う。

[VOID][System.Reflection.Assembly]::Load("WindowsBase, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

$s = "実験用文字列";

$package = [System.IO.Packaging.ZipPackage]::Open("test02.zip", [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite);

$packagePart = $package.CreatePart("/hoge.txt", [System.Net.Mime.MediaTypeNames+Text]::Plain);
$stream = $packagePart.GetStream();
$writer = new-object System.IO.StreamWriter($stream, [System.Text.Encoding]::Unicode);
for($i = 0; $i -lt 1000000; $i++) {
	try {
		$writer.WriteLine($s);
	} catch {
		# $_                          : System.Management.Automation.ErrorRecord
		# $_.Exception                : System.Management.Automation.MethodInvocationException
		# $_.Exception.InnerException : System.IO.IsolatedStorage.IsolatedStorageException
		write-error ("{0}, {1}, {2}" -f $_.Exception.GetType().ToString(), $_.Exception.InnerException.GetType().ToString(), $_.Exception.InnerException.ToString());
		break;
	}
}
$writer.Close();
$stream.Close();

$package.Flush();
$package.Close();

IsolatedStorageException なんてのが投げられて失敗する。分離ストレージを内部で使っているということなんだろうけど、詳細はわからない。
とりあえず、AppDomainの情報がそろっていれば上のスクリプトは成功する。
たとえば、C#なんかで、System.Management.Automation.RunspaceInvoke.Invoke() を使って、スクリプトを実行するコマンドとか作る。コマンドの AppDomain には ZipPackage が必要とする分離ストレージの操作において必要な情報がそろっていて、スクリプトの実行に成功する。非常にベタなサンプルを下に書く。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Management.Automation;

namespace TestInvoke
{
    class Program
    {
        static void Main(string[] args)
        {
            string script = @"
[VOID][System.Reflection.Assembly]::Load(""WindowsBase, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"");

$s = ""実験用文字列"";

$package = [System.IO.Packaging.ZipPackage]::Open(""test02.zip"", [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite);

$packagePart = $package.CreatePart(""/hoge.txt"", [System.Net.Mime.MediaTypeNames+Text]::Plain);
$stream = $packagePart.GetStream();
$writer = new-object System.IO.StreamWriter($stream, [System.Text.Encoding]::Unicode);
for($i = 0; $i -lt 1000000; $i++) {
	try {
		$writer.WriteLine($s);
	} catch {
		# $_                          : System.Management.Automation.ErrorRecord
		# $_.Exception                : System.Management.Automation.MethodInvocationException
		# $_.Exception.InnerException : System.IO.IsolatedStorage.IsolatedStorageException
		write-error (""{0}, {1}, {2}"" -f $_.Exception.GetType().ToString(), $_.Exception.InnerException.GetType().ToString(), $_.Exception.InnerException.ToString());
		break;
	}
}
$writer.Close();
$stream.Close();

$package.Flush();
$package.Close();
";

            //script = "write 1; write-error 2; write 3; write-error 4;"; // RunspaceInvoke.Invoke() の返値がどういうものになるかを簡単に確認する。

            RunspaceInvoke invoke = new RunspaceInvoke();
            System.Collections.IList errors;
            Collection results = invoke.Invoke(script, null, out errors);
            Console.WriteLine(">Results");
            foreach (PSObject r in results)
            {
                Console.WriteLine(r);
            }
            // スクリプトが write-error に書き込んだオブジェクトを出力する。
            Console.WriteLine(">Errors");
            foreach (PSObject e in errors)
            {
                Console.Error.WriteLine(e);
            }
        }
    }
}

なお、RunspaceInvoke.Invoke() メソッドでスクリプトを実行する場合、プロファイル("Microsoft.PowerShell_profile.ps1"とか)を事前に読み込まない。プロファイルのなかで定義している関数やエイリアスを利用する場合は、実行するスクリプト内でプロファイルを読み込む処理を書いておく必要がある。

参考にさせてもらったページ

//ufcpp.net/study/powershell/interop.html" title="C# 上で PowerShell スクリプトを実行 (Windows PowerShell)" target="_blank">[C# 上で PowerShell スクリプトを実行 (Windows PowerShell)]:Visual Studio 2008 でのサンプルを紹介している。LINQなど2008のC# 3.0じゃないとダメなものを使っているサンプルを除けば(var なんかは単純なものなら object に変えてしまえる)、修正すれば2005でもだいたい通るのじゃないだろうか? 自分に必要なものだけ試したから、どれぐらい2005で使えるかはわからないけど。
//csharper.blog57.fc2.com/blog-entry-55.html" title="C#と諸々 コマンドレットの作成方法" target="_blank">[C#と諸々 コマンドレットの作成方法]:"System.Management.Automation.dll"をVisual Studioから参照する方法。

*1:AppDomain の詳細をあまりわかっていないため、煮え切らない書き方をしている箇所がたくさんあるかと思う。

アセンブリのロード

自分メモ。

グローバルアセンブリ

[VOID][System.Reflection.Assembly]::Load("System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");

アセンブリ"System.Web"のロードは、AssemblyName を使って下のようにも書ける。書く量が増えて面倒くさいけど。

$asmName = new-object System.Reflection.AssemblyName;
$asmName.Name = 'System.Web';
$asmName.Version = new-object System.Version(2, 0, 0, 0);
$asmName.CultureInfo = new-object System.Globalization.CultureInfo("");
$asmName.SetPublicKeyToken(@([byte]0xb0, [byte]0x3f, [byte]0x5f, [byte]0x7f, [byte]0x11, [byte]0xd5, [byte]0x0a, [byte]0x3a));
[VOID][System.Reflection.Assembly]::Load($asmName);

プライベートアセンブリ

System.Reflection.Assembly.Load() を使う場合は、文字列を引数にとるLoad()に、アセンブリのパスを直接わたすとエラーになる。Load()を使う場合は、AssemblyNameを使わないといけないくさい。

$asmName = [System.Reflection.AssemblyName]::GetAssemblyName('アセンブリのパス');
[VOID][System.Reflection.Assembly]::Load($asmName);

System.Reflection.Assembly.LoadFile() だったら、そのままアセンブリのパスを渡せばよい。

[VOID][System.Reflection.Assembly]::LoadFile('アセンブリのパス');

System.Web.HttpUtility.UrlEncode() を使う

自分メモ。
アセンブリ"System.Web"をロードしないと System.Web.HttpUtility.UrlEncode() を使えないので、最初にアセンブリをロードしている。

[VOID][System.Reflection.Assembly]::Load("System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");

("`"{0}`"" -f [System.Web.HttpUtility]::UrlEncode([System.Text.Encoding]::UTF8.GetBytes('山田(仮)')));

上のコードは Windows 7 x64/Windows XP SP3(x86)、PowerShell 2.0 で試した。