Author Archives: misohena

2021-09-06

Emacs用のSVG実装のカラーピッカー

先日からEmacsの中で動く作図ツールを作っています。

https://github.com/misohena/el-easydraw

その一環として今日はカラーピッカーを作りました。この手のソフトには必ずあるアレです。

Emacs上での先行事例はいくつかあるようでしたがSVGでの実装は見当たりませんでしたし、まぁ、自分で作りたいじゃないですか。こういうの作るの楽しいですし。

というわけで出来たのがこちら。

https://github.com/misohena/el-easydraw/blob/master/edraw-color-picker.el

一応ライブラリとして他で使い回すことを考えています。

応用としてとりあえず作ったコマンドがいくつか。

edraw-color-picker-read-colorread-colorの代わりを意識して作った色入力コマンドです。ミニバッファ内にカラーピッカーを配置してみました。文字からでもカラーピッカーからでも色を選択できます。

ミニバッファ内に表示されるカラーピッカー
図1: ミニバッファ内に表示されるカラーピッカー

edraw-color-picker-insert-colorは選択した色をそのままバッファに挿入するコマンドです。またedraw-color-picker-replace-color-at-pointは現在のポイントの位置にある色表記を読み取ってカラーピッカーで選択した色に置き換えるコマンドです。これらはバッファ内にインラインでカラーピッカーが表示されます。

インラインで表示されるカラーピッカー。
図2: インラインで表示されるカラーピッカー。

SVGは本当に簡単でこういう絵を出すのはサクッとすぐに出来ます。いや、本当は少し調査が必要だったのが二点。meshGradientの対応状況と色相バーの仕様です。こういう二次元のグラデーションで連想するのがメッシュ状の図形(いわゆるポリゴンの組み合わせ)で頂点に色を設定する仕組みです。3Dグラフィックスではおなじみですよね。SVGにも似たような仕組みがあったようなと調べてみたのですが、残念ながらEmacs(というかlibrsvg)では対応していないようですね(そもそもmeshGradient自体がSVG2から先送りされてたんですね)。幸い二枚のグラデーションを重ねれば良いとすぐに気がつけました。もう一点の色相バーですが、これ6色(赤、黄、緑、水、青、紫)を均等に並べて線形補間すればいいだけみたいですね。というわけでそれさえ分かればこういうカラーピッカーの絵自体はすぐに作れました。

画像内の各パーツは「領域」というオブジェクトで構成されています。指定に基づいて「領域」をレイアウト処理することで、いくらか柔軟に構成要素を増やしたり減らしたりできるようになっています。

領域にマウスイベントを配信(ディスパッチ)する仕組みを作り、それに反応して各部が動くようにしていきます。マウスイベントを受けて関連付けられた値が変わり、値の変更が通知されて表示が変わります。

各領域で選択した場所から最終的な色を求める方法ですが、色相バー(1次元)→彩度明度領域(2次元)→不透明度バー(1次元)の順に処理していきます。色相バーで選んだ色が彩度明度領域の右上の色になります。彩度明度領域は水平のグラデーション(左:白から右:色相選択色)の上に垂直グラデーション(上:透明黒から下:不透明黒)が重なっています。選んだ位置によって線形補間で水平、垂直の順で色を求めていきます。その色が今度は不透明度バーの上の色になって、最後に不透明度バーで選択した不透明度をその色に適用して完了です。仕組み自体はグラデーションの内容に依存していないので、色相、彩度、明度の軸を入れ替えても機能すると思いますし、1次元バーを緑、2次元領域を青と赤、などとしても大丈夫だと思います。(そのための切り替えUIを作るのが面倒くさかったのでやってませんが)

こうして出来たSVG画像はテキストプロパティまたはオーバーレイを使ってEmacs内に表示されます。というかEmacsではそれしか画像を表示する方法はありません(環境依存性の強い方法を除けば)。テキストプロパティでも良いのですが、オーバーレイの方がこういう用途では使いやすいでしょう。

オーバーレイにも画像を表示できる場所が三つあります。before-stringとdisplayとafter-stringです。displayはいいとして、before-stringとafter-stringですが、これはオーバーレイの前後に文字列を挿入するためのプロパティです。一見画像とは関係なさそうですが、この文字列にpropertize関数でdisplayテキストプロパティを適用するとオーバーレイの前後にさらに画像を表示することが可能になります。

これら色々ある表示方法を都合に合わせて選択できるように構造には柔軟性を持たせてあります。

先行事例を見てみると表示にフレームを使っているものがありました。普通のフレームを使っているものやchild frameという比較的最近のEmacsで使えるようになった仕組みを使っているものもありました。child frameを使うとフレーム内の好きな位置に重ねることが出来るのでポップアップで情報を出すパッケージで使われているようです。

とはいえフレームは扱いが難しく勉強が必要なので、とりあえずオーバーレイだけで出来る範囲で実装しています。

カラーピッカーはどこに表示すれば良いのでしょうか。だいたいポイント(やマウスカーソル)の近くですよね。ただ、ポイントの左や右にカラーピッカーのオーバーレイを挿入すると行が大きく膨らんで見づらくなってしまいますし、ウィンドウの左右からはみ出して見えなくなってしまうこともあります。となれば上の行か下の行に表示してはどうでしょう。上の行や下の行に適切な表示カラムが無いならどうすればいいでしょうか。幸いオーバーレイは行を新たに作ることが出来ます。before-string、display、after-stringの各プロパティにはそれぞれ文字列が指定できますが、いずれでも"\n"を入れることで改行が出来ます。例えば現在ポイントが20カラム目にあるとして、その真下付近に画像を表示するには、行末の改行文字にオーバーレイをかけて、before-stringを "\n " (←空白20個 - 画像の幅/2くらい)、displayに画像、after-stringに "\n" を指定してやればOKです。水平スクロールしている可能性があるのでwindow-hscrollの値を考慮するのを忘れずに。

また、ミニバッファに色を入力するようなシチュエーションでは画面の下の方にカラーピッカーを表示して欲しいです。いや、いっそのことミニバッファ内にカラーピッカーを入れたらどうでしょう。ここでもオーバーレイに改行させれば上をカラーピッカー、下を入力欄とすることが出来ます。ただし一つ注意点が。バッファの先頭にオーバーレイを入れたいわけですが、先頭の1文字にかけるとbefore-stringに画像を設定しなければならずその後に改行が入れられません。displayで改行すると1文字目が(改行に置き換わって)表示されなくなってしまいますし、after-stringで改行すると1文字目の後で改行されてしまいます。なので先頭の空の範囲((point-min)から(point-min)まで)にオーバーレイを設定します。注意点が二つ。evaporateプロパティがtだと空の範囲になった段階でオーバーレイが消えてしまいます。なのでevaporateは使わずちゃんとオーバーレイを管理する必要があります。もう一点は空の範囲のオーバーレイではdisplayプロパティが機能しないということ。before-stringとafter-stringは表示されるのですが、なぜかdisplayプロパティは空の範囲だと表示されません。なのでbefore-stringに画像を、after-stringに"\n"を指定することになります。(2021-09-07追記:よく考えたらbefore-stringの中で文字列の一部にだけdisplayプロパティをかけて、残りで改行すれば済むような気もします。未確認ですが、多分動くのではないかと)

他にもEmacsの色名とWeb(HTMLやCSS)の色名に対応するとか、色の表現形式とか、画像のスケーリングについてとか(上のスクリーンショット、ピッカーの大きさが違うのに気がつきましたか?)、色々書きたいことは山ほどありますが、この辺にしておきます。

(追記: Emacsのcss-modeやcustomize-face等でカラーピッカーを使う設定)

2021-08-29 ,

org-modeのアジェンダで土曜日と日本の祝日を色づけする

土曜日は青で、日曜と祝日は 'org-agenda-date-weekend のfaceで表示するには次のように設定します。

(setq org-agenda-day-face-function
      (lambda (date)
        (let ((face (cond
                     ;; 土曜日
                     ((= (calendar-day-of-week date) 6)
                      '(:inherit org-agenda-date :foreground "#0df"))
                     ;; 日曜日か日本の祝日
                     ((or (= (calendar-day-of-week date) 0)
                          (let ((calendar-holidays japanese-holidays))
                            (calendar-check-holidays date)))
                      'org-agenda-date-weekend)
                     ;; 普通の日
                     (t 'org-agenda-date))))
          ;; 今日は色を反転
          (if (org-agenda-today-p date) (list :inherit face :inverse-video t) face))))
土曜日が青、日曜日と祝日が赤で色づけされたorg-agenda
図1: 土曜日が青、日曜日と祝日が赤で色づけされたorg-agenda

素のcalendarパッケージでもそうなのですが土曜日が青色で表示されないんですよね。日本のカレンダーでは当たり前で、もはや青くないと違和感を感じるレベルなのですが、きっと海外では違うのでしょうね? 今ちょっと検索してみましたがいろんな文化があって面白そうです。

日付の色づけは org-agenda-day-face-function 変数で変更できます。

引数の date には (month day year) という形式のリストが渡されてきます。これはcalendarパッケージが日付を扱うときの形式に合わせているようです。

土曜日用に新しいfaceを定義するのも面倒なのでanonymousフェイスで返しています。フェイス属性:inherit は先頭にないとダメなようです。

祝日の判定は calendar-check-holidays 関数で行っています。ただ、私は calendar-holidays 変数に日本の祝日以外も入れてしまっているので、一時的に calendar-holidays 変数シンボルを japanese-holidays の示すリストに束縛してからチェックしています。

「今日」のfaceを変えたいのですが土、日祝、普通の三種類があるので今日か否かで変えるとなると組み合わせで六種類必要になってしまいます。なのでここでもanonymousフェイスを生成して :inherit が指す先を日によって変えることにしました。 :inherit にはシンボルだけでなくanonymousフェイスも指定できるようです。foregroundはそのままにbackgroundだけちょっと色を付けるといったくらいならこの方法が楽でしょう。全ての組み合わせを細かく調整したいのであればいっそ次のような感じの方が清々しいかもしれません。

(setq org-agenda-day-face-function
      (lambda (date)
        (pcase (+ (if (org-agenda-today-p date) 3 0)
                  (cond
                   ((= (calendar-day-of-week date) 6)
                    0)
                   ((or (= (calendar-day-of-week date) 0)
                        (let ((calendar-holidays japanese-holidays))
                          (calendar-check-holidays date)))
                    1)
                   (t
                    2)))
          (0 (list :foreground "#08a"))
          (1 (list :foreground "#a00"))
          (2 (list :foreground "#aaa"))
          (3 (list :foreground "#0cf"))
          (4 (list :foreground "#f00"))
          (5 (list :foreground "#fff")))))
2021-08-08 , , ,

Windowsのコマンドラインからスクリーンショットを撮る(PowerShell)

ちょっと欲しくなったので調べてみました。

How can I do a screen capture in Windows PowerShell? - Stack Overflow の Jacob Colvin さんの回答が一番簡潔だったのでそれを元にいくつか用意してみました。

まずはマルチモニターを含めた全領域。

# screenshot-all.ps1
# From https://stackoverflow.com/questions/2969321/how-can-i-do-a-screen-capture-in-windows-powershell
Add-Type -AssemblyName System.Windows.Forms,System.Drawing

$screens = [Windows.Forms.Screen]::AllScreens

$top    = ($screens.Bounds.Top    | Measure-Object -Minimum).Minimum
$left   = ($screens.Bounds.Left   | Measure-Object -Minimum).Minimum
$width  = ($screens.Bounds.Right  | Measure-Object -Maximum).Maximum
$height = ($screens.Bounds.Bottom | Measure-Object -Maximum).Maximum

$bounds   = [Drawing.Rectangle]::FromLTRB($left, $top, $width, $height)
$bmp      = New-Object System.Drawing.Bitmap ([int]$bounds.width), ([int]$bounds.height)
$graphics = [Drawing.Graphics]::FromImage($bmp)

$graphics.CopyFromScreen($bounds.Location, [Drawing.Point]::Empty, $bounds.size)

$bmp.Save($Args[0])

$graphics.Dispose()
$bmp.Dispose()

次にプライマリースクリーンのみ。

# screenshot-primary.ps1
# From https://stackoverflow.com/questions/2969321/how-can-i-do-a-screen-capture-in-windows-powershell
Add-Type -AssemblyName System.Windows.Forms,System.Drawing

$screen = [Windows.Forms.Screen]::PrimaryScreen

$top    = $screen.Bounds.Top
$left   = $screen.Bounds.Left
$right  = $screen.Bounds.Right
$bottom = $screen.Bounds.Bottom

$bounds   = [Drawing.Rectangle]::FromLTRB($left, $top, $right, $bottom)
$bmp      = New-Object System.Drawing.Bitmap ([int]$bounds.width), ([int]$bounds.height)
$graphics = [Drawing.Graphics]::FromImage($bmp)

$graphics.CopyFromScreen($bounds.Location, [Drawing.Point]::Empty, $bounds.size)

$bmp.Save($Args[0])

$graphics.Dispose()
$bmp.Dispose()

最後にアクティブウィンドウのみ。

# screenshot-activewin.ps1
# Get Foreground Window's Rect
Add-Type -Type @'
using System;
using System.Runtime.InteropServices;
namespace Win32Util {
    public struct RECT {
        public int Left, Top, Right, Bottom;
    }
    public class Utils {
        [DllImport("user32.dll")]
        public static extern IntPtr GetForegroundWindow();
        // [DllImport("user32.dll")]
        // public static extern int GetWindowRect(IntPtr hWnd, out RECT lpRect);
        [DllImport("dwmapi.dll")]
        public static extern int DwmGetWindowAttribute(IntPtr hWnd, uint dwAttribute, out RECT lpRect, int cbAttribute);

        public static uint DWMWA_EXTENDED_FRAME_BOUNDS = 0x09;

        public static RECT GetForegroundWindowRect(){
            IntPtr hwnd = GetForegroundWindow();
            // RECT rect = new RECT();
            // GetWindowRect(hwnd, out rect);
            RECT rect = new RECT();
            DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, out rect, Marshal.SizeOf(typeof(RECT)));

            return rect;
        }
    }
}
'@
$rect = [Win32Util.Utils]::GetForegroundWindowRect()

# https://stackoverflow.com/questions/2969321/how-can-i-do-a-screen-capture-in-windows-powershell
Add-Type -AssemblyName System.Windows.Forms,System.Drawing

$top    = $rect.Top
$left   = $rect.Left
$right  = $rect.Right
$bottom = $rect.Bottom

$bounds   = [Drawing.Rectangle]::FromLTRB($left, $top, $right, $bottom)
$bmp      = New-Object System.Drawing.Bitmap ([int]$bounds.width), ([int]$bounds.height)
$graphics = [Drawing.Graphics]::FromImage($bmp)

$graphics.CopyFromScreen($bounds.Location, [Drawing.Point]::Empty, $bounds.size)

$bmp.Save($Args[0])

$graphics.Dispose()
$bmp.Dispose()

実行は powershell screenshot-activewin.ps1 test.png のようにします。

それにしても久しぶりのWin32がまさかこんな形だとは。Windows10でGetWindowRectだと左右下に7ピクセルずつ余分に広い矩形を返してしまうだとか。影とかの効果の分でしょうか? なので今はDwmGetWindowAttributeを使うんだそうです。

あ、ちなみにEmacsのorg-downloadからを使うには次のように設定すればいいんです。使用頻度が少なそうなのでHydraが無いと覚えられる気がしませんね。まぁ、別に全部クリップボードからでもいいじゃんって思ったりもしますが。

(when (eq system-type 'windows-nt)
  (defun my-org-download-screenshot (script-name)
    (let ((org-download-screenshot-method
           (format
            "powershell %s %%s"
            (expand-file-name
             (format "<path-to-script-dir>/%s" script-name)))))
      (org-download-screenshot)))
  (defun my-org-download-screenshot-all ()
    (interactive)
    (my-org-download-screenshot "screenshot-all.ps1"))
  (defun my-org-download-screenshot-primary ()
    (interactive)
    (my-org-download-screenshot "screenshot-primary.ps1"))
  (defun my-org-download-screenshot-active-window ()
    (interactive)
    (my-org-download-screenshot "screenshot-activewin.ps1"))
  ;; Key bind help
  (when (featurep 'hydra)
    (define-key org-mode-map (kbd "C-c d")
      (defhydra my-org-download-hydra
        (:color red :exit t :hint nil)
        "
org-download copy from:
_c_: Clipboard
_y_: Full-path or URL on kill-ring
_a_: All monitors
_p_: Primary monitor
_f_: Foreground window
or drop from a local image file.
"
        ("c" org-download-clipboard)
        ("y" org-download-yank)
        ("a" my-org-download-screenshot-all)
        ("p" my-org-download-screenshot-primary)
        ("f" my-org-download-screenshot-active-window)))))