職業プログラマの休日出勤

職業プログラマによる日曜自宅プログラミングや思考実験の成果たち。リアル休日出勤が発生すると更新が滞りがちになる。記事の内容は個人の意見であり、所属している(いた)組織の意見ではない。

続・パイプを通るPDF(兼・第35回 #シェル芸 勉強会 参加記)

シェル芸勉強会 第35回 に参加しました!
今回は東京会場での参加です。
usptomo.doorkeeper.jp

あわせて、LTにて 続・パイプを通るPDF という題でしゃべったりもしたので、その内容も解説しておきたいと思います。

続・パイプを通るPDF

LTでしゃべった時のスライドです(川柳・ちょっと字余り)
speakerdeck.com
※スライドの9ページには微妙に間違いがあります。。

意図

PDFファイルに対して、何らかのちょっとした加工(線を引いたり)を、ワンライナーで実現することを目的にしています。
ワンライナーでPDFを吐くのならば、おそらくPostScriptを生成して、それをGhostScriptでPDFに変換する、といったアプローチも取ることができるでしょうし、その方が簡単でしょう。

最終的なワンライナー

convert xc:none -page A4 pdf:-| gs -q -sOutputFile=- -sDEVICE=pdfwrite -c '<< /EndPage {exch pop 2 lt { newpath 200 300 moveto 10 200 rlineto closepath 0.8 0.2 0.1 setrgbcolor stroke true}{false} ifelse} bind >> setpagedevice' -| pdftk - output - uncompress

簡単に解説
  • convert (ImageMagick)
  • gs (GhostScript)
    • -sOutputFile=- 出力をstdoutとします
    • -sDEVICE=pdfwrite 出力形式をPDFとします。省略するとPostScriptで吐きます。gsはもともとPostScriptの処理用のプログラムですので。
    • -c PostScriptのコマンドを与えていることを示します
    • newpath パスの開始
    • 200 300 moveto ペンの移動:座標平面の原点(左下原点です!)からx右方向に200、y上方向に300移動します。
    • 10 200 rlineto 線分をパスとする:現在のペンの位置から、相対的にx右方向に10、y上方向に200移動した先までの線分をパスとします。
    • closepath 幾何学的に「閉じた」パスを作成するために、現在のペンの位置からパスの開始位置までに戻る線分をパスとして加えます。ここでは不要なのですが、「閉じた」パスになっていると色々便利なのでよく使います。
    • 0.8 0.2 0.1 setrgbcolor RGBで色を指定します。左から赤、緑、青の順で、 0 0 0 なら黒、1 1 1 なら白、0.8 0.2 0.1 なら赤っぽい色、になります。
    • stroke 上記で描いたパスで、実際に線を描きます。
    • 上記以外のPostScript部分:説明するとそれだけで記事1つ書けちゃいますので省略。説明みたい人は、このStackOverflowへ:jpeg - Is it possible in Ghostscript to add watermark to every page in PDF - Stack Overflow
  • pdftk
    • これによって、PDFの描画部分の FlateDecode での圧縮を伸長しています。PDFのグラフィックコマンド群が human-readable (ほんとか?www)になります。
環境構築例

上記のワンライナーを走らせる際の環境構築例を示します。(Dockerの場合)

  • docker run -it -v "$(pwd)/pdf":/pdf \
ubuntu:16.04 bash
    • カレントディレクトリ配下の pdf というディレクトリを、コンテナ内から /pdfとして読み書きできるようになっちゃいます。大事なデータとかは避難しましょ。ディレクトリが無ければ作られます。
    • exit後に再利用するときは docker start -ai $(docker ps -a で見えるname)
  • apt update
  • apt install ghostscript imagemagick pdftk less
今後の展望

ご想像の通り、PostScript部分は yesなりsedなりawkなり、お好きな方法で構築することができるでしょう。あとは、ちょっとしたPostScriptの文法やら何やらを覚えれば自由自在にPDFを作れそうですね! ちなみに、非ASCII文字を出すハードルは相当高いので、幾何学模様を出す程度に留めておいた方が幸せかもしれません。

問題と思考の過程

シェル芸勉強会の本編で出された問題と、それに対する回答や思考の過程を紹介しておきます。

問題文 【問題のみ】jus共催 第35回またまためでたいシェル芸勉強会 | 上田ブログ

Q1

取り組んだアプローチは、次のようなものでした。

  • curl parrot.live が画面をクリアする際に使っている制御文字を特定し、
  • その制御文字を検出したら wait が入るような何かを仕込みつつ
  • ファイルに保存したデータを読み出す

まずは制御文字を割り出すために、次のような操作をやりました。

curl parrot.live | od -x | head -n 100 | sed -e 's/[ ][2-7][0-9a-f]//g' -e 's/[ ][2-7][0-9a-f]//g'

この結果、次のようなものが得られます。

0001460                0a20    
0001500                  
0001520                  
0001540                   0a
0001560                  
0001600                  
0001620                  
0001640      0a20   1b      1b6d      1b4a     1b
0001660                  
0001700                  
0001720                  
0001740          0a20          
0001760                  

ここで、0aは皆さんご存知、Line Feed(改行文字)です。20とか6dとかは、消し損ねた通常の文字ですね。
これで、どうやら「ページ」が変わるタイミングで使われている制御文字が0x1bらしいということがわかります。これはEscapeの制御文字ですので、次の文字との組み合わせによって特殊な効果を生みます。
では、sedで消す前の文字を見ると、

(前略)
0001640      0a20    5b1b    3933    1b6d    325b    1b4a    485b    5b1b
0001660      3133    206d    2020    2020    2020    2020    2020    2020
(後略)

endianに気をつけながらこの辺のバイト列を上手く検出できれば、冒頭に掲げた理論で突っ走るだけです!
となったところで時間切れ。
他の皆さんは他の方法で解かれてたので、そちらの方が真っ当な解法だと思います。

Q2

これで回答しました
cat herohero | nkf -Z | sed -e 's/\([0-9]*\)/\1 /' | awk 'BEGIN{l=0;}{for(i=l+1;i<$1;i++){print""}l=$1;print $2;}'
awkのくだりは、古典的なプログラミング言語な感じがしますが、まあこれもいいでしょう(たぶん)

Q3

これで回答しました
cat data | sort | uniq -c | awk '{a[$2]=a[$2] " " $3 ":" $1}END{for(i in a)print i a[i]}'
ここで a[$2]=a[$2] " " $3 ":" $1の文字列結合の時点で挟み込んでいる空白文字が、キモですね。上手く使えた感があります。

Q4

MeCabの辞書から人名を引っ張り出せないかなーと検討したものの、時間切れです。
良さげな回答は、ランダムな文字列をMeCabにかけて、 grep 人名、なるほど〜〜

Q5

orz

Q6

これで回答しました
seq 1 100000 | factor | grep -v ' [3-9][0-9]' | grep -v ' 2[4-9]' | grep -v ' [0-9][0-9][0-9]' | head -n 1985 | sed 's/:.*//'
ひたすら grep -vで条件対象外のものを除去する作戦です。

Q7

これで回答しました
seq 1 100 | factor | tr -d ':' | awk 'BEGIN{a=0}$1==$2&&a<10{a++;print substr("HelloWorld",a,1)}$1!=$2{print "x"}' | xargs | tr -d ' '
手元の環境では日本語の入力ができなかったので、ASCII文字だけです。出題の意図から考えれば、print "x"はランダムさが必要です。

Q8

orz

書籍紹介コーナー

この本を読めば、$ cat hoge.pdf とかしちゃっても戸惑うことは減るはずです。

PDF構造解説

PDF構造解説

こちらの記事でも紹介してます => さぁ、PDF手書きの世界へ。 - 職業プログラマの休日出勤