LoginSignup
3
2

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-02-11

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

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

したい理由

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

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

output_clip.jpg

例:上の図は平成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倍に増加

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

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

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

サンプルコード

EPSに変換するコード。gsavegrestoreをコピペして座標変換をうまく設定すれば、PostScriptに画像を重ねることができる。

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
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
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

参考


  1. 地球流体電脳ライブラリ(DCL) v5.3.4.2 ※v6から出力形式が変更された 

  2. 高知大学気象情報頁より 

  3. MERRA再解析データより 

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2