高機能な画像編集ソフトの使い方ではなく、自力でPostScriptを編集してJPEGやPNGを埋め込む方法のメモ。
※数年前に書いたまま放置していたが、平成が終わる前に公開することにした。コードが拙い…。
したい理由
- 手元のPostScriptの図に画像を追加して重ねたい
- 図を描いたライブラリ1は外部画像の埋め込みに非対応(元々そういう用途でないので)
- 汎用ソフトで編集すると大幅に書き換えられ、ライブラリ付属の編集ツールが一切使えなくなる
- というより自分自身がPostScriptを理解できなくなる
- 非圧縮のビットマップ画像ならライブラリを模倣して埋め込めるが、そのために圧縮画像を展開するのは気が進まない
- ファイルサイズが無駄に増えてしまう
- 元の形に再圧縮するのはたぶんどのソフトでも無理
要するに、素材本来の情報をなるべく維持して画像を追加したい。
例:上の図は平成26年豪雪のときの衛星雲画像2(JPEG)と海面気圧3・海岸線・経緯線(PostScript)を重ねたもの。海岸線がきれいに一致しているので、正しく重ねられていそう。
考え方
PostScriptはデータをエンコード・デコードするフィルターを複数持っている。これを利用して、「ASCII化したデータ」(PSファイルに貼り付け)→「圧縮画像本来のバイナリデータ」(元画像から抽出したもの)→「展開した画像データ」(ビットマップに近いもの)とデコードさせる。
- 画像
- DCTDecode : JPEGデータの展開
- FlateDecode : PNGなど、deflateで圧縮されたデータの展開
- RLEDecode : 連長圧縮データの展開
- ASCII化データの読み込み
-
Ascii85Decode : 4バイトのデータ(=256進数4桁)を
"!-u"
の範囲の文字5つ(=85進数5桁)で表す。約1.25倍に増加 - AsciiHexDecode : 16進数の文字列で表す。約2倍に増加
-
Ascii85Decode : 4バイトのデータ(=256進数4桁)を
データを展開し終わったら、それを適切な画像として解釈するための情報が必要になる。これを事前に画像ファイルから抜き出すことになる。
- 色空間 : グレースケール・RGB・インデックスカラーなど
- 色深度 : 普通は8ビット、インデックスカラーでは4, 2, 1も
- 画像の幅と高さ
PostScriptはラスタ画像を描画する際、設定された座標上の1x1の領域に描こうとする。そのため、まともな大きさ・縦横比の画像を表示するには事前に座標変換を指定しておく必要がある。
サンプルコード
EPSに変換するコード。gsave
~grestore
をコピペして座標変換をうまく設定すれば、PostScriptに画像を重ねることができる。
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
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
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"
)の中に、画像サイズや色の種類の情報がある
- SOF0(マーカー
- 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
- 通常は0~1。次元の数だけ必要なので、RGBなら
参考
-
地球流体電脳ライブラリ(DCL) v5.3.4.2 ※v6から出力形式が変更された ↩
-
高知大学気象情報頁より ↩
-
MERRA再解析データより ↩