#!/usr/bin/env ruby # # Script for embedding album cover images in MP3 files. This requires an existing # ID3-V2 tag in the file. # # (c) 2005-2006 Mark Kretschmann # License: GNU General Public License V2 # # Returns the size of the ID3-V2 tag. In other words, the offset from # where the real mp3 data starts. # @see http://id3lib.sourceforge.net/id3/id3v2com-00.html#sec3.1 # def getId3v2Size( data ) return data[6]*2**21 + data[7]*2**14 + data[8]*2**7 + data[9] end # # Sets the size of the ID3-V2 tag. # @see http://id3lib.sourceforge.net/id3/id3v2com-00.html#sec3.1 # def setId3v2Size( data, size ) data[6] = size >> 21 & 0x7f data[7] = size >> 14 & 0x7f data[8] = size >> 7 & 0x7f data[9] = size >> 0 & 0x7f end # # Unsynchronize the entire ID3-V2 tag, frame by frame (replace 0xfff* with 0xff00f*) # def unsynchronize( data ) data[5] |= 0b10000000 # Set Unsychronization global tag flag index = 10 # Skip ID3 Header until data[index] == 0 or data[index..index+2] == "3DI" frametype = data[index..index+3] framesize = data[index+4]*2**21 + data[index+5]*2**14 + data[index+6]*2**7 + data[index+7] # Check if frame is already unsynced if data[index+9] & 0x02 == 0 puts( "#{frametype} unsynced " ) data[index+9] |= 0x02 index += 10 framesize.times() do sync = data[index] == 0xff sync1 = data[index+1] & 0b11100000 == 0b11100000 sync2 = data[index+1] & 0b11111111 == 0b00000000 if sync and ( sync1 or sync2 ) and index < framesize - 2 print( "." ) if sync1 print( "O" ) if sync2 data[index+1, 0] = "\0" framesize += 1 index += 1 end index += 1 end else puts( "#{frametype} synced" ) index += framesize + 10 end end # Index == new tag length return index - 10 end ############################################################################ # MAIN ############################################################################ path = "" destination = "" if $*.length() < 2 or $*[0] == "--help" puts( "Usage: addimage2mp3.rb image source [destination]" ) puts() puts( "Adds an image to the ID3-V2 tag of a MP3 file. Requires an existing ID3-V2 tag in" ) puts( "the file." ) puts() exit( 1 ) end imagepath = $*[0] path = $*[1] destination = path if $*.length() == 3 destination = $*[2] end unless FileTest::readable_real?( imagepath ) puts( "Error: Image not found or not readable." ) exit( 1 ) end unless FileTest::readable_real?( path ) puts( "Error: Source not found or not readable." ) exit( 1 ) end unless File.extname( path ).downcase() == ".mp3" puts( "Error: File is not mp3." ) exit( 1 ) end mimetype = case File.extname( imagepath ).downcase() when ".bmp" then "image/bmp" when ".gif" then "image/gif" when ".jpg" then "image/jpeg" when ".jpeg" then "image/jpeg" when ".png" then "image/png" when "" then "image/png" # Amarok's cover images are always PNG else puts( "Error: Image type invalid." ) exit( 1 ) end file = File.new( path, "r" ) data = file.read() id3length = 0 if data[0,3] == "ID3" id3length = getId3v2Size( data ) puts( "ID3-V2 detected. Tag size: #{id3length}" ) else puts( "ID3-V1 detected." ) puts( "Error: File does not have a ID3-V2 tag." ) exit( 1 ) end file = File.new( imagepath, "r" ) image = file.read() apicframe = String.new() apicframe << 0x00 # text encoding apicframe << mimetype apicframe << 0x00 # mimetype end apicframe << 0x03 # Picture type (Cover front) apicframe << 0x00 # Description (empty) apicframe << image apicheader = String.new() apicheader << "APIC" apicheader << ( ( apicframe.length() >> 21 ) & 0x7f ) apicheader << ( ( apicframe.length() >> 14 ) & 0x7f ) apicheader << ( ( apicframe.length() >> 7 ) & 0x7f ) apicheader << ( ( apicframe.length() >> 0 ) & 0x7f ) apicheader << 0x00 # Flags apicheader << 0x00 # Flags # Find end of last ID3 frame, before padding or footer (if present) puts() puts( "Locating end of last ID3 frame.." ) index = 10 # Skip ID3 Header until data[index] == 0 or data[index..index+2] == "3DI" frametype = data[index..index+3] puts( "Frame Type : #{frametype}" ) if frametype == "APIC" puts( "Error: File already contains an image." ) exit( 1 ) end framesize = data[index+4]*2**21 + data[index+5]*2**14 + data[index+6]*2**7 + data[index+7] index += 10 + framesize end index += 10 if data[index..index+2] == "3DI" # Footer # Insert APIC frame into string, after the last ID3 frame data[index, 0] = apicheader + apicframe id3length += apicheader.length() + apicframe.length() # Unsynchronization isn't supported by TagLib at this point :| # # puts() # puts( "Unsynchronizing tag.." ) # id3length = unsynchronize( data ) # data[5] |= 0b10000000 # Set ID3 Unsychronization flag # Adjust ID3V2 tag size setId3v2Size( data, id3length ) puts() puts( "Adjusting new ID3 size: #{id3length + 10}" ) puts() print( "Writing file.. " ) destfile = File::open( destination, File::CREAT|File::TRUNC|File::WRONLY ) if destfile == nil puts( "Error: Destination file is not writable." ) exit( 1 ) end destfile << data puts( "done." ) puts() puts( "done." )