PowerShellからRTF(RichTextFormat)を操作して装飾した文字列をコピーする
環境:
> $psversiontable
Name Value
---- -----
PSVersion 7.4.5
PSEdition Core
GitCommitId 7.4.5
OS Microsoft Windows 10.0.22631
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
実演
Add-Type -AssemblyName "System.Windows.Forms"
$rtf = "{\rtf{\colortbl;\red0\green0\blue0;\red255\green255\blue0;}これによって{\cf1\i\b\highlight2 色つき}になります。}"
[System.Windows.Forms.Clipboard]::SetText($rtf, [System.Windows.Forms.TextDataFormat]::Rtf)
上記のコードを実行して、
> cat .\copy.ps1
Add-Type -AssemblyName "System.Windows.Forms"
$rtf = "{\rtf{\colortbl;\red0\green0\blue0;\red255\green255\blue0;}これによって{\cf1\i\b\highlight2 色つき}になります。}"
[System.Windows.Forms.Clipboard]::SetText($rtf, [System.Windows.Forms.TextDataFormat]::Rtf)
> .\copy.ps1
それからWordを開いて「貼り付け」操作をすると 色つき
の部分に黄色のハイライトが入って書体もイタリックボールドになります。
何が起きているのか
RTF(RichTextFormat)形式でデータをクリップボードに格納しています。
RTF構造
上記のコード内のRTFに改行とインデントを追加してみます。
{\rtf
{\colortbl;
\red0\green0\blue0;
\red255\green255\blue0;
}
これによって{\cf1\i\b\highlight2 色つき}になります。
}
ここで重要なのが \colortbl
。
事前にカラーテーブルを決めておいて、以降のマークアップではテーブル内のインデックスで色を指定します(フォントなども同様に \fonttbl
を使う)。
上記では、
-
\cf1
で文字色(1番目の黒色) -
\highlight2
でマーカーの色(2番目の黄色)
を決めています。
インデックスが1始まりのように見えますが、0
が「自動」を意味するようです。
\colortbl
は1種類だけ
注意点①:指定できる {\rtf
{\colortbl;
\red0\green0\blue0;
\red255\green255\blue0;
}
これによって{\cf1\i\b\highlight2 色つき}になります。
{\colortbl;
\red0\green0\blue0;
\red0\green255\blue255;
}
2つめの{\cf1\i\b\highlight2 色}はどうなるのでしょうか。
}
複数の \colortbl
宣言がある場合は最初に登場したものが利用されるようです。
装飾箇所ごとに宣言するのではなく、最初に使う色はテーブルとして宣言しておき、インデックスで指定します。
注意点②:非ASCII文字の扱い
{\rtf
{\colortbl;
\red0\green0\blue0;
\red255\green255\blue0;
}
これによって{\cf1\i\b\highlight2 色つき}になりま〰す。
}
〰
(U+3030)を入れてみると ?
に文字化けします。どうやらShift-JISの範囲外はそのままでは表現できない様子。
{\rtf
{\colortbl;
\red0\green0\blue0;
\red255\green255\blue0;
}
これによって{\cf1\i\b\highlight2 色つき}になりま\u12336?す。
}
\u12336?
のように文字コードを指定すれば解決します。
実践
> gcb
あい▲う▲えお
か▲き▲くけこ
さし▲すせ▲そ
> gcb | ConvertTo-RtfInsideSymbol | Set-ClipboardAsRtf
- テキストエディタを使って装飾したい部分を
▲
で囲む - コピーする
- PowerShellの
gcb
(Get-Clipboard
のエイリアス)を使って読み込む - ▲で囲まれた部分をイタリック&黄色マーカーにする
という一連の操作をコマンドにしてみました。
Wordでワイルドカードをゴニョゴニョする必要がなくなります。時短!
クラス定義
colortbl定義などのツールをクラスにまとめておきます。
`RtfUtil` クラス全体(長いので折りたたみ)
class RtfUtil {
RtfUtil() {}
static $table = [ordered]@{
"Black" = @(0, 0, 0);
"Blue" = @(0, 0, 255);
"Cyan" = @(0, 255, 255);
"Green" = @(0, 255, 0);
"Magenta" = @(255, 0, 255);
"Red" = @(255, 0, 0);
"Yellow" = @(255, 255, 0);
"White" = @(255, 255, 255);
"DarkBlue" = @(0, 0, 128);
"DarkCyan" = @(0, 128, 128);
"DarkGreen" = @(0, 128, 0);
"DarkMagenta" = @(128, 0, 128);
"DarkRed" = @(128, 0, 0);
"DarkYellow" = @(128, 128, 0);
"DarkGray" = @(128, 128, 128);
"LightGray" = @(192, 192, 192);
}
static [string] getColortbl() {
return [RtfUtil]::table.Values | ForEach-Object {
$rgb = $_ -as [array]
return ("\red{0}\green{1}\blue{2};" -f $rgb)
} | Join-String -Separator "" -OutputPrefix "{\colortbl;" -OutputSuffix "}"
}
static [int] getColorIndex([string]$colorName) {
$names = [RtfUtil]::table.Keys
if ($colorName -in $names) {
return $names.IndexOf($colorName) + 1
}
return $names.IndexOf("Yellow") + 1
}
static [string] escape([string]$s) {
return $s.GetEnumerator() | ForEach-Object {
$c = $_ -as [char]
return "\u{0}?" -f [System.Convert]::ToInt32($c)
} | Join-String -Separator ""
}
}
String型にメソッドを生やす
Update-TypeData
で自作メソッドを生やします。
Update-TypeData -TypeName "System.String" -Force -MemberType ScriptMethod -MemberName "ToRtfHighlight" -Value {
param([string]$color = "Yellow", [bool]$italic = $true, [bool]$bold = $false)
$colortbl = [rtfURtfUtil]::getColortbl()
$rtf = "\cf1"
$rtf += "\highlight{0}" -f [RtfUtil]::getColorIndex($color)
if ($italic) {
$rtf += "\i"
}
if ($bold) {
$rtf += "\b"
}
$rtf += " "
$t = "{" + $rtf + [RtfUtil]::escape($this) + "}"
return -join @("{", $colortbl, $t, "}")
}
用法:
> "あいう".ToRtfHighlight()
{{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;}{\cf1\highlight7\i \u12354?\u12356?\u12358?}}
RTFフォーマットでコピーするコマンドの定義
function Set-ClipboardAsRtf {
<#
.EXAMPLE
"aa" + "bb".ToRtfHighlight() + "cc" | Set-ClipboardAsRtf
#>
param (
[parameter(ValueFromPipeline = $true)][string]$inputLine
)
begin {
$lines = @()
}
process {
$lines += ("{" + $inputLine + "}")
}
end {
$rtf = $lines | Join-String -Separator "\par" -OutputPrefix "{\rtf\fs21" -OutputSuffix "}"
[System.Windows.Forms.Clipboard]::SetText($rtf, [System.Windows.Forms.TextDataFormat]::Rtf)
}
}
RTFではブレースの対応さえ一貫していればネストは影響しないようです({{{{{あ}}}}}
のようになっても普通に貼り付けられる)。
機械的にパイプ入力をブレースで囲んでみました。
メイン処理
Regex.Matches()
で正規表現マッチさせると Index
と Length
を取得できます。
単純にループで文字列結合しています。
function ConvertTo-RtfInsideSymbol {
param (
[parameter(ValueFromPipeline = $true)][string]$inputLine
,[string]$symbol = "▲"
)
begin {
$reg = [regex]::new("{0}.+?{0}" -f $symbol)
}
process {
$s = ""
$offset = 0
foreach($m in @($reg.Matches($inputLine))) {
$pre = $inputLine.Substring($offset, $m.Index - $offset)
$s += [RtfUtil]::escape($pre)
$deco = $m.Value.Trim($symbol).ToRtfHighlight("Yellow", $true, $false)
$s += $deco
$offset = $m.Index + $m.Length
}
if ($offset -lt $inputLine.Length) {
$rest = $inputLine.Substring($offset)
$s += [RtfUtil]::escape($rest)
}
$s | Write-Output
}
end {
}
}
Discussion