Skip to content

Instantly share code, notes, and snippets.

@zunda
Created November 2, 2009 06:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zunda/223991 to your computer and use it in GitHub Desktop.
Save zunda/223991 to your computer and use it in GitHub Desktop.
A ruby script to show segments in jpeg files
#!/usr/bin/ruby
#
# usage: ruby inspect-jpeg.rb jpeg-file ...
#
# Copyright (C) 2009 zunda <zunda at freeshell.org>
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
class JpegError < StandardError; end
class Jpeg
def Jpeg::read_onebyte(io)
io.read(1).unpack('C')[0]
end
def Jpeg::read_twobytes(io)
io.read(2).unpack('n')[0]
end
def Jpeg::read_segment_length(io)
io.read(2).unpack('n')[0] - 2
end
# reads data until next marker
def Jpeg::read_data(io)
data = ''
while !io.eof?
t = io.read(1)
if "\xFF" == t
u = io.read(1)
unless "\x00" == u
# Marker for next segment
io.seek(-2, IO::SEEK_CUR)
break
end
end
data += t
end
return data
end
class MarkerBase
@@markers = Array.new
def length(io); 0; end # length of the payload not including two bytes for length data itself
def self.second_byte; raise NotImplementedError; end
def self.name; 'To be implemented'; end
def self.has_data?; false; end
def MarkerBase.inherited(subclass)
@@markers << subclass
end
def self.marker_hash
r = Hash.new
@@markers.each do |marker|
r[marker.second_byte] = marker
end
return r
end
end
class Marker
# http://en.wikipedia.org/wiki/JPEG
class SOI < MarkerBase
def length(io); 0; end
def self.second_byte; 0xD8; end
def self.name; 'Start Of Image'; end
end
class SOF0 < MarkerBase
def self.second_byte; 0xC0; end
def self.name; 'Start Of Frame (Baseline DCT)'; end
def length(io); Jpeg::read_segment_length(io); end
end
class SOF2 < MarkerBase
def self.second_byte; 0xC2; end
def self.name; 'Start Of Frame (Progressive DCT)'; end
def length(io); Jpeg::read_segment_length(io); end
end
class DHT < MarkerBase
def self.second_byte; 0xC4; end
def self.name; 'Define Huffman Table(s)'; end
def length(io); Jpeg::read_segment_length(io); end
end
class DQT < MarkerBase
def self.second_byte; 0xD8; end
def self.name; 'Define Quantization Table(s)'; end
def length(io); Jpeg::read_segment_length(io); end
end
class DRI < MarkerBase
def self.second_byte; 0xDD; end
def self.name; 'Define Restart Interval'; end
def length(io); 2; end
end
class SOS < MarkerBase
def self.second_byte; 0xDA; end
def self.name; 'Start Of Scan'; end
def length(io); Jpeg::read_segment_length(io); end
def self.has_data?; true; end
end
0.upto(7) do |i|
eval <<-_END
class RST#{i} < MarkerBase
def self.second_byte; #{0xD0+i}; end
def self.name; 'Restart'; end
end
_END
end
1.upto(0xf) do |i|
eval <<-_END
class APP#{i} < MarkerBase
def self.second_byte; #{0xE0+i}; end
def self.name; 'Application-specific'; end
def length(io); Jpeg::read_segment_length(io); end
end
_END
end
class COM < MarkerBase
def self.second_byte; 0xFE; end
def self.name; 'Comment'; end
def length(io); Jpeg::read_segment_length(io); end
end
class EOI < MarkerBase
def self.second_byte; 0xD9; end
def self.name; 'End Of Image'; end
end
# http://www.obrador.com/essentialjpeg/headerinfo.htm
class JFIF < MarkerBase
def self.second_byte; 0xE0; end
def self.name; 'JFIF marker'; end
def length(io); Jpeg::read_segment_length(io); end
end
class DQT < MarkerBase
def self.second_byte; 0xDB; end
def self.name; 'Define Quantization Table'; end
def length(io); Jpeg::read_segment_length(io); end
end
@@markers = MarkerBase.marker_hash
def self.with(second_byte)
klass = @@markers[second_byte]
unless klass
raise JpegError, "Unknown marker with second byte 0x%02X" % second_byte
end
klass.new
end
end
class Segment
attr_reader :marker
attr_reader :length # length of the payload not including two bytes for length data itself
attr_reader :payload
attr_reader :data
attr_reader :pos
def initialize(marker, io)
@marker = marker
@pos = io.pos - 2
@length = marker.length(io)
@payload = io.read(@length)
if @marker.class.has_data?
@data = Jpeg::read_data(io)
else
@data = nil
end
end
def to_s
r = "#{"%-4.4s" % @marker.class.to_s.split(/::/)[-1]} pos:#{@pos} len:#{@length}"
r += " data:#{@data.length}" if @data
return r
end
end
def initialize(io)
@io = io
end
def each_segment
while not @io.eof?
t = Jpeg.read_onebyte(@io)
unless 0xFF == t
@io.seek(-1, IO::SEEK_CUR)
raise JpegError, "Segment starts with #{"0x%02X" % t} instead of 0xFF at byte #{@io.pos - 1} in #{@io.path.inspect}"
end
yield Segment.new(Jpeg::Marker::with(Jpeg.read_onebyte(@io)), @io)
end
end
def seek_to_segment
Jpeg::read_data(@io)
end
def eof?
@io.eof?
end
def pos
@io.pos
end
end
if __FILE__ == $0
ARGV.each do |path|
puts path
File.open(path) do |f|
image = Jpeg.new(f)
while !image.eof?
begin
image.each_segment do |segment|
puts " #{segment}"
end
rescue JpegError
pos = image.pos
len = image.seek_to_segment.length
puts " Skipping #{len} bytes from pos:#{pos}"
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment