barebone export mp3 cover (jpeg / png) no dll's

Post your FreeBASIC source, examples, tips and tricks here. Please don’t post code without including an explanation.
Post Reply
thrive4
Posts: 70
Joined: Jun 25, 2021 15:32

barebone export mp3 cover (jpeg / png) no dll's

Post by thrive4 »

further updates:
Integrated into cmdsqlite
See: https://github.com/thrive4/util.fb.cmdsqlite

Code exports the mp3 cover art, if present, to a temp folder 'cover'
as a bonus an attempt is made to get the image dimensions of the
exported jpeg or png.

usage: mp3cover.exe <file> single file or <path> ex. <g:\data\mp3\soul food>"
for multiple mp3 files the path is scanned recursively"
/? or -man shows above help

Just a few things about the code I am definitely 'shooting from the hip'
here the checks are very rudimentary plus both the mp3 header
and the image headers of jpeg and png are a bit more complex see
code annotations for more info.
In other words use with caution and if somebody wants to chip in
and enhance / correct the possible boo boo's by all means have a go.

update v1.0 20/03/2023
- fixed incorrect width dimension for png
- better thumbnail detection in jpeg
pattern ffd8 ffd8 ffd9 ffd9 next to
pattern ffd8 ffd9 ffd8 ffd9
- better png check using exif as check
- misc bugfixes stability and error handling

update 12/02/2023
- reworked most of the code for detecting the jpg / png
> in the mp3 plus attaining the width and height of said
> images (still rough around the edges though).
- added a simple layout detection with a tolerance
> i.e. is the image square or not
- added a thumbnail detection
- added a csv report as output can alternatively be used
> with an sql variant db

more details
For an example of the report output and releases (windows 32bit) see:
https://github.com/thrive4/util.fb.mp3cover

Code: Select all

' export cover art if present in mp3 file jan 2023 by thrive4
' info https://en.wikipedia.org/wiki/ID3
' exif jpeg needs more research    
' progressive DCT-based JPEG if instr(chunk, CHR(&hFF, &hC2)) > 0 then
' baseline DCT-based JPEG if instr(chunk, CHR(&hFF, &hC0)) > 0 then
' needs more research
' see http://home.elka.pw.edu.pl/~mmanowie/psap/neue/1%20JPEG%20Overview.htm
' and https://www.media.mit.edu/pia/Research/deepview/exif.html
' usefull tool for importing csv https://sqlitebrowser.org/

' dir function and provides constants to use for the attrib_mask parameter
#include once "vbcompat.bi"
#include once "dir.bi"

dim filename    as string
dim tempfolder  as string = exepath + "\cover"
dim listitem    as string
dim maxitems    as integer = 0
dim itemnr      as integer = 0
dim f           as integer
dim chk         as boolean
dim nocover     as string = ""
common shared coverwidth    as integer
common shared coverheight   as integer
common shared report        as string
common shared thumbnail     as string
common shared layout        as string
common shared csv           as string
csv = "'filename', 'width', 'height', 'thumbnail'" + chr$(13) + chr$(10)
report    = ""
thumbnail = ""
 
' parse arguments
dim as boolean validarg = false
if command(1) = "/?" or command(1) = "-man" or command(1) = "" then
    print "export cover art from mp3 file(s)"
    print "usage: mp3cover <file> single file or <path> ex. <g:\data\mp3\soul food>"
    print "       for multiple mp3 files the path is scanned recursively"    
    print "       an additional mp3cover.csv is generated for futher analysis"    
    sleep
    end
end if
select case true
    case instr(command(1), ".mp3") > 0
        validarg = true
    case instr(command(1), "\") > 0
        validarg = true
    case else
        print "error: invalid switch " + command(1) + " valid switches are file or path"
        sleep
        end
end select

' attempt to extract and write cover art of mp3 to temp thumb file
Function getmp3cover(filename As String, temp as string) As boolean
    Dim buffer  As String
    dim chunk   as string
    dim length  as string
    dim bend    as integer
    dim ext     as string = ""
    dim image   as string
    dim thumb   as integer = 0
    report = ""
    Open filename For Binary Access Read As #1
        If LOF(1) > 0 Then
            buffer = String(LOF(1), 0)
            Get #1, , buffer
        End If
    Close #1
    if instr(1, buffer, "APIC") > 0 then
        length = mid(buffer, instr(buffer, "APIC") + 4, 4)
        ' ghetto check funky first 4 bytes signifying length image
        ' not sure how reliable this info is
        ' see comment codecaster https://stackoverflow.com/questions/47882569/id3v2-tag-issue-with-apic-in-c-net
        if val(asc(length, 1) & asc(length, 2)) = 0 then
            bend = (asc(length, 3) shl 8) or asc(length, 4)
        else
            bend = (asc(length, 1) shl 24 + asc(length, 2) shl 16 + asc(length, 3) shl 8 or asc(length, 4))
        end if
        ' get image dimensions jpg
        ' aided by https://www.freebasic.net/forum/viewtopic.php?t=21922&hilit=instr+hex+search&start=15
        ' and https://stackoverflow.com/questions/18264357/how-to-get-the-width-height-of-jpeg-file-without-using-library
        if instr(1, buffer, "JFIF") > 0 then
            ' override end jpg if marker FFD9 is present
            if instr(buffer, CHR(&hFF, &hD9)) > 0 then
                bend = instr(1, mid(buffer, instr(1, buffer, "JFIF")), CHR(&hFF, &hD9)) + 7
            end if
            chunk = mid(buffer, instr(buffer, "JFIF") - 6, bend)
            ' thumbnail detection
            if instr(instr(1, buffer, "JFIF") + 4, buffer, "JFIF") > 0 then
                thumbnail = thumbnail + "thumbnail in " + filename + chr$(13) + chr$(10)
                thumb = 1
                chunk = mid(buffer, instr(10, buffer, CHR(&hFF, &hD8)), instr(instr(buffer, CHR(&hFF, &hD9)) + 1, buffer, CHR(&hFF, &hD9)) - (instr(10, buffer, CHR(&hFF, &hD8)) - 2))
                ' thumbnail in thumbnail edge case ffd8 ffd8 ffd9 ffd9 pattern in jpeg
                if instr(chunk, CHR(&hFF, &hD8, &hFF)) > 0 then
                    chunk = mid(buffer,_
                    instr(1,buffer, CHR(&hFF, &hD8)),_
                    instr(instr(instr(instr(1,buffer, CHR(&hFF, &hD9)) + 1, buffer, CHR(&hFF, &hD9)) + 1, buffer, CHR(&hFF, &hD9))_
                    , buffer, CHR(&hFF, &hD9)) + 2 - instr(buffer, CHR(&hFF, &hD8)))
                end if
            end if
            if instr(chunk, CHR(&hFF, &hC2)) > 0 then
                coverwidth  = ((asc(mid(chunk, instr(chunk, CHR(&hFF, &hC2)) + 7, 1)) shl 8) or asc(mid(chunk, instr(chunk, CHR(&hFF, &hC2)) + 8, 1)))
                coverheight = ((asc(mid(chunk, instr(chunk, CHR(&hFF, &hC2)) + 5, 1)) shl 8) or asc(mid(chunk, instr(chunk, CHR(&hFF, &hC2)) + 6, 1)))
            else
                coverwidth  = ((asc(mid(chunk, instr(chunk, CHR(&hFF, &hC0)) + 7, 1)) shl 8) or asc(mid(chunk, instr(chunk, CHR(&hFF, &hC0)) + 8, 1)))
                coverheight = ((asc(mid(chunk, instr(chunk, CHR(&hFF, &hC0)) + 5, 1)) shl 8) or asc(mid(chunk, instr(chunk, CHR(&hFF, &hC0)) + 6, 1)))
            end if
            ext = ".jpg"
        end if
        ' use ext and exif check to catch false png
        if instr(1, buffer, "‰PNG") > 0 and instr(1, buffer, "Exif") = 0 and ext = "" then
            ' override end png if tag is present
            if instr(1, buffer, "IEND") > 0 then
                bend = instr(1, mid(buffer, instr(1, buffer, "‰PNG")), "IEND") + 7
            end if
            chunk = mid(buffer, instr(buffer, "‰PNG"), bend)
            ' get image dimensions png
            ' aided by see post by Ry- https://stackoverflow.com/questions/15327959/get-height-and-width-dimensions-from-base64-png
            ' and https://www.w3.org/TR/PNG-Chunks.html
            ' width
            length = mid(chunk, instr(chunk, "IHDR") + 4, 4)
            if val(asc(length, 1) & asc(length, 2)) = 0 then
                coverwidth  = cint("&H" + hex(asc(length, 3)) & hex(asc(length, 4)))
            else
                coverwidth  = cint("&H" + hex(asc(length, 1)) & hex(asc(length, 2)) & hex(asc(length, 3)) & hex(asc(length, 4)))
            end if
            ' height
            length = mid(chunk, instr(chunk, "IHDR") + 8, 4)
            if val(asc(length, 1) & asc(length, 2)) = 0 then
                coverheight = cint("&H" + hex(asc(length, 3)) & hex(asc(length, 4)))
            else
                coverheight = cint("&H" + hex(asc(length, 1)) & hex(asc(length, 2)) & hex(asc(length, 3)) & hex(asc(length, 4)))
            end if
            ext = ".png"
        end if
        ' funky variant for non jfif and jpegs video encoding?
        if (instr(1, buffer, "Lavc58") > 0 or instr(1, buffer, "Exif") > 0) and ext = "" then
            ' override end jpg if marker FFD9 is present
            if instr(buffer, CHR(&hFF, &hD9)) > 0 then
                bend = instr(1, mid(buffer, instr(1, buffer, "Exif")), CHR(&hFF, &hD9)) + 7
            end if
            if instr(1, buffer, "Exif") > 0 then
                chunk = mid(buffer, instr(buffer, "Exif") - 6, bend)
            else
                chunk = mid(buffer, instr(buffer, "Lavc58") - 6, bend)
            end if
            if instr(chunk, CHR(&hFF, &hC2)) > 0 then
                coverwidth  = ((asc(mid(chunk, instr(chunk, CHR(&hFF, &hC2)) + 7, 1)) shl 8) or asc(mid(chunk, instr(chunk, CHR(&hFF, &hC2)) + 8, 1)))
                coverheight = ((asc(mid(chunk, instr(chunk, CHR(&hFF, &hC2)) + 5, 1)) shl 8) or asc(mid(chunk, instr(chunk, CHR(&hFF, &hC2)) + 6, 1)))
            else
                coverwidth  = ((asc(mid(chunk, instr(chunk, CHR(&hFF, &hC0)) + 7, 1)) shl 8) or asc(mid(chunk, instr(chunk, CHR(&hFF, &hC0)) + 8, 1)))
                coverheight = ((asc(mid(chunk, instr(chunk, CHR(&hFF, &hC0)) + 5, 1)) shl 8) or asc(mid(chunk, instr(chunk, CHR(&hFF, &hC0)) + 6, 1)))
            end if
            ext = ".jpg"
        end if
        ' last resort just check on begin and end marker very tricky...
        ' see https://stackoverflow.com/questions/4585527/detect-end-of-file-for-jpg-images#4614629
        if instr(buffer, CHR(&hFF, &hD8)) > 0 and ext = "" then
            chunk = mid(buffer, instr(1, buffer, CHR(&hFF, &hD8)), instr(1, buffer, CHR(&hFF, &hD9)))
            ext = ".jpg"
            if instr(chunk, CHR(&hFF, &hC2)) > 0 then
                coverwidth  = ((asc(mid(chunk, instr(chunk, CHR(&hFF, &hC2)) + 7, 1)) shl 8) or asc(mid(chunk, instr(chunk, CHR(&hFF, &hC2)) + 8, 1)))
                coverheight = ((asc(mid(chunk, instr(chunk, CHR(&hFF, &hC2)) + 5, 1)) shl 8) or asc(mid(chunk, instr(chunk, CHR(&hFF, &hC2)) + 6, 1)))
            else
                coverwidth  = ((asc(mid(chunk, instr(chunk, CHR(&hFF, &hC0)) + 7, 1)) shl 8) or asc(mid(chunk, instr(chunk, CHR(&hFF, &hC0)) + 8, 1)))
                coverheight = ((asc(mid(chunk, instr(chunk, CHR(&hFF, &hC0)) + 5, 1)) shl 8) or asc(mid(chunk, instr(chunk, CHR(&hFF, &hC0)) + 6, 1)))
            end if
        end if
        buffer = ""
        Close #1
        ' attempt to write coverart to temp file
        if ext <> "" then
            image = exepath + "\cover\" + temp + ext
            open image for Binary Access Write as #1
                put #1, , chunk
            close #1
        else
            ' optional use folder.jpg if present as thumb
        end if
        ' report check for square layout with tolerance
        if coverwidth > 0 and coverheight > 0 then
            select case coverwidth / coverheight
                case is > 1.1
                    layout = layout + "coverart not square " + "w: " & coverwidth  & " / h: " & coverheight & " - " & filename + chr$(13) + chr$(10)
                case is < 0.9
                    layout = layout + "coverart not square " + "w: " & coverwidth  & " / h: " & coverheight & " - " & filename + chr$(13) + chr$(10)
            end select
            'print filename + "w" & coverwidth & " h" & coverheight & " ratio " & coverwidth / coverheight
        end if   
        report = report + "w: " & coverwidth
        report = report + " / h: " & coverheight
        report = report + " - " + filename
        csv = csv + chr$(34) + filename + chr$(34) + "," & coverwidth & "," & coverheight & "," & thumb & chr(13) + chr$(10)
        print report

        return true
    else
        return false
        ' no thumb generated remove old thumb if present
        'delfile(exepath + "\thumb.jpg")
        'delfile(exepath + "\thumb.png")
    end if
end function

function createlist(folder as string, filterext as string, listname as string) as integer
    ' setup filelist
    dim chk as boolean
    redim path(1 to 1) As string
    dim as integer i = 1, n = 1, attrib
    dim file as string
    dim fileext as string
    dim maxfiles as integer
    dim f as integer
    f = freefile
    dim filelist as string = exepath + "\" + listname + ".tmp"
    open filelist for output as #f

    #ifdef __FB_LINUX__
      const pathchar = "/"
    #else
      const pathchar = "\"
    #endif

    ' read dir recursive starting directory
    path(1) = folder 
    if( right(path(1), 1) <> pathchar) then
        file = dir(path(1), fbNormal or fbDirectory, @attrib)
        if( attrib and fbDirectory ) then
            path(1) += pathchar
        end if
    end if

    while i <= n
    file = dir(path(i) + "*" , fbNormal or fbDirectory, @attrib)
        while file > ""
            if (attrib and fbDirectory) then
                if file <> "." and file <> ".." then
                    n += 1
                    redim preserve path(1 to n)
                    path(n) = path(i) + file + pathchar
                end if
            else
                fileext = lcase(mid(file, instrrev(file, ".")))
                if instr(1, filterext, fileext) > 0 and len(fileext) > 3 then 
                    print #f, path(i) & file
                    maxfiles += 1
                else
                    'logentry("warning", "file format not supported - " + path(i) & file)
                end if    
            end if
            file = dir(@attrib)
        wend
        i += 1
    wend
    close(f)

    ' chk if filelist is created
    if FileExists(filelist) = false then
        print "could not create filelist: " + filelist
        exit function
    end if
    
    ' setup base shuffle and reduce probability
    dim lastitem as string = exepath + "\" + listname + ".lst"
    
    return maxfiles

end function

' export covers to jpeg or png file(s)
mkdir (tempfolder) ' create export folder regardless
print "scanning and exporting mp3 covers(s)...."
if instr(command(1), ".mp3") > 0 then
    filename = lcase(mid(command(1), instrrev(command(1), "\") + 1))
    filename =  lcase(mid(filename, 1, instr(filename, ".") - 1))
    getmp3cover(command(1), filename)
    itemnr = 1
else
    createlist(command(1), ".mp3", "cover")
    open "cover.tmp" for input as 10
    Do Until EOF(10)
        Line Input #10, listitem
        filename = lcase(mid(listitem, instrrev(listitem, "\") + 1))
        filename =  lcase(mid(filename, 1, instrrev(filename, ".") - 1))
        if getmp3cover(listitem, filename) then
            itemnr += 1
        else
            nocover = nocover + "no cover art found in " + filename + chr$(13) + chr$(10)
            csv = csv + chr$(34) + command(1) + "\" + filename + chr$(34) + ",0,0" + chr$(13) + chr$(10)
        end if
        listitem = ""
        maxitems += 1
    loop
    close 10
    ' cleanup listplay files
    If Kill(exepath + "\cover.tmp") <> 0 Then
        print "error deleting cover.tmp"
    end if
end if

' report to command line
print nocover
if thumbnail = "" then
    print "no thumbnail(s) found in scanned files"
    print
else
    print thumbnail
end if
if layout = "" then
    print "all scanned file(s) are sqare"
    print    
else
    print layout
end if
print "finished scanning " & maxitems & " file(s)"
print "exported " & itemnr & " covers(s) to " + tempfolder

' export results as csv
open "mp3cover.csv" for output encoding "utf8" as #1
print #1, csv
close

end
Post Reply