├── README.md ├── images └── porsche.jpg ├── license.txt └── micro-jpeg-visualizer.py /README.md: -------------------------------------------------------------------------------- 1 | # micro-jpeg-visualizer 2 | 3 | ## Intro 4 | 5 | This a a JPEG visualizer in just ~~280~~ 250 lines in easy to read Python 3.0 code. 6 | 7 | ## Features 8 | 9 | - No external libraries were used 10 | - Friendly code can be easily ported to any other languages and embedded devices 11 | - It works with JPG's that are made of 8x8 blocks and 3 channels (Y, Cr, Cb). 12 | - Its slow, that is because the IDTC, it is done using brute force 13 | 14 | Feel free to drop me a mail if you find this useful :-) 15 | 16 | ## Funny Facts 17 | 18 | - I wrote it just to make sure I understood how JPG work 19 | - It took me 3 evenings to finish it up 20 | - the most difficult part was handling the run length encoding that I had to reverse engineer myself. 21 | - I used notepad and debugged it using print 22 | - I used python because it was the handiest thing I had 23 | - What an amazing feeling to see such a simple piece of code displaying a pic! 24 | 25 | ## Credits 26 | 27 | by Raul Aguaviva 28 | 29 | This project wouldn't have been possible without the hard work of these folks: 30 | 31 | - Calvin Hass for his excellent web page http://www.impulseadventure.com/ 32 | - The editors of the wikipedia JPEG article (that now references this code). 33 | -------------------------------------------------------------------------------- /images/porsche.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aguaviva/micro-jpeg-visualizer/471597fa393ea64822083edd3aff3d65b2f4aa19/images/porsche.jpg -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Raul Aguaviva 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /micro-jpeg-visualizer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Written by Raul Aguaviva as an exercise 3 | # beware not optimized for speed :-) 4 | 5 | from struct import * 6 | import math 7 | 8 | zigzag = [0, 1, 8, 16, 9, 2, 3, 10, 9 | 17, 24, 32, 25, 18, 11, 4, 5, 10 | 12, 19, 26, 33, 40, 48, 41, 34, 11 | 27, 20, 13, 6, 7, 14, 21, 28, 12 | 35, 42, 49, 56, 57, 50, 43, 36, 13 | 29, 22, 15, 23, 30, 37, 44, 51, 14 | 58, 59, 52, 45, 38, 31, 39, 46, 15 | 53, 60, 61, 54, 47, 55, 62, 63] 16 | 17 | def Clamp(col): 18 | col = 255 if col>255 else col 19 | col = 0 if col<0 else col 20 | return int(col) 21 | 22 | def HexDump(data): 23 | for i in range(len(data)): 24 | b, = unpack("B",data[i:i+1]) 25 | print("%02x " % b), 26 | 27 | def ColorConversion(Y, Cr, Cb): 28 | R = Cr*(2-2*.299) + Y 29 | B = Cb*(2-2*.114) + Y 30 | G = (Y - .114*B - .299*R)/.587 31 | return (Clamp(R+128),Clamp(G+128),Clamp(B+128) ) 32 | 33 | def GetArray(type,l, length): 34 | s = "" 35 | for i in range(length): 36 | s =s+type 37 | return list(unpack(s,l[:length])) 38 | 39 | def DecodeNumber(code, bits): 40 | l = 2**(code-1) 41 | if bits>=l: 42 | return bits 43 | else: 44 | return bits-(2*l-1) 45 | 46 | def PrintMatrix( m): 47 | for j in range(8): 48 | for i in range(8): 49 | print("%2f" % m[i+j*8]), 50 | print 51 | print 52 | 53 | def XYtoLin(x,y): 54 | return x+y*8 55 | 56 | def RemoveFF00(data): 57 | datapro = [] 58 | i = 0 59 | while(True): 60 | b,bnext = unpack("BB",data[i:i+2]) 61 | if (b == 0xff): 62 | if (bnext != 0): 63 | break 64 | datapro.append(data[i]) 65 | i+=2 66 | else: 67 | datapro.append(data[i]) 68 | i+=1 69 | return datapro,i 70 | 71 | # helps build a MCU matrix 72 | class IDCT: 73 | def __init__(self): 74 | self.base = [0]*64 75 | 76 | def NormCoeff(self, n): 77 | return math.sqrt( 1.0/8.0) if (n==0) else math.sqrt( 2.0/8.0) 78 | 79 | def AddIDC(self, n,m, coeff): 80 | an = self.NormCoeff(n) 81 | am = self.NormCoeff(m) 82 | 83 | for y in range(0,8): 84 | for x in range(0,8): 85 | nn = an*math.cos( n* math.pi * (x +.5)/8.0 ) 86 | mm = am*math.cos( m* math.pi * (y +.5)/8.0 ) 87 | self.base[ XYtoLin(x, y) ] += nn*mm*coeff 88 | 89 | def AddZigZag(self, zi, coeff): 90 | i = zigzag[zi] 91 | n = i & 0x7 92 | m = i >> 3 93 | self.AddIDC( n,m, coeff) 94 | 95 | # convert a string into a bit stream 96 | class Stream: 97 | def __init__(self, data): 98 | self.data= data 99 | self.pos = 0 100 | 101 | def GetBit(self): 102 | b = self.data[self.pos >> 3] 103 | s = 7-(self.pos & 0x7) 104 | self.pos+=1 105 | return (b >> s) & 1 106 | 107 | def GetBitN(self, l): 108 | val = 0 109 | for i in range(l): 110 | val = val*2 + self.GetBit() 111 | return val 112 | 113 | # Create huffman bits from table lengths 114 | class HuffmanTable: 115 | def __init__(self): 116 | self.root=[] 117 | self.elements = [] 118 | 119 | def BitsFromLengths(self, root, element, pos): 120 | if isinstance(root,list): 121 | if pos==0: 122 | if len(root)<2: 123 | root.append(element) 124 | return True 125 | return False 126 | for i in [0,1]: 127 | if len(root) == i: 128 | root.append([]) 129 | if self.BitsFromLengths(root[i], element, pos-1) == True: 130 | return True 131 | return False 132 | 133 | def GetHuffmanBits(self, lengths, elements): 134 | self.elements = elements 135 | ii = 0 136 | for i in range(len(lengths)): 137 | for j in range(lengths[i]): 138 | self.BitsFromLengths(self.root, elements[ii], i) 139 | ii+=1 140 | 141 | def Find(self,st): 142 | r = self.root 143 | while isinstance(r, list): 144 | r=r[st.GetBit()] 145 | return r 146 | 147 | def GetCode(self, st): 148 | while(True): 149 | res = self.Find(st) 150 | if res == 0: 151 | return 0 152 | elif ( res != -1): 153 | return res 154 | 155 | # main class that decodes the jpeg 156 | class jpeg: 157 | def __init__(self): 158 | self.quant = {} 159 | self.quantMapping = [] 160 | self.tables = {} 161 | self.width = 0 162 | self.height = 0 163 | self.image = [] 164 | 165 | def BuildMatrix(self, st, idx, quant, olddccoeff): 166 | i = IDCT() 167 | code = self.tables[0+idx].GetCode(st) 168 | bits = st.GetBitN(code) 169 | dccoeff = DecodeNumber(code, bits) + olddccoeff 170 | 171 | i.AddZigZag(0,(dccoeff) * quant[0]) 172 | l = 1 173 | while(l<64): 174 | code = self.tables[16+idx].GetCode(st) 175 | if code == 0: 176 | break 177 | if code >15: 178 | l+= (code>>4) 179 | code = code & 0xf 180 | 181 | bits = st.GetBitN( code ) 182 | 183 | if l<64: 184 | coeff = DecodeNumber(code, bits) 185 | i.AddZigZag(l,coeff * quant[l]) 186 | l+=1 187 | return i,dccoeff 188 | 189 | def StartOfScan(self, data, hdrlen): 190 | data,lenchunk = RemoveFF00(data[hdrlen:]) 191 | 192 | st = Stream(data) 193 | 194 | oldlumdccoeff, oldCbdccoeff, oldCrdccoeff = 0, 0, 0 195 | for y in range(self.height//8): 196 | for x in range(self.width//8): 197 | # decode 8x8 block 198 | matL, oldlumdccoeff = self.BuildMatrix(st,0, self.quant[self.quantMapping[0]], oldlumdccoeff) 199 | matCr, oldCrdccoeff = self.BuildMatrix(st,1, self.quant[self.quantMapping[1]], oldCrdccoeff) 200 | matCb, oldCbdccoeff = self.BuildMatrix(st,1, self.quant[self.quantMapping[2]], oldCbdccoeff) 201 | # store it as RGB 202 | for yy in range(8): 203 | for xx in range(8): 204 | self.image[(x*8+xx) + ((y*8+yy) * self.width)] = ColorConversion( matL.base[XYtoLin(xx,yy)] , matCb.base[XYtoLin(xx,yy)], matCr.base[XYtoLin(xx,yy)]) 205 | 206 | return lenchunk +hdrlen 207 | 208 | def DefineQuantizationTables(self, data): 209 | while(len(data)>0): 210 | hdr, = unpack("B",data[0:1]) 211 | #print hdr >>4, hdr & 0xf 212 | self.quant[hdr & 0xf] = GetArray("B", data[1:1+64],64) 213 | #PrintMatrix(self.quant[hdr >>4]) 214 | data = data[65:] 215 | 216 | def BaselineDCT(self, data): 217 | hdr, self.height, self.width, components = unpack(">BHHB",data[0:6]) 218 | print("size %ix%i" % (self.width, self.height)) 219 | self.image = [0] * (self.width * self.height); 220 | 221 | for i in range(components): 222 | id, samp, QtbId = unpack("BBB",data[6+i*3:9+i*3]) 223 | self.quantMapping.append(QtbId) 224 | 225 | def DefineHuffmanTables(self, data): 226 | while(len(data)>0): 227 | off = 0 228 | hdr, = unpack("B",data[off:off+1]) 229 | off+=1 230 | 231 | lengths = GetArray("B", data[off:off+16],16) 232 | off += 16 233 | 234 | elements = [] 235 | for i in lengths: 236 | elements+= (GetArray("B", data[off:off+i], i)) 237 | off = off+i 238 | 239 | hf = HuffmanTable() 240 | hf.GetHuffmanBits( lengths, elements) 241 | self.tables[hdr] = hf 242 | 243 | data = data[off:] 244 | 245 | def decode(self, data): 246 | while(True): 247 | hdr, = unpack(">H", data[0:2]) 248 | if hdr == 0xffd8: 249 | lenchunk = 2 250 | elif hdr == 0xffd9: 251 | break 252 | else: 253 | lenchunk, = unpack(">H", data[2:4]) 254 | lenchunk+=2 255 | chunk = data[4:lenchunk] 256 | if hdr == 0xffdb: 257 | self.DefineQuantizationTables(chunk) 258 | elif hdr == 0xffc0: 259 | self.BaselineDCT(chunk) 260 | elif hdr == 0xffc4: 261 | self.DefineHuffmanTables(chunk) 262 | elif hdr == 0xffda: 263 | lenchunk = self.StartOfScan(data, lenchunk) 264 | 265 | data = data[lenchunk:] 266 | if len(data)==0: 267 | break 268 | return (self.width, self.height, self.image) 269 | 270 | width, height, image = jpeg().decode(open('images/porsche.jpg', 'rb').read()) 271 | 272 | #show image 273 | from PIL import Image 274 | img = Image.new("RGB", (width, height)) 275 | img.putdata(image) 276 | img.show() --------------------------------------------------------------------------------