Class: Rixmap::Format::PNG::PNGImageIO

Inherits:
ImageIO::BaseImageIO show all
Defined in:
lib/rixmap/format/png/imageio.rb

Overview

PNG入出力実装クラス.

さすがにPNGフル実装はきついですな(´・ω・`)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ImageIO::BaseImageIO

#open, #read, #readable?, #save, #writable?, #write

Constructor Details

#initialize(options = {}) ⇒ PNGImageIO

PNG入出力オブジェクトを初期化します.

Parameters:

  • options (Hash) (defaults to: {})

    オプションパラメータ

Options Hash (options):

  • :compression_level (Integer)

    圧縮レベル (0-9).

  • :filter_method (Integer)

    スキャンラインのフィルタリングメソッド



200
201
202
203
204
# File 'lib/rixmap/format/png/imageio.rb', line 200

def initialize(options={})
  @compression_level = Zlib::BEST_COMPRESSION
  @filter_method     = ADAPTIVEFILTER_UP
  super(options)
end

Instance Attribute Details

#compression_levelInteger

ピクセルデータ圧縮時の圧縮レベル.

値としては Zlib モジュールの定数か、0~9の数値を指定します.

Returns:

  • (Integer)

    圧縮レベル



180
181
182
# File 'lib/rixmap/format/png/imageio.rb', line 180

def compression_level
  @compression_level
end

#filter_methodInteger

スキャンライン毎のフィルタリングメソッド.

値は以下のいずれかです.

Returns:

  • (Integer)

    フィルタリングメソッド



193
194
195
# File 'lib/rixmap/format/png/imageio.rb', line 193

def filter_method
  @filter_method
end

Class Method Details

.readable?(magic) ⇒ Boolean

指定マジックデータがPNG画像のものかを判定します.

Parameters:

  • magic (String)

    画像先頭データ

Returns:

  • (Boolean)

    PNG形式のシグネチャならtrue



163
164
165
# File 'lib/rixmap/format/png/imageio.rb', line 163

def self.readable?(magic)
  return (magic[0, FILE_SIGNATURE_SIZE] == FILE_SIGNATURE)
end

.writable?(image) ⇒ Boolean

指定画像をPNGとしてエンコードできるかを返します.

Parameters:

Returns:

  • (Boolean)

    PNGとしてエンコードできる場合はtrue



171
172
173
# File 'lib/rixmap/format/png/imageio.rb', line 171

def self.writable?(image)
  return true
end

Instance Method Details

#decode(data, options = {}) ⇒ Rixmap::Image

バイトデータをPNG画像として復元します.

Parameters:

  • data (String)

    入力データ

  • options (Hash) (defaults to: {})

    オプションパラメータ

Returns:



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# File 'lib/rixmap/format/png/imageio.rb', line 355

def decode(data, options={})
  if data.byteslice(0, FILE_SIGNATURE_SIZE) != FILE_SIGNATURE
    raise ArgumentError.new("InvalidSignature detected. Input is not PNG Image Data.")
  end

  # チャンクを分割
  offset = FILE_SIGNATURE_SIZE
  length = data.bytesize
  chunks = []
  while offset < length
    chunk_size = data.byteslice(offset, offset + 4).unpack('N')[0]; offset += 4
    chunk_data = nil
    chunk_type = nil
    chunk_crc  = nil
    if chunk_size > 0
      chunk_type, chunk_data, chunk_crc = data.byteslice(offset, offset + (chunk_size + 8)).unpack("a4a#{chunk_size}N")
    elsif chunk_size == 0
      chunk_type, chunk_crc = data.byteslice(offset, offset + 8).unpack("a4N")
    else
      raise RuntimeError.new("Invalid Chunk Size: #{chunk_size}")
    end
    offset += (chunk_size + 8)

    # チャンクがあるかをチェック
    unless Chunk.has?(chunk_type)
      # ないので警告出して次へ
      warn("Unknown or Not-Implemented Chunk Type: #{chunk_type}")
      next
    end

    # チャンクを作成
    chunk = Chunk.get(chunk_type).new
    chunk.data = chunk_data

    chunks << chunk
    break if chunk.type == Chunk::IENDChunk::TYPE
  end

  # IHDRチャンクを処理
  ihdr = chunks.shift
  unless ihdr.type == Chunk::IHDRChunk::TYPE
    raise ArgumentError.new("Illegal Chunk Order. First chunk must to be IHDR chunk.")
  end

  image_width  = ihdr.width
  image_height = ihdr.height
  image_depth  = ihdr.depth
  colortype    = ihdr.colortype

  # 残りチャンクを処理
  image_data = ''
  palette    = nil
  bgcolor    = nil
  transcolor = nil
  chunks.each do |chunk|
    case chunk
    when Chunk::IDATChunk
      image_data.concat(chunk.data)

    when Chunk::PLTEChunk
      palette = Rixmap::Palette.new(2 ** image_depth)
      palette.size.times do |i|
        palette[i] = chunk[i]
      end

    when Chunk::IENDChunk
      break

    when Chunk::BKGDAncillaryChunk
      chunk.colortype = colortype
      chunk.unpack    # 中身を更新
      bgcolor = chunk.value

    when Chunk::TRNSAncillaryChunk
      chunk.colortype = colortype
      chunk.unpack
      transcolor = chunk.value
    end

  end

  # ピクセルを復元
  pixels = Zlib.inflate(image_data).each_byte

  # ベース画像を作成
  image = nil
  case colortype
  when COLORTYPE_INDEXED
    image  = Rixmap::Image.new(Rixmap::INDEXED, image_width, image_height, :palette => palette, :background => bgcolor)
    filter = AdaptiveFilter.new(image.mode)
    pixels.each_slice(image_width + 1).each_with_index do |bytes, i|
      image[i] = filter.recon(bytes)
    end

    # 透明度を反映
    unless transcolor.nil?
      transcolor.each_with_index do |alpha, i|
        color = image.palette[i].to_a
        color[3] = alpha
        image.palette[i] = color
        # TODO image.palette[i]の結果をPaletteColorとかにして、各コンポーネント毎に編集できるようにするべきかな
      end
    end
  when COLORTYPE_GRAYSCALE
    image  = Rixmap::Image.new(Rixmap::GRAYSCALE, image_width, image_height, :background => bgcolor, :transparent => transcolor)
    filter = AdaptiveFilter.new(image.mode)
    pixels.each_slice(image_width + 1).each_with_index do |bytes, i|
      image[i] = filter.recon(bytes)
    end
  when COLORTYPE_GRAYSCALE_WITH_ALPHA
    image  = Rixmap::Image.new(Rixmap::GRAYALPHA, image_width, image_height, :background => bgcolor)
    filter = AdaptiveFilter.new(image.mode)
    pixels.each_slice((image_width * 2) + 1).each_with_index do |bytes, i|
      image[i] = filter.recon(bytes)
    end
  when COLORTYPE_TRUECOLOR
    image  = Rixmap::Image.new(Rixmap::RGB, image_width, image_height, :background => bgcolor, :transparent => transcolor)
    filter = AdaptiveFilter.new(image.mode)
    pixels.each_slice((image_width * 3) + 1).each_with_index do |bytes, i|
      image[i] = filter.recon(bytes)
    end
  when COLORTYPE_TRUECOLOR_WITH_ALPHA
    image  = Rixmap::Image.new(Rixmap::RGBA, image_width, image_height, :background => bgcolor)
    filter = AdaptiveFilter.new(image.mode)
    pixels.each_slice((image_width * 4) + 1).each_with_index do |bytes, i|
      image[i] = filter.recon(bytes)
    end
  else
    raise RuntimeError.new("Unsupported Colo-Type: #{colortype}")
  end

  return image
end

#encode(image, options = {}) ⇒ String

画像をPNG形式でエンコードします.

Parameters:

  • image (Rixmap::Image)

    対象画像

  • options (Hash) (defaults to: {})

    オプションパラメータ

Options Hash (options):

  • :filter_method (Integer)

    スキャンラインのフィルタリングメソッド

Returns:

  • (String)

    エンコードされた画像データ



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/rixmap/format/png/imageio.rb', line 212

def encode(image, options={})
  # イメージヘッダ
  ihdr = Chunk::IHDRChunk.new
  ihdr.width  = image.width
  ihdr.height = image.height
  ihdr.depth  = 8 #image.mode.depth

  # 必要チャンク
  plte = nil

  # ピクセルデータ
  pixels = ''.force_encoding(Encoding::BINARY)
  filter = AdaptiveFilter.new(image.mode)
  filter_method = nil
  case options[:filter_method]
  when ADAPTIVEFILTER_NONE, ADAPTIVEFILTER_SUB, ADAPTIVEFILTER_UP, ADAPTIVEFILTER_AVERAGE, ADAPTIVEFILTER_PEATH
    filter_method = options[:filter_method].to_i
  else
    filter_method = @filter_method
  end
  compression_level = @compression_level
  unless options[:compression_level].nil?
    compression_level = options[:compression_level].to_i
  end

  # 画像種別による処理
  trns = nil  # 透明度チャンクデータ
  case image.mode
  when Rixmap::INDEXED
    ihdr.colortype = COLORTYPE_INDEXED

    # パレットを設定
    plte = Chunk::PLTEChunk.new
    alphas = []
    hasalpha = false
    image.palette.each_with_index do |color, i|
      plte[i] = [color.red, color.green, color.blue]
      alphas << color.alpha
      if color.alpha < 255
        hasalpha = true
      end
    end

    if hasalpha
      # 基本的にパレットのαを入れる
      trns = Chunk::TRNSAncillaryChunk.new(ihdr.colortype)
      trns.indexes = alphas
    elsif !image.transparent.nil?
      # 透過インデックス設定の場合
      # ※ 復元できない
      trns = Chunk::TRNSAncillaryChunk.new(ihdr.colortype)
      index = image.transparent.to_i
      indexes = [255] * index
      indexes[index] = 0
      trns.indexes = indexes
    end

    # ピクセルを作成
    image.each_line do |line|
      # ByteArrayはto_bytesできる
      pixels.concat(filter.filt(line, filter_method).to_str)
    end
  when Rixmap::GRAYSCALE
    ihdr.colortype = COLORTYPE_GRAYSCALE
    image.each_line do |line|
      pixels.concat(filter.filt(line, filter_method).to_str)
    end

    unless image.transparent.nil?
      trns = Chunk::TRNSAncillaryChunk.new(ihdr.colortype)
      trns.gray = image.transparent.to_i
    end
  when Rixmap::GRAYALPHA
    ihdr.colortype = COLORTYPE_GRAYSCALE_WITH_ALPHA
    image.each_line do |line|
      pixels.concat(filter.filt(line.to_s('LA'), filter_method).to_str)
    end
  when Rixmap::RGB
    ihdr.colortype = COLORTYPE_TRUECOLOR
    image.each_line do |line|
      pixels.concat(filter.filt(line.to_s('RGB'), filter_method).to_str)
    end

    unless image.transparent.nil?
      trns = Chunk::TRNSAncillaryChunk.new(ihdr.colortype)
      trns.value = image.transparent
    end
  when Rixmap::RGBA
    ihdr.colortype = COLORTYPE_TRUECOLOR_WITH_ALPHA
    image.each_line do |line|
      pixels.concat(filter.filt(line.to_s('RGBA'), filter_method).to_str)
    end
  else
    raise ArgumentError.new("Unknown image mode: #{image.mode}")
  end

  # ピクセルを圧縮
  deflated_pixels = Zlib.deflate(pixels, compression_level)

  # IDATチャンクを生成
  idat = Chunk::IDATChunk.new
  idat.data = deflated_pixels

  # チャンクリストの作成開始
  chunks = []
  chunks << ihdr
  chunks << plte unless plte.nil?
  chunks << idat
  chunks << trns unless trns.nil?

  # 背景色
  unless image.background.nil?
    bkgd = Chunk::BKGDAncillaryChunk.new(ihdr.colortype)
    bkgd.value = image.background
    chunks << bkgd
  end

  # チャンクリスト構築終了
  chunks << Chunk::IENDChunk.new

  # PNGストリームを構築
  png_data = ''
  png_data.concat(FILE_SIGNATURE)
  png_data.force_encoding(Encoding::BINARY)
  chunks.each do |chunk|
    chunk_data = chunk.data
    chunk_size = chunk_data.bytesize
    chunk_fulldata = chunk.type + chunk_data
    chunk_crc = Zlib.crc32(chunk_fulldata)
    #chunk_crc = CRC.crc(chunk_fulldata)   # 自前実装
    png_data.concat([chunk_size].pack('N'))
    png_data.concat(chunk_fulldata)
    png_data.concat([chunk_crc].pack('N'))
  end

  return png_data
end