拡張テンパズル

実に久しぶりのエントリ。

テンパズルとは、1桁の数4つと加減乗除を組み合わせて10を作る、っていうパズル。「拡張」とついてるのはルールを拡張してるから。すなわち、

  • 計算式に使う数字は4個でなくても良い。2個以上なら良いものとする
  • 計算式に使う数字は二桁以上でも良い。要するに自然数なら良い
  • 計算結果は10でなくても良い

元ネタはこのブログのエントリ。

さらにその元ネタはQuizKnockのYouTubeチャンネルだそうだ。面白いな、これ。

さて、これを解くプログラムを作ってみた。上に挙げたブログでは Ruby で書いているので、Python で書いた。GitHubに上げてある。

実装上の工夫はあるものの基本的なアルゴリズムは元ネタのブログと同じだ。数と加減乗除からなる計算式を木構造として扱って、その値が10になる答えを探索する。答えは複数ある可能性もあるけど、最初に見つかった答えを返す。

今のところ数4つ、合計が10、に決め打ちで「拡張」にはなってない。とはいえ、解けるようには作ったのでプログラムにオプションを追加するだけでいい。

さて、実装上の工夫について書いておこう。

元ネタの Ruby のプログラムでは、ビルトインの Numeric クラスにvalueメソッドを追加しているけど、オープンクラスでない Python ではそういうことはできない。なので、数をラップするLeafクラスにvalueメソッドを定義した。内部では数をFraction(分数)として保持している。

もうひとつは、答えの木構造を、出力のために文字列に変換するところ。足し算・引き算を掛け算・割り算よりも優先するためにカッコが必要になるんだけど、この部分は元ネタのプログラムよりもすっきりと書けてると思う。

Rails6の最新版までアップグレード

ローカルネットワークで運用してる Rails アプリを Rails6 系の最新版 6.1.7.3 までアップグレードした。Ruby のバージョンは 3.1.4。Docker コンテナ上で動かしている。

以下、メモ。後で時間があればもう少し詳しく書く。

Rails 5.2 以降の credentials.yml.enc について。

Active Storage について。

Rails の設定について。

Python:Pillowでサムネイルの作成に失敗することがある

自宅サーバで運用してる Python で作った webアプリがあって、Pillow で画像のサムネイルを作るようになってるんだけど、ときどきサムネイルの作成に失敗していることがあるのに気がついた。

この webアプリを作ったときにどう考えたのか忘れたけど、もとの画像が png ならサムネイルも png、もとの画像が jpg ならサムネイルも jpg になる。失敗してるのは png のサムネイルを作るとき(の一部)のようだ。サムネイルをつくるのには Pillow の Image.thumbnail() 関数を使ってる。

で、検証のためのスクリプトを書いた。Python と Pillow のバージョンは次の通り。

  • Python 3.10.8
  • Pillow 9.5.0
from PIL import Image
from os import path
import argparse


THUMBNAIL_SIZE = (240, 240)


def main():
    args = parse_arguments()
    orig_file = args.orig
    im = Image.open(orig_file)
    im.thumbnail(THUMBNAIL_SIZE)

    (name, _) = path.splitext(orig_file)
    thumb_file = f'thumb_{name}.{args.format}'
    im.save(thumb_file)
    print(f'Thumbnail has maked successfully: {thumb_file}')


def parse_arguments():
    parser = argparse.ArgumentParser(
        description='Make thumbnail from original image.'
    )
    parser.add_argument(
        'orig',
        action='store',
        help='original image'
    )
    parser.add_argument(
        '-f', '--format',
        action='store',
        default='jpg',
        help='specify thumbnail file format (`jpg` to default)'
    )
    args = parser.parse_args()

    return args


main()

使い方は簡単。もと画像のファイルを引数にして実行すればサムネイルが作られる。--format オプションでサムネイルのフォーマットを指定できる(デフォルトは jpg)。

takatoh@sofa: Documents > python make_thumbnail.py --help
usage: make_thumbnail.py [-h] [-f FORMAT] orig

Make thumbnail from original image.

positional arguments:
  orig                  original image

options:
  -h, --help            show this help message and exit
  -f FORMAT, --format FORMAT
                        specify thumbnail file format (default to `jpg`)

さて、このスクリプトで、用意した png 画像のサムネイルを作ってみる。フォーマットに jpg を指定したときは期待通りにサムネイルが作成される。

takatoh@sofa: Documents > python make_thumbnail.py --format jpg sample1.png
Thumbnail has maked successfully: thumb_sample1.jpg

ところが、フォーマットに png を指定するとエラーになる。

takatoh@sofa: Documents > python make_thumbnail.py --format png sample1.png
Traceback (most recent call last):
  File "C:\Users\takatoh\Documents\make_thumbnail.py", line 42, in <module>
    main()
  File "C:\Users\takatoh\Documents\make_thumbnail.py", line 17, in main
    im.save(thumb_file)
  File "C:\Users\takatoh\AppData\Local\Programs\Python\Python310\lib\site-packages\PIL\Image.py", line 2432, in save
    save_handler(self, fp, filename)
  File "C:\Users\takatoh\AppData\Local\Programs\Python\Python310\lib\site-packages\PIL\PngImagePlugin.py", line 1318, in _save
    data = name + b"\0\0" + zlib.compress(icc)
TypeError: a bytes-like object is required, not 'str'

png のときはいつもエラーになるってわけでもない。下の例のように、ちゃんとサムネイルが作成されることもある。

takatoh@sofa: Documents > python make_thumbnail.py --format png sample2.png
Thumbnail has maked successfully: thumb_sample2.png

どうも、もとの png 画像によってエラーになることがあるらしい。多分まれなケース。ヒントはエラーメッセージに出ている。TypeError だ。bytes-like object でなければならないところで str が来ている。Pillow の PIL/PngImagePlugin.py ファイルの1318行目でエラーになってる。ソースファイルからこの付近を抜き出してみよう。

    icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
    if icc:
        # ICC profile
        # according to PNG spec, the iCCP chunk contains:
        # Profile name  1-79 bytes (character string)
        # Null separator        1 byte (null character)
        # Compression method    1 byte (0)
        # Compressed profile    n bytes (zlib with deflate compression)
        name = b"ICC Profile"
        data = name + b"\0\0" + zlib.compress(icc)
        chunk(fp, b"iCCP", data)

これは _save() 関数の一部で、下から2行目がファイルの1318行目にあたる。バイト文字列を連結して data 変数に代入してる。name 変数は上の行でバイト文字列のリテラルを代入してるし、b"\0\0" もバイト文字列のリテラルだ。てことは zlib.compress(icc) が怪しい。ちょっと話を端折るけど、icc 変数には im.info.get("icc_profile") で取得した値が入ってる。im は Pillow の Image オブジェクト。

そこで、手を動かしてこれを追いかけてみる。

takatoh@sofa: Documents > python
Python 3.10.8 (tags/v3.10.8:aaaf517, Oct 11 2022, 16:50:30) [MSC v.1933 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from PIL import Image
>>> import zlib
>>> im = Image.open('sample1.png')
>>> icc = im.info.get("icc_profile")
>>> name = b"ICC Profile"
>>> data = name + b"\0\0" + zlib.compress(icc)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: a bytes-like object is required, not 'str'

サムネイルを作るときと同じエラーが出た。もう少し粒度を細かくしてみよう。zlib.compress(icc) のとこだけ。

>>> icc_compressed = zlib.compress(icc)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: a bytes-like object is required, not 'str'

ああ、つまり iccbytes-like object じゃなくて str なわけだ。

>>> type(icc)
<class 'str'>

やっぱり。

エラーにならなかった sample2.png ファイルでも試してみよう。

>>> im2 = Image.open('sample2.png')
>>> icc2 = im2.info.get("icc_profile")
>>> type(icc2)
<class 'bytes'>

なるほど、ファイルによって im.info.get("icc_profile") で得られる値が bytes だったり str だったりするらしい。で、str だと zlib.compress() でエラーになる、と。

これは Pillow のバグなのか?それとも不適切な png ファイルのせいなのか?

これ以上追いかけるのはちょっと手に余るな。

Ruby:ランダムな文字列を作る

以前、Python で UUID をもとにしてランダムな文字列を作るってのをやった。

同じことを Ruby でやろうとしたら存外に手間を食ったのでメモしておく。

UUID は 標準ライブラリの SecureRandom モジュールで生成できる。

irb(main):001:0> require "securerandom"
=> true
irb(main):002:0> u = SecureRandom.uuid
=> "4ad31376-3c83-49d7-a70a-14467f2b4022"

が、Python では UUID クラスのインスタンスが返ってきてバイト列を得るのも簡単だったけど、Ruby では文字列が返ってくる。なので、16進数表示の文字列からバイト列に変換してやらないといけない。とりあえず邪魔なハイフンを取り除いておく。

irb(main):003:0> h = u.tr("-", "")
=> "4ad313763c8349d7a70a14467f2b4022"

で、バイト列に変換するメソッドを書く。

はじめは2文字ずつに切り分けて String#to_i で整数に変換してやればいいかと思ったけど、これだと得られるのは Fixnum クラスのインスタンス(の配列)であってバイト列じゃない。

結局次のようになった。2文字ずつ切り分けるのは同じだけど、切り出した2文字は別々に整数に変換して、1文字目のほうは 4bit 左シフトしてから2文字目のほうと論理OR をとる。そうすると整数16個の配列ができる。これをバイト列に変換するには Array#pack を使う。

irb(main):004:1* def hex2bstring(hex)
irb(main):005:1*   barray = []
irb(main):006:2*   hex.split("").each_slice(2) do |pair|
irb(main):007:2*     barray.push((pair[0].to_i(16) << 4) | (pair[1].to_i(16)))
irb(main):008:1*   end
irb(main):009:1*   barray.pack("C")
irb(main):010:0> end
=> :hex2bstring

このメソッドを使ってバイト列に変換。

irb(main):011:0> b = hex2bstring(h)
=> "J\xD3\x13v<\x83I\xD7\xA7\n\x14F\x7F+@\"" 

さらに Base32 エンコードするんだけど、Ruby の標準ライブラリには Base32 がないので、あらかじめ gem install base32 しておく必要がある。エンコードしたらパディングの = を取り除く。

irb(main):012:0> require "base32"
=> true
irb(main):013:0> s = Base32.encode(b)
=> "JLJRG5R4QNE5PJYKCRDH6K2AEI======"
irb(main):014:0> s2 = s.tr("=", "")
=> "JLJRG5R4QNE5PJYKCRDH6K2AEI"

最後は見た目のランダムさを増すために小文字を混ぜるようにする。if の条件式には Array#sample を使って [true, false].sample としてもよかったんだけど、せっかく UUID を生成するのに SecureRandom を使ってるのでここでも使ってみた。でもわかりにくいかも。

irb(main):015:1 def down(c)
irb(main):016:2*  if SecureRandom.random_number(2) > 0
irb(main):017:2*    c.downcase
irb(main):018:2*  else
irb(main):019:2*    c
irb(main):020:1*  end
irb(main):021:0> end
=> :down

最終的にはこうなった。

irb(main):022:0> s2.split("").map{|c| down(c) }.join
=> "JljRg5r4qnE5pjyKCRdH6k2aei"

Python より面倒だな。

Node.jsのバージョン管理にVoltaとfnmを試してみた

Node.jsのバージョン管理ツール多すぎ問題

Ruby なら rbenv、Python なら pyenv で決まり、みたいなバージョン管理ツールだけど、Node.js ではやたらと乱立していてどれを使うのがいいのかよくわからない。この問題自体はいろんなところで書かれていて、今回ググった中では↓このページがよくまとまっていた。

このページに挙げられているツールは以下の通り:

  • nvm
  • n
  • volta
  • asdf
  • nodenv
  • fnm
  • nvs
  • nodebrew

nvm、n、nodenv は前に試したことがある。ほかは聞いたことだけあったり、初めて知ったりだ。

いずれにせよ、JavaScript (Node.js)は Ruby や Python ほど使わないこともあって今までテキトーにしてたんだけど、静的サイトジェネレータの 11ty を試したこと(しかも Windows で)がきっかけで、少しまじめに考えてみようという気になった。なので Linux でも Windows でも使えるのが最低条件だ。

で、どれがいいかというと、上のページでは volta を薦めている。ほかには「Windows なら fnm がいい」というページがいくつか見つかった。そういうわけでこの2つを試してみることにする。どちらも Rust で書かれている。

Volta

公式サイトから Windows 用のインストーラをダウンロードする。

最新バージョンは1.0.8だ。ダウンロードしたら普通のアプリ同様にインストールすればいい。

takatoh@sofa: Documents > volta --version
1.0.8

ヘルプを見るとこんな感じ。

takatoh@sofa: Documents > volta --help
Volta 1.0.8
The JavaScript Launcher ⚡

    To install a tool in your toolchain, use `volta install`.
    To pin your project's runtime or package manager, use `volta pin`.

USAGE:
    volta.exe [FLAGS] [SUBCOMMAND]

FLAGS:
        --verbose
            Enables verbose diagnostics

        --quiet
            Prevents unnecessary output

    -v, --version
            Prints the current version of Volta

    -h, --help
            Prints help information


SUBCOMMANDS:
    fetch          Fetches a tool to the local machine
    install        Installs a tool in your toolchain
    uninstall      Uninstalls a tool from your toolchain
    pin            Pins your project's runtime or package manager
    list           Displays the current toolchain
    completions    Generates Volta completions
    which          Locates the actual binary that will be called by Volta
    setup          Enables Volta for the current user / shell
    run            Run a command with custom Node, npm, and/or Yarn versions
    help           Prints this message or the help of the given subcommand(s)

Node.js をインストールするには volta install

takatoh@sofa: Documents > volta install node@14
success: installed and set [email protected] (with [email protected]) as default

node@14 という書き方をしてるのは、Volta が Node.js だけでなく npm や yarn のバージョン管理にも対応してるから。ともかく Node 14 系の最新版がインストールされた。

takatoh@sofa: Documents > node -v
v14.20.0

Node 16 系もインストールしてみる。

takatoh@sofa: Documents > volta install node@16
success: installed and set [email protected] (with [email protected]) as default
takatoh@sofa: Documents > node -v
v16.17.0

volta list all でインストールされている Node.js のリストが表示される。

takatoh@sofa: Documents > volta list node
⚡️ Node runtimes in your toolchain:

    v14.20.0
    v16.17.0 (default)

プロジェクトごとにバージョンを分けることもできる。Volta では package.json に利用するバージョンが記載される。サンプルを作って試してみよう。

takatoh@sofa: Documents > cd node-sample
takatoh@sofa: node-sample > npm init

この状態で package.json はこんなふうになっている。

{
  "name": "sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

このプロジェクトに特定の Node.js のバージョンを指定するには volta pin コマンド。

takatoh@sofa: node-sample > volta pin node@14
success: pinned [email protected] (with [email protected]) in package.json

バージョンの指定は package.json に書き込まれ、次のようになる。

{
  "name": "sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "volta": {
    "node": "14.20.0"
  }
}

node -v で確認。

takatoh@sofa: node-sample > node -v
v14.20.0

プロジェクトのディレクトリを抜けると、デフォルトのバージョンに戻る。

takatoh@sofa: node-sample > cd ..
takatoh@sofa: Documents > node -v
v16.17.0

fnm

fnm は Fast Node Manager の略だそうだ。公式サイトは見当たらないんだけど、GitHub で公開されている。

Windows にインストールするには Chocolatey を使う。PowerShell を「管理者として実行」して、次のコマンドでインストール。

takatoh@sofa: Documents > choco install fnm

PowerShell で使うため、設定ファイルに次の行を書き足す。

fnm env --use-on-cd | Out-String | Invoke-Expression

PowerShell の設定ファイルは $profile と打ち込めばパスを表示してくれる。

takatoh@sofa: Documents > $profile
C:\Users\takatoh\Documents\PowerShell\Microsoft.PowerShell_profile.ps1

これで準備は完了。

Node.js をインストールするには fnm install コマンドを使う。16系をインストールしてみよう。

takatoh@sofa: Documents > fnm install 16
Installing Node v16.17.0 (x64)

でも、これだけだとまだ使えない。

takatoh@sofa: Documents > node -v
node: The term 'node' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

PowerShell を起動しなおしたら使えるようになった。

takatoh@sofa: Documents > node -v
v16.17.0

14系もインストールしてみよう。

takatoh@sofa: Documents > fnm install 14
Installing Node v14.20.0 (x64)

fnm list コマンドでインストール済みのバージョンを確認できる。v14.20.0 とv16.17.0 がインストールされてるけど、この時点では v16.17.0 がデフォルトになってる。

takatoh@sofa: Documents > fnm list
* v14.20.0
* v16.17.0 default
* system
takatoh@sofa: Documents > node -v
v16.17.0

切り替えるには fnm use コマンド。

takatoh@sofa: Documents > fnm use 14
Using Node v14.20.0
takatoh@sofa: Documents > node -v
v14.20.0

ところで system ってのは何だろう?

takatoh@sofa: Documents > fnm use system
Bypassing fnm: using system node
takatoh@sofa: Documents > node -v
node: The term 'node' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

たぶん、fnm の管理下にない、もとからインストールされてるバージョンのことだな。今回の場合はそんなものはないのでエラーが出てる、と。fnm use コマンドでインストール済みのバージョンを指定してやれば直る。

takatoh@sofa: Documents > fnm use 16
Using Node v16.17.0
takatoh@sofa: Documents > node -v
v16.17.0

プロジェクトごとにバージョンを使い分けるには次のようにする。

takatoh@sofa: Documents > mkdir node-sample2
takatoh@sofa: Documents > cd node-sample2
takatoh@sofa: node-sample2 > npm init
takatoh@sofa: node-sample2 > fnm use 14
takatoh@sofa: node-sample2 > node -v > .node-version

fnm では Volta と違って .node-version ファイルに保存される。中身は次のようになってる。

takatoh@sofa: node-sample2 > cat .node-version
v14.20.0

.node-version ファイルがあるディレクトリに移動すると、自動的にバージョンを切り替えてくれる。いったん上のディレクトリに移動し、v16.17.0 に変更した後、もう一度プロジェクトのディレクトリに戻ると、v14.20.0 に切り替わる。

takatoh@sofa: node-sample2 > node -v
v14.20.0
takatoh@sofa: node-sample2 > cd ..
takatoh@sofa: Documents > fnm use 16.17.0
Using Node v16.17.0
takatoh@sofa: Documents > node -v
v16.17.0
takatoh@sofa: Documents > cd node-sample2
Using Node v14.20.0
takatoh@sofa: node-sample2 > node -v
v14.20.0

が、上のディレクトリに戻っても v16.17.0 には切り替わってくれない。どうも .node-version ファイルがないディレクトリに移動したときには切り替えが発生しないみたいだ。どうしようかと思ったら fnm default コマンドがあった。

takatoh@sofa: Documents > fnm default v16.17.0
takatoh@sofa: Documents > node -v
v14.20.0
takatoh@sofa: Documents > fnm use 16
Using Node v16.17.0
takatoh@sofa: Documents > node -v
v16.17.0
takatoh@sofa: Documents > cd node-sample2
Using Node v14.20.0
takatoh@sofa: node-sample2 > node -v
v14.20.0
takatoh@sofa: node-sample2 > cd ..
takatoh@sofa: Documents > node -v
v14.20.0

なのに期待通り動作しないじゃない。よくよくみると上のほうで fnm list コマンドを実行したときの出力に v16.17.0 が default ってなってる。

何か忘れてる?それともバグ?

2つのGitリポジトリを統合する

別々に作った2つの Git リポジトリを統合する方法。違う方法もあると思うけど、調べた結果この方法でできた、ってことで記録しておく。

前提として:

  • 既存の2つのリポジトリ、repo_arepo_b を、新しく作った repo_combined に統合する
  • いずれも Python のプロジェクトで、Poetry を使ってる

まずは新しいプロジェクトを作る。

takatoh@apostrophe:w$ poetry new repo_combined

ディレクトリに移動して、Git の初期化と最初のコミット。このコミットには Poetry がデフォルトで作ってくれるファイルしかない。

takatoh@apostrophe:w$ cd repo_combined
takatoh@apostrophe:repo_combined$ git init
Initialized empty Git repository in /home/takatoh/w/repo_combined/.git/
takatoh@apostrophe:repo_combined$ git add .
takatoh@apostrophe:repo_combined$ git commit -m "First commit."
takatoh@apostrophe:repo_combined$ git branch -M main

リポジトリ repo_a をリモートブランチに取り込む。

takatoh@apostrophe:repo_combined$ git remote add -f repo_a https://github.com/takatoh/repo_a.git

これで、repo_combined の中には main ブランチと、全く繋がりのない repo_a/main が存在する状態になる。で、その repo_a/mainmain にマージする。

takatoh@apostrophe:repo_combined$ git merge repo_a/main
fatal: refusing to merge unrelated histories

……が、失敗した。mainrepo_a/main は全く別もので繋がりがないのでマージできない。マージするには --allow-unrelated-histories オプションをつけてやる。

takatoh@apostrophe:repo_combined$ git merge --allow-unrelated-histories repo_a/main
CONFLICT (add/add): Merge conflict in pyproject.toml
Auto-merging pyproject.toml
Automatic merge failed; fix conflicts and then commit the result.

pyproject.toml にコンフリクトが発生したので、解消してやる。

takatoh@apostrophe:repo_combined$ code pyproject.toml
takatoh@apostrophe:repo_combined$ git add pyproject.toml
takatoh@apostrophe:repo_combined$ git commit -m "Merge repository 'repo_a/main'."

これで repo_a/main のマージが完了。根元が別のコミットツリーが合流するという、ちょっと不思議な感じのするツリーが出来上がる。

つぎは repo_b。これも同じようにすればいい。

takatoh@apostrophe:repo_combined$ git remote add -f repo_b https://github.com/takatoh/repo_b.git

リモートブランチ repo_b/main に取り込んだら、main にマージ(--allow-unrelated-histories をつけて)。

takatoh@apostrophe:repo_combined$ git merge --allow-unrelated-histories repo_b/main
CONFLICT (add/add): Merge conflict in pyproject.toml
Auto-merging pyproject.toml
CONFLICT (add/add): Merge conflict in poetry.lock
Auto-merging poetry.lock
CONFLICT (add/add): Merge conflict in .gitignore
Auto-merging .gitignore
Automatic merge failed; fix conflicts and then commit the result.

当然のようにコンフリクトが発生する(今度はファイル3つ)ので解消してやる。

poetry.lock のコンフリクトを解消するのは厄介だけど、そもそもこのファイルは人間が編集するようなものじゃないので、あとで poetry install で作り直せばすむ。ここでは内容の整合は無視して、とにかくコンフリクトを解消してやればいい。

takatoh@apostrophe:repo_combined$ git add .gitignore
takatoh@apostrophe:repo_combined$ git add pyproject.toml
takatoh@apostrophe:repo_combined$ git add poetry.lock
takatoh@apostrophe:repo_combined$ git commit -m "Merge repository 'repo_b/main'."

で、poetry.lock をいったん削除してから poetry install

takatoh@apostrophe:repo_combined$ git rm poetry.lock
rm 'poetry.lock'
takatoh@apostrophe:repo_combined$ poetry install
takatoh@apostrophe:repo_combined$ git add poetry.lock
takatoh@apostrophe:repo_combined$ git commit -m "poetry.lock: Re-generate."

これでめでたく完了。

ディレクトリ内の画像からコンタクトシートを作るツールを作った(Go 言語で)

コンタクトシートってのは、画像(写真)のサムネイルを一覧にして1枚のシート(あるいは1ページ)に並べたもののこと。説明するまでもないか。

とにかく、コンタクトシートを作る必要ができて方法を調べてみたら、Windows の標準機能でできることを知った。ところが、確かに簡単に PDF に出力できるんだけど、どういうわけかファイル名の順に並んでくれない。用が足りないわけじゃないんだけどなんとも気持ちが悪い。

なので、ツールを自作することにした。最近は Python を使うことが多いので今回も Python で、と思って PDF の作り方を調べている途中で、そういえば以前、ディレクトリ内の画像をサムネイルのリストを表示する html ファイルを作るツールを作ったな、と思い出した。↓これ。

Go 言語だった。

そういうわけで、久しぶり(最後のコミットは2019年の2月、なんと3年半前だ)に Go を書いて、自分でも忘れてたツールを機能拡張することにした。

方針はこうだ。

mkphotoindex コマンドに --contactsheet オプションを追加して、指定したディレクトリに含まれる画像のサムネイルとファイル名を並べた PDF ファイルを出力する。

PDF を出力できるパッケージを探してみると、github.com/signintech/gopdf というのが見つかった。おおまかな使い方は、ページを追加し、テキストや画像を配置、PDF に出力する、というシンプルなもの。あるいは原始的ともいう。

もう少し楽にできそうなのはないかと探して見つけたのが、github.com/mikeshimura/goreport。これは内部では github.com/signintech/gopdf を使っているけど、帳票を作るのに便利なように作られている。決まったフォーマットの PDF を作るにはちょうどよさそうだと思って、これを使って実装を始めた。参考にしたページは以下。

ところが、なかなか思うようにはいかなかった。Qiita の記事を読むと、結構複雑な帳票も簡単にできるように思えるんだけど、サムネイルを例えば 4列×5行とかに並べるのがうまくできない。多分理解が足りないだけで、いいやり方があるんだと思うんだけど。

で、結局のところ、A4 のページにサムネイル(とファイル名)を並べたいだけだと思いなおして、github.com/signintech/gopdf を直接使うことにした。サムネイルのサイズや位置の調整に時間をとられたけど、とりあえず用は足りるものはできた。

とはいえ問題が一つ残った。github.com/signintech/gopdf は ttf フォントを読み込んで使える(もちろん日本語も)ので、IPAex フォントをダウンロードして使ったんだけど、現状の実装ではカレントディレクトリに フォントファイル ipaex.ttf を置いておく必要がある。これは結構面倒だ。今回に限っては Windows で実行するのが前提なので Windows に標準でついているフォントファイルのパスをコードに埋め込んでしまえば回避できる。でも、ほかの OS でも使うことを考えるとそれは避けたい。コマンドラインオプションでフォントファイルを指定してやることも考えたけど、デフォルトのフォントがあったほうがいい。OS ごとにデフォルトのフォントを決めてやるにはどうしたらいいんだろうか。

iPadからローカルネットワーク上のWebサーバにアクセスする

メモ。あとでもう少し丁寧に書くつもり。

Squidのインストールと設定

apt でインストール。

takatoh@wplj:~$ sudo apt install -y squid

設定ファイルは /etc/squid/squid.conf だけど、/etc/squid/conf.d/*.conf を読み込むようになってるので、直接編集せずに /etc/squid/conf.d/panicblanket.conf ファイルを作る。

acl lan src 192.168.2.0/24
http_access allow lan

できたら Squid を再起動。

takatoh@wplj:~$ sudo systemctl restart squid

プロキシ用のポートを開く

Squid のデフォルトのまま。3128番のポートを開く。

takatoh@wplj:~$ sudo ufw allow 3128
Rule added
Rule added (v6)

iPadの設定

Wi-Fi の設定の一番下にある「プロキシを構成」をタップ。「手動」にチェックを入れて、サーバの IP アドレスとポート番号を入力。保存すれば OK。

これで iPad からローカルネットワーク上の Web サーバにアクセスできるようになった。

Python:ランダムな文字列を作る

メモ。

UUID を生成。

>>> import uuid
>>> u = uuid.uuid4()
>>> u
UUID('b7206123-4b26-49ca-b33a-d0fb240c17f9')

Base32エンコードする。

>>> import base64
>>> b = base64.b32encode(u.bytes)
>>> b
b'W4QGCI2LEZE4VMZ22D5SIDAX7E======'

b はバイト文字列なので、文字列に変換。ついでに後ろに付いている = を取り去る。これは文字数合わせのためのパディングなので取ってしまって構わない。

>>> s = b.decode().rstrip('=')
>>> s
'W4QGCI2LEZE4VMZ22D5SIDAX7E'

さらに、見た目のランダムさを増すために、ラテン文字の大文字と小文字を混在させる。そのために、ランダムにラテン文字を小文字に変換する関数を定義。

>>> import random
>>> def down(c):
...     if random.choice([True, False]):
...         return c.lower()
...     else:
...         return c

これを、s の一文字ずつに適用。

>>> random_id = ''.join([ down(c) for c in s ])
>>> random_id
'W4qgCi2LEzE4vMz22D5sidAx7E'

これで OK。やや長いのが難点かな。

>>> len(random_id)
26

Ubuntu Server 22.04にSambaサーバをたてる(Dockerなしで)

Docker で Samba サーバをたてるのは諦めた。Ubuntu の上で直接 Samba サーバを動かすことにする。

まずはお決まりの apt update から。

takatoh@wplj:~$ sudo apt update

Samba サーバのインストール。

takatoh@wplj:~$ sudo apt install -y samba

設定ファイルを編集(バックアップをとってから)。

takatoh@wplj:~$ sudo cp /etc/samba/smb.conf /etc/samba/smb.conf.orig
takatoh@wplj:~$ sudo vim /etc/samba/smb.conf

ワークグループの名前を変更したほか、ユーザー認証なしでアクセスできる public と takatoh のみアクセスできる restricted の2つの共有フォルダを作った。

もとの設定ファイルとの差分:

takatoh@wplj:~$ diff /etc/samba/smb.conf.orig /etc/samba/smb.conf
29c29
<    workgroup = WORKGROUP
---
>    workgroup = PANICBLANKET
241a242,257
> 
> [public]
>     path = /mnt/data/samba
>     writable = yes
>     force create mode = 0644
>     force directory mode = 0755
>     guest ok = yes
>     guest only = yes
> 
> [restricted]
>     path = /mnt/data/samba_takatoh
>     writable = yes
>     force create mode = 0644
>     force directory mode = 0755
>     valid users = takatoh
>     force user = takatoh

コメントを除く全体はこう:

[global]
   workgroup = PANICBLANKET
   server string = %h server (Samba, Ubuntu)
   log file = /var/log/samba/log.%m
   max log size = 1000
   logging = file
   panic action = /usr/share/samba/panic-action %d
   server role = standalone server
   obey pam restrictions = yes
   unix password sync = yes
   passwd program = /usr/bin/passwd %u
   passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* .
   pam password change = yes
   map to guest = bad user
   usershare allow guests = yes

[printers]
   comment = All Printers
   browseable = no
   path = /var/spool/samba
   printable = yes
   guest ok = no
   read only = yes
   create mask = 0700

[print$]
   comment = Printer Drivers
   path = /var/lib/samba/printers
   browseable = yes
   read only = yes
   guest ok = no

[public]
    path = /mnt/data/samba
    writable = yes
    force create mode = 0644
    force directory mode = 0755
    guest ok = yes
    guest only = yes

[restricted]
    path = /mnt/data/samba_takatoh
    writable = yes
    force create mode = 0644
    force directory mode = 0755
    valid users = takatoh
    force user = takatoh

で、共有用のディレクトリを作る。

takatoh@wplj:~$ sudo mkdir -p /mnt/data/samba
takatoh@wplj:~$ sudo chown nobody:nogroup /mnt/data/samba
takatoh@wplj:~$ sudo mkdir -p /mnt/data/samba_takatoh
takatoh@wplj:~$ sudo chown takatoh:takatoh /mnt/data/samba_takatoh

結果としてこうなった。

takatoh@wplj:~$ ls -l /mnt/data
total 8
drwxr-xr-x 2 nobody  nogroup 4096 May  6 18:13 samba
drwxr-xr-x 3 takatoh takatoh 4096 May  6 18:13 samba_takatoh

そして、smbd をリスタート。

takatoh@wplj:~$ sudo systemctl restart smbd

Samba にユーザーを作る。pdbedit コマンド。

takatoh@wplj:~$ sudo pdbedit -a takatoh
new password:
retype new password:
Unix username:        takatoh
NT username:          
Account Flags:        [U          ]
User SID:             S-1-5-21-3828460484-3255695466-1925772709-1000
Primary Group SID:    S-1-5-21-3828460484-3255695466-1925772709-513
Full Name:            takatoh
Home Directory:       \\WPLJ\takatoh
HomeDir Drive:        
Logon Script:         
Profile Path:         \\WPLJ\takatoh\profile
Domain:               WPLJ
Account desc:         
Workstations:         
Munged dial:          
Logon time:           0
Logoff time:          Thu, 07 Feb 2036 00:06:39 JST
Kickoff time:         Thu, 07 Feb 2036 00:06:39 JST
Password last set:    Fri, 06 May 2022 18:12:32 JST
Password can change:  Fri, 06 May 2022 18:12:32 JST
Password must change: never
Last bad password   : 0
Bad password count  : 0
Logon hours         : FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

これでOK。Windows マシンからアクセスしてみると、ちゃんと \\wplj\public には認証無しで読み書きできる。\\wplj\restricted にはユーザー名とパスワードを要求され、正しく入力すれば読み書きできる。

無事完了。

[追記]

Samba のインストールと設定に際して、ファイアウォール関連は何もしていない。設定は次のようになっている。

takatoh@wplj:~$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere                  
137                        ALLOW       Anywhere                  
138                        ALLOW       Anywhere                  
139                        ALLOW       Anywhere                  
445                        ALLOW       Anywhere                  
22/tcp (v6)                ALLOW       Anywhere (v6)             
137 (v6)                   ALLOW       Anywhere (v6)             
138 (v6)                   ALLOW       Anywhere (v6)             
139 (v6)                   ALLOW       Anywhere (v6)             
445 (v6)                   ALLOW       Anywhere (v6)

Docker を使ってやってたときには何が悪かったのか、結局よくわからない。