File: id3pic.go 1 /* 2 The MIT License (MIT) 3 4 Copyright (c) 2026 pacman64 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy of 7 this software and associated documentation files (the "Software"), to deal 8 in the Software without restriction, including without limitation the rights to 9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 of the Software, and to permit persons to whom the Software is furnished to do 11 so, subject to the following conditions: 12 13 The above copyright notice and this permission notice shall be included in all 14 copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 SOFTWARE. 23 */ 24 25 /* 26 To compile a smaller-sized command-line app, you can use the `go` command as 27 follows: 28 29 go build -ldflags "-s -w" -trimpath id3pic.go 30 */ 31 32 package main 33 34 import ( 35 "bufio" 36 "encoding/binary" 37 "errors" 38 "io" 39 "os" 40 ) 41 42 const info = ` 43 id3pic [options...] [file...] 44 45 Extract picture/thumbnail bytes from ID3/MP3 metadata, if available. 46 47 All (optional) leading options start with either single or double-dash: 48 49 -h, -help show this help message 50 ` 51 52 // errNoThumb is a generic error to handle lack of thumbnails, in case no 53 // picture-metadata-starters are found at all 54 var errNoThumb = errors.New(`no thumbnail data found`) 55 56 // errInvalidPIC is a generic error for invalid PIC-format pics 57 var errInvalidPIC = errors.New(`invalid PIC-format embedded thumbnail`) 58 59 func main() { 60 if len(os.Args) > 1 { 61 switch os.Args[1] { 62 case `-h`, `--h`, `-help`, `--help`: 63 os.Stdout.WriteString(info[1:]) 64 return 65 } 66 } 67 68 if len(os.Args) > 2 { 69 showError(`can only handle 1 file`) 70 os.Exit(1) 71 return 72 } 73 74 name := `-` 75 if len(os.Args) > 1 { 76 name = os.Args[1] 77 } 78 79 if err := run(os.Stdout, name); err != nil && err != io.EOF { 80 showError(err.Error()) 81 os.Exit(1) 82 return 83 } 84 } 85 86 func showError(msg string) { 87 os.Stderr.WriteString(msg) 88 os.Stderr.WriteString("\n") 89 } 90 91 func run(w io.Writer, name string) error { 92 if name == `-` { 93 return id3pic(w, bufio.NewReader(os.Stdin)) 94 } 95 96 f, err := os.Open(name) 97 if err != nil { 98 return errors.New(`can't read from file named "` + name + `"`) 99 } 100 defer f.Close() 101 102 return id3pic(w, bufio.NewReader(f)) 103 } 104 105 func match(r *bufio.Reader, what []byte) bool { 106 for _, v := range what { 107 b, err := r.ReadByte() 108 if b != v || err != nil { 109 return false 110 } 111 } 112 return true 113 } 114 115 func id3pic(w io.Writer, r *bufio.Reader) error { 116 // match the ID3 mark 117 for { 118 b, err := r.ReadByte() 119 if err == io.EOF { 120 return errNoThumb 121 } 122 if err != nil { 123 return err 124 } 125 126 if b == 'I' && match(r, []byte{'D', '3'}) { 127 break 128 } 129 } 130 131 for { 132 b, err := r.ReadByte() 133 if err == io.EOF { 134 return errNoThumb 135 } 136 if err != nil { 137 return err 138 } 139 140 // handle APIC-type chunks 141 if b == 'A' && match(r, []byte{'P', 'I', 'C'}) { 142 return handleAPIC(w, r) 143 } 144 } 145 } 146 147 func handleAPIC(w io.Writer, r *bufio.Reader) error { 148 // section-size seems stored as 4 big-endian bytes 149 var size uint32 150 err := binary.Read(r, binary.BigEndian, &size) 151 if err != nil { 152 return err 153 } 154 155 n, err := skipThumbnailTypeAPIC(r) 156 if err != nil { 157 return err 158 } 159 160 _, err = io.Copy(w, io.LimitReader(r, int64(int(size)-n))) 161 return err 162 } 163 164 func skipThumbnailTypeAPIC(r *bufio.Reader) (skipped int, err error) { 165 m, err := r.Discard(2) 166 if err != nil || m != 2 { 167 return -1, errors.New(`failed to sync APIC flags`) 168 } 169 skipped += m 170 171 m, err = r.Discard(1) 172 if err != nil || m != 1 { 173 return -1, errors.New(`failed to sync APIC text-encoding`) 174 } 175 skipped += m 176 177 junk, err := r.ReadSlice(0) 178 if err != nil { 179 return -1, errors.New(`failed to sync to APIC thumbnail MIME-type`) 180 } 181 skipped += len(junk) 182 183 m, err = r.Discard(1) 184 if err != nil || m != 1 { 185 return -1, errors.New(`failed to sync APIC picture type`) 186 } 187 skipped += m 188 189 junk, err = r.ReadSlice(0) 190 if err != nil { 191 return -1, errors.New(`failed to sync to APIC thumbnail description`) 192 } 193 skipped += len(junk) 194 195 return skipped, nil 196 }