ホームページ開発ツール>Xojo / Real Studio Trial and Error・zlibを使って圧縮/伸長してみる

 Xojo / Real Studio Trial and Error

zlibを使って圧縮/伸長してみる

目次
 はじめに

 以下は、Xojo Cocoaビルドについての話題です。

 zlibを使った圧縮/伸長について試してみました。
 なお検証には、Xojo 2016 Release 3を用いています。(Mac mini mid 2010 + OS X 10.13.6 High Sierra)


 方針

 zlibは一般的なデータ圧縮/伸長ライブラリで、Macにも標準でインストールされています。
 使い方は、以下のサイトを参考にさせて頂きました。(他にも沢山あります。)

 参考サイト(1):zlib の使い方

 上記サイトによると、圧縮/伸長の方法としては、分割方式(deflateとinflate)と一括方式(compressとuncompress)があるとのことです。
 Xojoでの利用にあたり、そのまま利用できるものがないか、まずは例によってmacoslibを調べたところ、compress2とuncompressを使ったメソッドは見つかりました。
 deflateとinflateについては、macoslibになかったため、ウェブ上で探したのですが、Objective-cでの例がいくつか見つかったものの、フルセットではなさそう(?)なので、今回は公式サイトのサンプルソースコードを元に、アレンジすることにしました。

 参考サイト(2):zlib Usage Example(ソースコードのみは、zpipe.cのリンクからダウンロード可)
他に、zlib.hが手元にあると便利だが、Mac上ではどこにあるのかがよく分からなかった(/opt/local/include/zlib.hは見つかったが、バージョンが古い?)ので、例えば以下が参考になる。
参考サイト(3):GitHub - madler/zlib: A massively spiffy yet delicately unobtrusive compression library.
 また、これは実装してから判明したことですが、初期化のためのメソッド(deflateInitとinflateInit)が見つからないという事態に。
 あれこれ調べていたら、以下のサイトがヒットしました。

 参考サイト(4):dylib がいなくなった、tbd ってなんだ? - Qiita

 ここの、シンボル一覧のスクリーンショットには、deflateInitとinflateInitはなく、代わりにdeflateInit_とinflateInit_があります。そこで、deflateInit_とinflateInit_を使ってみたところ、とりあえずうまくいっているようだったので、これを使うことにしました。(本来は内部関数で、外部から使うべきではないようなので、本当にこれでいいかは要検証。)
参考サイト(3)の本来の趣旨である、dylibからtbdへの変更は、Xojoの場合は不要のようで、"libz.dylib"を指定すればOK。
 なお、deflateInit_とinflateInit_は、バージョンとz_stream構造体のサイズを指定する必要があるのですが、バージョンの方は必ずしも使用中のもの(おそらく1.2.11と思われるが、詳細不明)と一致しなくても問題ないようなのですが、構造体のサイズの方は、uLong型(total_in, total_out, adler, reserved)を(当初64bitかと思っていたのですが、)32bitにしないと、動作しませんでした。

 これらを踏まえて今回は、(1)分割方式、(2)一括方式、の2種類を試してみることにしました。
 それぞれの仕様は以下の通りとしました。

 共通
 分割方式
 一括方式

 Xojoでの実装

 今回のサンプルは互換性検証の利便性を考えて、二つ分をまとめて一つのプロジェクトとしています。
【ソースコードのコピー&ペーストについて】
ソースコード(グレー背景部分の全文)をコピーし、指定のウィンドウ/クラスにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
ただし、この方法は、メソッドでは問題ないようですが、イベント/アクション/プロパティでは不安定?なので、ペーストできない場合は、各項目のカッコ内を適用して下さい。
  1. Xojoで新規プロジェクトを作成
  2. ファイルタイプグループをプロジェクトに追加(Name:FileTypes1)し、ファイルタイプの追加(書類に+アイコン)を押し、名前:PictZlib、DisplayName:PictureZlib、Extensions:.piczを追加。
  3. Window1に、ImageWell(Name:ImageWell1)、PushButton4個(Name:PushButton1, Caption:Save 分割、Name:PushButton2, Caption:Open 分割、Name:PushButton3, Caption:Save 一括、Name:PushButton4, Caption:Open 一括)を追加
  4. ImageWell1に、言語リファレンス>>ImageWell>Examples>drag and dropの項のコードを記述。
  5. 以下をPushButton1にペースト(できなければ、Sub - Endの間をActionイベントに記述)
    Sub Action() Handles Action
      SaveFileDeflate()
    End Sub
    
  6. 以下をPushButton2にペースト(できなければ、Sub - Endの間をActionイベントに記述)
    Sub Action() Handles Action
      OpenFileInflate()
    End Sub
    
  7. 以下をPushButton3にペースト(できなければ、Sub - Endの間をActionイベントに記述)
    Sub Action() Handles Action
      SaveFileCompress()
    End Sub
    
  8. 以下をPushButton4にペースト(できなければ、Sub - Endの間をActionイベントに記述)
    Sub Action() Handles Action
      OpenFileUncompress()
    End Sub
    
  9. 以下をWindow1にペースト
    Protected Sub OpenFileInflate()
      Dim f As FolderItem
      
      // ファイル保存ダイアログ
      Dim dlg As New OpenDialog
      dlg.Filter = FileTypes1.PictZlib
      f=dlg.ShowModalWithin(self)
      if f=nil then
        return
      end if
      
      // ファイルを読み込みながら伸長
      Dim fx As New Xojo.IO.FolderItem(f.NativePath.ToText)
      Dim xmb As Xojo.Core.MemoryBlock = OpenFileInflateZlib(fx)
      if xmb = nil then
        return
      end if
      
      // Xojo.Core.MemoryBlockに格納後、MemoryBlockに変換
      Dim temp As MemoryBlock = xmb.Data
      Dim mb As New MemoryBlock(xmb.Size)
      mb.StringValue(0, mb.Size) = temp.StringValue(0, mb.Size)
      
      // データをイメージウェルにセット
      ImageWell1.Image = Picture.FromData(mb)
    End Sub
    
  10. 以下をWindow1にペースト
    Protected Function OpenFileInflateZlib(fx As Xojo.IO.FolderItem) as Xojo.Core.MemoryBlock
      Const CHUNK As Integer = 16384
      
      Dim ret, offset, lngh As Integer
      Dim have As UInteger
      Dim strm As z_stream
      Dim out As new Xojo.Core.MemoryBlock(CHUNK)
      Dim dest As Xojo.Core.MutableMemoryBlock = New Xojo.Core.MutableMemoryBlock(0)
      
      strm.zalloc = nil
      strm.zfree = nil
      strm.opaque = nil
      
      Dim ver As Xojo.Core.MemoryBlock = Xojo.Core.TextEncoding.UTF8.ConvertTextToData("1.2.11")  // zlibのバージョン
      soft declare function inflateInit_ lib "libz.dylib" (byRef strm as z_stream, ver as Ptr, size as Integer) as integer
      ret = inflateInit_(strm, ver.Data, strm.Size)
      if ret<>0 then
        // -4 = Z_MEM_ERROR メモリ不足でmalloc()できなかった
        // -5 = Z_BUF_ERROR 出力バッファのサイズ不足
        // -6 = Z_VERSION_ERROR 呼び出し側の想定したzlibライブラリバージョンと互換性がない、またはz_streamのサイズが一致しない
        return nil
      end if
      
      Dim bs As Xojo.IO.BinaryStream = Xojo.IO.BinaryStream.Open(fx, Xojo.IO.BinaryStream.LockModes.Read)
      Dim dmy As UInt64 = bs.ReadUInt64  // 伸長後のデータサイズの読み込み(この方式では使わないが、一括方式と互換性を持たせるための処置)
      offset=8  // データサイズのバイト分、読み込み開始位置を移動
      
      soft declare function inflate lib "libz.dylib" (byRef strm as z_stream, fin as Integer) as integer
      soft declare sub inflateEnd lib "libz.dylib" (byRef strm as z_stream)
      
      do
        
        if offset+CHUNK>bs.Length then
          lngh=bs.Length-offset
        else
          lngh=CHUNK
        end if
        strm.avail_in = lngh
        if strm.avail_in = 0 then
          break
        end if
        Dim inn As new Xojo.Core.MemoryBlock(lngh)
        bs.Position=offset
        inn=bs.Read(lngh)
        strm.next_in = inn.Data
        offset=offset+CHUNK
        
        do
          
          strm.avail_out = CHUNK
          strm.next_out = out.Data
          ret = inflate(strm, 0)  // 0 = Z_NO_FLUSH
          select case ret
          case  2 //  2 = Z_NEED_DICT
            ret = -3 // Z_DATA_ERROR
          case -3 // -3 = Z_DATA_ERROR
          case -4 // -4 = Z_MEM_ERROR
            inflateEnd(strm)
            return nil
          end select
          have = CHUNK - strm.avail_out
          Dim out2 As new Xojo.Core.MemoryBlock(have)
          out2=out.Mid(0,have)
          dest.Append(out2)
          out2=nil
          
        loop until strm.avail_out <> 0
        
        inn=nil
        
      loop until ret = 1  // 1 = Z_STREAM_END
      
      inflateEnd(strm)
      
      return dest
    End Function
    
  11. 以下をWindow1にペースト
    Protected Sub OpenFileUncompress()
      Dim f As FolderItem
      
      // ファイル保存ダイアログ
      Dim dlg As New OpenDialog
      dlg.Filter = FileTypes1.PictZlib
      f=dlg.ShowModalWithin(self)
      if f=nil then
        return
      end if
      
      // ファイルの内容を読み込み
      Dim fx As New Xojo.IO.FolderItem(f.NativePath.ToText)
      Dim bs As Xojo.IO.BinaryStream = Xojo.IO.BinaryStream.Open(fx, Xojo.IO.BinaryStream.LockModes.Read)  // ファイルを読み込みモードで開く
      Dim ll As UInt64 = bs.ReadUInt64  // 伸長後のデータサイズの読み込み
      Dim source As Xojo.Core.MemoryBlock = bs.Read(bs.Length-8)  // 圧縮されたデータの読み込み
      bs.Close  // ファイルクローズ
      
      // 伸長
      Dim xmb As Xojo.Core.MemoryBlock = OpenFileUncompressZlib(source,ll)
      if xmb = nil then
        return
      end if
      
      // Xojo.Core.MemoryBlockに格納後、MemoryBlockに変換
      Dim temp As MemoryBlock = xmb.Data
      Dim mb As New MemoryBlock(xmb.Size)
      mb.StringValue(0, mb.Size) = temp.StringValue(0, mb.Size)
      
      // データをイメージウェルにセット
      ImageWell1.Image = Picture.FromData(mb)
    End Sub
    
  12. 以下をWindow1にペースト
    Protected Function OpenFileUncompressZlib(source as Xojo.Core.MemoryBlock, expectedMaxSize as integer) as Xojo.Core.MemoryBlock
      // 一括圧縮したデータではexpectedMaxSizeで問題ないが、分割圧縮したデータではバッファサイズ不足になるので余裕をみている
      Dim dest As Xojo.Core.MemoryBlock = New Xojo.Core.MemoryBlock( expectedMaxSize*1.2 )
      Dim destSize As UInt32 = dest.Size
      
      soft declare function uncompress lib "libz.dylib" (outBuf as Ptr, byref outLen as UInt32, inBuf as Ptr, inLen as UInt32) as integer
      Dim err As Integer = uncompress( dest.Data, destSize, source.Data, source.Size)
      
      if err=0 then
        return dest
      else
        // -3 = Z_DATA_ERROR zlibに伸長できないデータが入力された
        // -4 = Z_MEM_ERROR メモリ不足でmalloc()できなかった
        // -5 = Z_BUF_ERROR 出力バッファのサイズ不足
        return nil
      end if
    End Function
    
  13. 以下をWindow1にペースト
    Protected Sub SaveFileCompress()
      Dim f As FolderItem
      
      // ファイル保存ダイアログ
      Dim dlg As New SaveAsDialog
      dlg.Filter = FileTypes1.PictZlib
      f=dlg.ShowModalWithin(self)
      if f=nil then
        return
      end if
      
      // 画像をバイト列に変換
      Dim data As MemoryBlock = ImageWell1.Image.GetData(Picture.FormatBMP)
      
      // バイト列をXojo.Core.MemoryBlockに変換
      Dim source As new Xojo.Core.MemoryBlock(data,data.Size)
      
      // 圧縮
      Dim mb As Xojo.Core.MemoryBlock = SaveFileCompressZlib(source)  // levelを指定していないので、デフォルトの6が使われる
      if mb=nil then
        return
      end if
      
      // 圧縮結果をファイル書き出し
      Dim fx As New Xojo.IO.FolderItem(f.NativePath.ToText)
      Dim bs As Xojo.IO.BinaryStream = Xojo.IO.BinaryStream.Open(fx, Xojo.IO.BinaryStream.LockModes.Write)
      bs.WriteUInt64(data.Size)  // 圧縮前のデータサイズ
      bs.Write(mb)  // 圧縮データ
      bs.Close
    End Sub
    
  14. 以下をWindow1にペースト
    Protected Function SaveFileCompressZlib(source As Xojo.Core.MemoryBlock, level as integer = 6) as Xojo.Core.MemoryBlock
      Dim dest As Xojo.Core.MemoryBlock = New Xojo.Core.MemoryBlock( source.Size * 1.001 + 12 )
      Dim destSize As UInt32 = dest.Size
      
      soft declare function compress2 lib "libz.dylib" (outBuf as Ptr, byref outBufLen as UInt32, inBuf as Ptr, inBufLen as UInt32, level as Integer) as integer
      Dim err As Integer = compress2( dest.Data, destSize, source.Data, source.Size, level )
      
      if err=0 then
        return dest.Left(destSize)
      else
        // -4 = Z_MEM_ERROR メモリ不足でmalloc()できなかった
        // -5 = Z_BUF_ERROR 出力バッファのサイズ不足
        return nil
      end if
    End Function
    
  15. 以下をWindow1にペースト
    Protected Sub SaveFileDeflate()
      Dim f As FolderItem
      
      // ファイル保存ダイアログ
      Dim dlg As New SaveAsDialog
      dlg.Filter = FileTypes1.PictZlib
      f=dlg.ShowModalWithin(self)
      if f=nil then
        return
      end if
      
      // 画像をバイト列に変換
      Dim data As MemoryBlock = ImageWell1.Image.GetData(Picture.FormatBMP)
      
      // バイト列をXojo.Core.MemoryBlockに変換
      Dim source As new Xojo.Core.MemoryBlock(data,data.Size)
      
      // 圧縮しながらファイル書き出し
      Dim fx As New Xojo.IO.FolderItem(f.NativePath.ToText)
      SaveFileDeflateZlib(fx,source,data.Size)
    End Sub
    
  16. 以下をWindow1にペースト
    Protected Sub SaveFileDeflateZlib(fx As Xojo.IO.FolderItem, source As Xojo.Core.MemoryBlock, dsize As Integer)
      Const CHUNK As Integer = 16384
      
      Dim ret, flush, offset, lngh As Integer
      Dim have As UInteger
      Dim strm As z_stream
      Dim out As new Xojo.Core.MemoryBlock(CHUNK)
      
      strm.zalloc = nil
      strm.zfree = nil
      strm.opaque = nil
      
      Dim ver As Xojo.Core.MemoryBlock = Xojo.Core.TextEncoding.UTF8.ConvertTextToData("1.2.11")  // zlibのバージョン
      soft declare function deflateInit_ lib "libz.dylib" (byRef strm as z_stream, level as Integer, ver as Ptr, size as Integer) as integer
      ret = deflateInit_(strm, -1, ver.Data, strm.Size)  // -1 = Z_DEFAULT_COMPRESSION
      if ret<>0 then
        // -4 = Z_MEM_ERROR メモリ不足でmalloc()できなかった
        // -5 = Z_BUF_ERROR 出力バッファのサイズ不足
        // -6 = Z_VERSION_ERROR 呼び出し側の想定したzlibライブラリバージョンと互換性がない、またはz_streamのサイズが一致しない
        return
      end if
      
      soft declare function deflate lib "libz.dylib" (byRef strm as z_stream, fin as Integer) as integer
      soft declare sub deflateEnd lib "libz.dylib" (byRef strm as z_stream)
      
      Dim bs As Xojo.IO.BinaryStream = Xojo.IO.BinaryStream.Open(fx, Xojo.IO.BinaryStream.LockModes.Write)
      bs.WriteUInt64(dsize)  // 圧縮前のデータサイズ(この方式では使わないが、一括方式と互換性を持たせるための処置)
      
      do
        
        if offset+CHUNK>source.Size then
          lngh=source.Size-offset
          flush=4  // 4 = Z_FINISH
        else
          lngh=CHUNK
          flush=0  // 0 = Z_NO_FLUSH
        end if
        strm.avail_in = CHUNK
        Dim inn As new Xojo.Core.MemoryBlock(lngh)
        inn=source.Mid(offset,lngh)
        strm.next_in = inn.Data
        offset=offset+CHUNK
        
        do
          
          strm.avail_out = CHUNK
          strm.next_out = out.Data
          ret = deflate(strm, flush)
          have = CHUNK - strm.avail_out
          Dim out2 As new Xojo.Core.MemoryBlock(have)
          out2=out.Mid(0,have)
          bs.Write(out2)  // Write to File
          out2=nil
          
        loop until strm.avail_out <> 0
        
        inn=nil
        
      loop until flush = 4  // 4 = Z_FINISH
      
      deflateEnd(strm)
      
      bs.Close
    End Sub
    
  17. 以下をWindow1の構造体(Structures)に追加
    Structure z_stream
      next_in As Ptr
      avail_in As Uinteger
      total_in As Uinteger
      next_out As Ptr
      avail_out As UInteger
      total_out As Uinteger
      msg As Ptr
      state As Ptr
      zalloc As Ptr
      zfree As Ptr
      opaque As Ptr
      data_type As Integer
      adler As Uinteger
      reserved As Uinteger
    End Structure
    
 実行してみたところ、圧縮/伸長が機能することを確認しました。
S Shot1


 おわりに

 テストには、非圧縮フォーマットであるBMP形式のデータを用いました。
 同一データを圧縮しても、分割と一括でファイルサイズが異なりますが、これは(1) 圧縮率が異なる、(2) 内部フォーマットが異なる、等が考えられますが、詳細は未調査です。
元ファイル(BMP形式) 圧縮ファイル(上段:分割、下段:一括)
76,854 byte
( 160 x 120 pixel )
60,823 byte
52,763 byte
5,706,054 byte
( 1,500 x 951 pixel )
1,709,248 byte
1,704,919 byte
 また、一括伸長する場合は、元データのサイズを指定する必要がありますが、一括圧縮したファイルでは問題ないのですが、分割圧縮したファイルでは、バッファサイズを多めに取らないとエラーになる、といった状況になります。(とりあえず20%位多めに取っておけば正常処理するので、詳細の追求はしていません。)
 この問題を除けば、互換性(圧縮と伸長の全ての組み合わせ)はとれているので、圧縮/伸長処理自体は動作しているとみて良さそうです。

 ちなみに、jpeg形式のデータは、圧縮すると3倍近くの大きさになってしまいました。

 最後に、分割と一括を併用する必要性はないのでどちらかを選ぶことになりますが、最近はメモリーの制約をあまり考えなくてもよくなっていることと、deflateInit_とinflateInit_の妥当性検証を回避できることから、一括が簡単で実用的かと。


 お世話になったサイト

 貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)

 参考サイト(1):zlib の使い方
 参考サイト(2):zlib Usage Example(ソースコードのみは、zpipe.cのリンクからダウンロード可)
 参考サイト(3):GitHub - madler/zlib: A massively spiffy yet delicately unobtrusive compression library.
 参考サイト(4):dylib がいなくなった、tbd ってなんだ? - Qiita


 更新履歴

 2019.03.26 新規作成


[Home]  [MacSoft]  [Donation]  [History]  [Privacy Policy]  [Affiliate Policy]