PostScriptをテキスト編集して圧縮画像を埋め込み

高機能な画像編集ソフトの使い方ではなく、**自力でPostScriptを編集して**JPEGやPNGを埋め込む方法のメモ。

※数年前に書いたまま放置していたが、平成が終わる前に公開することにした。コードが拙い…。

## したい理由

- 手元のPostScriptの図に画像を追加して重ねたい
	- 図を描いたライブラリ[^dcl]は外部画像の埋め込みに非対応（元々そういう用途でないので）
	- 汎用ソフトで編集すると大幅に書き換えられ、ライブラリ付属の編集ツールが一切使えなくなる
		- というより自分自身がPostScriptを理解できなくなる
- 非圧縮のビットマップ画像ならライブラリを模倣して埋め込めるが、そのために圧縮画像を展開するのは気が進まない
	- ファイルサイズが無駄に増えてしまう
	- 元の形に再圧縮するのはたぶんどのソフトでも無理

要するに、**素材本来の情報をなるべく維持**して画像を追加したい。

![output_clip.jpg](https://qiita-image-store.s3.amazonaws.com/0/127130/d2ff7938-639e-ff16-8c07-c2e37b520756.jpeg)

例：上の図は[**平成26年豪雪**](https://ja.wikipedia.org/wiki/平成26年豪雪)のときの衛星雲画像[^sat-data]（JPEG）と海面気圧[^slp-data]・海岸線・経緯線（PostScript）を重ねたもの。海岸線がきれいに一致しているので、正しく重ねられていそう。

[^dcl]: [地球流体電脳ライブラリ](https://www.gfd-dennou.org/library/dcl/)（DCL） v5.3.4.2 ※v6から出力形式が変更された
[^sat-data]: 高知大学気象情報頁より
[^slp-data]: MERRA再解析データより

## 考え方
PostScriptはデータをエンコード・デコードするフィルターを複数持っている。これを利用して、「ASCII化したデータ」（PSファイルに貼り付け）→「圧縮画像本来のバイナリデータ」（元画像から抽出したもの）→「展開した画像データ」（ビットマップに近いもの）とデコードさせる。

- 画像
	- **DCTDecode** : JPEGデータの展開
	- **FlateDecode** : PNGなど、deflateで圧縮されたデータの展開
	- RLEDecode : 連長圧縮データの展開
- ASCII化データの読み込み
	- **Ascii85Decode** : 4バイトのデータ（=256進数4桁）を `"!-u"` の範囲の文字5つ（=85進数5桁）で表す。約1.25倍に増加
	- AsciiHexDecode : 16進数の文字列で表す。約2倍に増加

データを展開し終わったら、それを適切な画像として解釈するための情報が必要になる。これを事前に画像ファイルから抜き出すことになる。

- 色空間 : グレースケール・RGB・インデックスカラーなど
- 色深度 : 普通は8ビット、インデックスカラーでは4, 2, 1も
- 画像の幅と高さ

PostScriptはラスタ画像を描画する際、設定された座標上の1x1の領域に描こうとする。そのため、まともな大きさ・縦横比の画像を表示するには事前に座標変換を指定しておく必要がある。

## サンプルコード
EPSに変換するコード。`gsave`～`grestore`をコピペして座標変換をうまく設定すれば、PostScriptに画像を重ねることができる。

```ruby:raster2ps.rb
class Raster2PS
	# 画像ファイルを指定してインスタンスを作成する
	# (必要なメソッドを実装したサブクラスを用いること)
	def initialize(filename)
		raise 'Not implemented' if self.instance_of?(Raster2PS)
		@filename = filename
		read_image
	end

	# 画像を埋め込んだPostScriptをファイルへ書き出す
	def write_image(filename)
		File.open(filename, 'w') do |f|
			f.puts <<-EOS
%!PS-Adobe-3.0 EPSF-3.0
%%Title: #{@filename}
%%Creator: raster2ps.rb
%%CreationDate: #{Time.now}
%%BoundingBox: 0 0 #{@width} #{@height}
%%DocumentData: Clean7Bit
%%LanguageLevel: 3
%%EndComments
%%BeginProlog
%%EndProlog
%%Page: 1 1
gsave

0 0 translate
0 rotate
#{@width} #{@height} scale

#{colorspace} setcolorspace

/Data currentfile /ASCII85Decode filter
#{image_filter} def
<< /ImageType 1
   /Width #{@width}
   /Height #{@height}
   /ImageMatrix [#{@width} 0 0 #{-@height} 0 #{@height}]
   /DataSource Data
   /BitsPerComponent #{@depth}
   /Decode [#{decode.join(" ")}]
   /Interpolate true
>> image
#{self.class.ascii85_encode(@data)}

grestore
%%EOF
			EOS
		end
	end

	private

	# @filenameを読み込み、@width, @height, @depth, @data, (適宜その他)へ保存する
	def read_image; end

	# PostScriptに埋め込む文字列を組み立てて返す
	def colorspace; end
	def image_filter; end
	def decode; end

	# バイナリ文字列をAdobe Ascii85形式の7bit文字列へエンコードする
	def self.ascii85_encode(str)
		m = (-str.length) % 4

		tuples = (str + "\x00" * m).unpack('N*')
		tuples.collect! do |tuple|
			5.times.inject([]) do |ary,i|
				tuple, mod = tuple.divmod(85)
				ary.unshift(mod + 0x21)
			end
		end
		tuples.flatten!
		tuples.pop(m)

		str = tuples.pack('C*')
		str.gsub!(/.{1,5}/) { |s| s == '!!!!!' ? 'z' : s }
		str.scan(/.{1,80}/).unshift('<~').push('~>').join("\n")
	end
end
```

```ruby:jpeg2ps.rb
require './raster2ps'

class JPEG2PS < Raster2PS
	def read_image
		@data = File.read(@filename, mode: 'rb')
		raise 'Invalid JPEG' if @data[0,4] != "\xff\xd8\xff\xe0"

		n = @data.length
		i = 2
		while i < n
			mark, size = @data[i,4].unpack('a2n')
			if mark == "\xff\xc0" # SOF0
				@height, @width, @type = @data[i+4,size-2].unpack('xnnc')
				break
			end
			i += 2 + size
		end

		@depth = 8

		self
	end

	def colorspace
		case @type
		when 1 then '/DeviceGray'
		when 3 then '/DeviceRGB'
		when 4 then '/DeviceCMYK'
		else        raise "unknown JPEG type: #{@type}"
		end
	end

	def image_filter
		'/DCTDecode'
	end

	def decode
		[0, 1] * @type
	end
end
```

```ruby:png2ps.rb
require './raster2ps'

class PNG2PS < Raster2PS
	def read_image
		@data = ''

		File.open(@filename, 'rb') do |f|
			raise 'Invalid PNG' if f.read(8) != "\x89PNG\r\n\x1a\n"

			until f.eof?
				size, name = f.read(8).unpack('Na4')
				bin = f.read(size)
				crc = f.read(4)

				case name
				when 'IHDR'
					@width, @height, @depth, @type, = bin.unpack('N2C5')
				when 'PLTE'
					@plte = bin
				when 'IDAT'
					@data << bin
				end
			end
		end

		self
	end

	def colorspace
		case @type
		when 0 then '/DeviceGray'
		when 2 then '/DeviceRGB'
		when 3 then
			<<-EOS
/Palette currentfile #{@plte.length} string readhexstring 
#{@plte.pack('H*').gsub(/.{1,6}/, '#\0 ').scan(/.{1,64}/).join("\n")}
pop def
[/Indexed /DeviceRGB #{@plte.length / 3 - 1} Palette]
			EOS
		else        raise "unknown PNG type: #{@type}"
		end
	end

	def image_filter
		<<-EOS
<< /Predictor 10
   /Columns #{@width}
   /Colors #{decode.length / 2}
   /BitsPerComponent #{@depth}
>> /FlateDecode
		EOS
	end

	def decode
		case @type
		when 0 then [0, 1]
		when 2 then [0, 1] * 3
		when 3 then [0, 2 ** @depth - 1]
		else        raise "unknown PNG type: #{@type}"
		end
	end
end
```

## 補足
サンプルコードを読めば分かるかもしれないが、細かい点はこちらにメモ。

### 画像の読み込み
幅と高さ・色深度・色数などの情報および貼り付けるデータ本体を画像ファイルから取り出す。

#### JPEG

- ファイルは `"\xff\xd8\xff\xe0"` で始まる
- 各種ヘッダは、マーカー（2バイト）・サイズ（2バイト）・データ（n-2バイト）という構造
	- SOF0（マーカー `"\xff\xc0"`）の中に、画像サイズや色の種類の情報がある
- PSに貼り付けるデータはファイル全体でよい。本体を抽出する必要なし

#### PNG

- ファイルは `"\x89PNG\r\n\x1a\n"` で始まる
- 各チャンクは、サイズ（4バイト）・名前（4バイト）・データ（nバイト）・CRC（4バイト）という構造
	- IHDRの中に、画像サイズや色深度や色の種類の情報がある
	- PLTEはカラーパレットなので、あれば抜き出しておく
	- IDATは画像本体
- PSに貼り付けるデータはIDATの中身。大きなデータは分割されているので、全て抜き出して連結する必要がある

### 画像設定
#### 色空間

- 基本的な場合は色の次元で決まり、 `/DeviceGray`, `/DeviceRGB` のように名前だけ指定すればいい
- インデックスカラーの場合は複雑で、`[/Indexed 色空間名 インデックス最大値(=色数-1) パレットデータ]` と配列で与える

### フィルター

- JPEGは `/DCTDecode` だけで全て処理してくれる
- PNGは汎用のZlibを利用しているので、画像のデコードであることを辞書で教える必要がある

### /Decode

- 画素の数値がとる範囲を指定する
	- 通常は0～1。次元の数だけ必要なので、RGBなら `[0 1 0 1 0 1]` と6要素の配列になる
	- インデックスの場合は色数ではなく、色深度によって決まる。例えば4ビットなら0～15

## 参考

- [PostScript Language Reference, third edition](https://www.adobe.com/products/postscript/pdfs/PLRM.pdf)
- [Jpeg2Ps for Windows](http://gnuwin32.sourceforge.net/packages/jpeg2ps.htm)