class ZSteg::CLI::Reflow

Constants

DEFAULT_ACTIONS

Public Class Methods

new(argv = ARGV) click to toggle source
# File lib/zsteg/cli/reflow.rb, line 10
def initialize argv = ARGV
  @argv = argv
  @cache = {}
  @wasfiles = Set.new
end

Public Instance Methods

load_image(fname) click to toggle source
# File lib/zsteg/cli/reflow.rb, line 125
def load_image fname
  if File.directory?(fname)
    puts "[?] #{fname} is a directory".yellow
  else
    ZPNG::Image.load(fname)
  end
rescue ZPNG::Exception, Errno::ENOENT
  puts "[!] #{$!.inspect}".red
end
parse_dimension(s) click to toggle source
# File lib/zsteg/cli/reflow.rb, line 114
def parse_dimension s
  s.split(',').map do |x|
    case x
    when /\A\d+\Z/        # single value
      x.to_i
    when /-/              # range
      Range.new(*x.split('-').map(&:to_i)).to_a
    end
  end.flatten.uniq
end
reflow() click to toggle source

actions

# File lib/zsteg/cli/reflow.rb, line 138
def reflow
  if @image.format != :bmp
    STDERR.puts "[!] only BMP format supported for now!"
    return
  end

  sl = @image.scanlines.first
  @bpp = sl.bpp
  @old_significant_sl_bytes = (sl.width*sl.bpp/8.0).ceil
  @old_total_sl_bytes       = sl.size

  if @options[:heights]
    @options[:heights].each do |h|
      t = 1.0*@image.width*@image.height/h
      t.floor.upto(t.ceil).each do |w|
        next if @options[:widths] && !@options[:widths].include?(w)
        _reflow w,h
      end
    end
  elsif @options[:widths]
    @options[:widths].each do |w|
      h = @image.width*@image.height/w
      _reflow w,h
    end
  elsif @options[:try_all]
    # enum all
    2.upto(@image.width*@image.height/2) do |w|
      h = @image.width*@image.height/w
      _reflow w,h
    end
  else
    # smart all
    w = 4
    loop do
      h = @image.width*@image.height/w
      break if h < 4
      _reflow w,h
      w += 1
    end
  end
end
run() click to toggle source
# File lib/zsteg/cli/reflow.rb, line 16
    def run
      @actions = []
      @options = {
        :verbose   => 0,
      }
      optparser = OptionParser.new do |opts|
        opts.banner = "Usage: #{File.basename($0)} [options] filename.png [param_string]"
        opts.separator ""

        opts.on( "-W", "--width X", "reflow to specified width(s)",
                                    "single value: '999', range: '100-200'",
                                    "or comma-separated: '100,200,300-350'"
        ) do |x|
#          if @options[:heights]
#            STDERR.puts "[!] width _OR_ height can be set".red
#            exit 1
#          end
          @options[:widths] = parse_dimension(x)
        end

        opts.on "-H", "--height X", "reflow to specified height(s)" do |x|
#          if @options[:widths]
#            STDERR.puts "[!] width _OR_ height can be set".red
#            exit 1
#          end
          @options[:heights] = parse_dimension(x)
        end

        opts.separator ""

        opts.on "-a", "--all", "try all possible sizes" do
          @options[:try_all] = true
        end

        opts.on "-r", "--rewrite", "just rewrite the header, keeping imagedata as-is" do
          @options[:rewrite] = true
        end

        opts.separator ""

        opts.on "-O", "--outfile FILENAME", "output single result to specified file" do |x|
          @options[:outfile] = x
        end

        opts.on "-D", "--dir DIRNAME", "output multiple results to specified dir" do |x|
          @options[:dir] = x
        end

        opts.separator ""
        opts.on "-v", "--verbose", "Run verbosely (can be used multiple times)" do |v|
          @options[:verbose] += 1
        end
        opts.on "-q", "--quiet", "Silent any warnings (can be used multiple times)" do |v|
          @options[:verbose] -= 1
        end
        opts.on "-C", "--[no-]color", "Force (or disable) color output (default: auto)" do |x|
          if defined?(Rainbow) && Rainbow.respond_to?(:enabled=)
            Rainbow.enabled = x
          else
            Sickill::Rainbow.enabled = x
          end
        end
      end

      if (argv = optparser.parse(@argv)).empty?
        puts optparser.help
        return
      end

      @actions = DEFAULT_ACTIONS if @actions.empty?

      argv.each do |arg|
        if arg[','] && !File.exist?(arg)
          @options.merge!(decode_param_string(arg))
          argv.delete arg
        end
      end

      argv.each_with_index do |fname,idx|
        if argv.size > 1 && @options[:verbose] >= 0
          puts if idx > 0
          puts "[.] #{fname}".green
        end
        next unless @image=load_image(@fname=fname)

        @actions.each do |action|
          if action.is_a?(Array)
            self.send(*action) if self.respond_to?(action.first)
          else
            self.send(action) if self.respond_to?(action)
          end
        end
      end
    rescue Errno::EPIPE
      # output interrupt, f.ex. when piping output to a 'head' command
      # prevents a 'Broken pipe - <STDOUT> (Errno::EPIPE)' message
    end

Private Instance Methods

_gen_fname(w,h) click to toggle source
# File lib/zsteg/cli/reflow.rb, line 182
def _gen_fname w,h
  ext = @fname[/\.\w{3}$/].to_s
  fname = "%s.reflow_%05dx%05d%s" % [@fname.chomp(ext), w, h, ext]
  fname = File.join(@options[:dir], File.basename(fname)) if @options[:dir]
  fname
end
_reflow(w,h) click to toggle source
# File lib/zsteg/cli/reflow.rb, line 189
    def _reflow w,h
      fname = @options[:outfile] || _gen_fname(w,h)
      raise "already written to #{fname}" if @wasfiles.include?(fname)
      @wasfiles << fname

      new_significant_sl_bytes = (w*@bpp/8.0).ceil
      padding = "\x00" * (4-new_significant_sl_bytes%4)
      padding = "" if padding.size == 4

#      p @old_significant_sl_bytes
#      p @old_total_sl_bytes
#      p new_significant_sl_bytes
#      p padding

      puts "[.] #{fname} .."
      File.open(@fname, "rb") do |fi|
        File.open(fname, "wb") do |fo|
          # 2 bytes - "BM" signature
          # 4 bytes - the size of the BMP file in bytes
          # 2 bytes - reserved
          # 2 bytes - reserved
          fo.write fi.read(2+4+2+2)

          # 4 bytes - imagedata offset
          data = fi.read(4)
          imagedata_offset = data.unpack('V').first
          fo.write data

          # 4 bytes - BITMAPINFOHEADER.biSize    (keep)
          # 4 bytes - BITMAPINFOHEADER.biWidth   (rewrite)
          # 4 bytes - BITMAPINFOHEADER.biHeight  (rewrite)
          data = fi.read(4+4+4)
          fo.write(data[0,4] + [w,h].pack("V2")) # write new size

          if @options[:rewrite]
            IO.copy_stream fi, fo
          else
            # copy remaining header bytes
            fo.write fi.read(imagedata_offset-fi.tell)

            # FIXME: if scanline sizes differ in BITS, not bytes...

            # scanline padding needs to be respected...
            imagedata = StringIO.new
            @image.height.times do
              data = fi.read @old_total_sl_bytes
              imagedata << data[0, @old_significant_sl_bytes]
              #p data[@old_significant_sl_bytes..-1]
            end
            imagedata << fi.read # read extradata, if any

            imagedata.rewind
            imagedata_start = fo.tell
            h.times do
              fo << imagedata.read(new_significant_sl_bytes)
              fo << padding
            end
            file_size = fo.tell
            imagedata_size = fo.tell - imagedata_start
            fo << imagedata.read # write extradata, if any

            # write new BITMAPFILEHEADER.bfSize
            fo.seek 2
            fo.write [file_size].pack('V')

            # write new BITMAPINFOHEADER.biSizeImage
            fo.seek 14+20 # BITMAPFILEHEADER::SIZE + 20
            fo.write [imagedata_size].pack('V')
          end
        end
      end
    end