├── .github ├── FUNDING.yml └── workflows │ ├── test.yaml │ └── repro.yaml ├── taglib.wasm ├── testdata ├── eg.m4a ├── eg.mp3 ├── eg.ogg ├── eg.wav ├── eg.flac ├── cover.jpg └── normal.flac ├── .gitmodules ├── go.mod ├── go.sum ├── CMakeLists.txt ├── taglib.cpp ├── README.md ├── taglib_test.go ├── taglib.go └── LICENSE /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [sentriz] 2 | -------------------------------------------------------------------------------- /taglib.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sentriz/go-taglib/HEAD/taglib.wasm -------------------------------------------------------------------------------- /testdata/eg.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sentriz/go-taglib/HEAD/testdata/eg.m4a -------------------------------------------------------------------------------- /testdata/eg.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sentriz/go-taglib/HEAD/testdata/eg.mp3 -------------------------------------------------------------------------------- /testdata/eg.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sentriz/go-taglib/HEAD/testdata/eg.ogg -------------------------------------------------------------------------------- /testdata/eg.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sentriz/go-taglib/HEAD/testdata/eg.wav -------------------------------------------------------------------------------- /testdata/eg.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sentriz/go-taglib/HEAD/testdata/eg.flac -------------------------------------------------------------------------------- /testdata/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sentriz/go-taglib/HEAD/testdata/cover.jpg -------------------------------------------------------------------------------- /testdata/normal.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sentriz/go-taglib/HEAD/testdata/normal.flac -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "taglib"] 2 | path = taglib 3 | url = https://github.com/taglib/taglib 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.senan.xyz/taglib 2 | 3 | go 1.24.0 4 | 5 | require github.com/tetratelabs/wazero v1.11.0 6 | 7 | require golang.org/x/sys v0.39.0 // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= 2 | github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= 3 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 4 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 5 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Lint and test 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | pull_request: 7 | jobs: 8 | test: 9 | name: Lint and test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Setup Golang with cache 15 | uses: magnetikonline/action-golang-cache@v5 16 | with: 17 | go-version-file: go.mod 18 | - name: Lint 19 | uses: golangci/golangci-lint-action@v8 20 | with: 21 | install-mode: goinstall 22 | version: v2.4.0 23 | - name: Test 24 | run: go test -race ./... 25 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5.0 FATAL_ERROR) 2 | project(taglib_wasm) 3 | 4 | set(CMAKE_BUILD_TYPE Release) 5 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 6 | set(WITH_ZLIB OFF) 7 | set(BUILD_SHARED_LIBS OFF) 8 | set(BUILD_TESTING OFF) 9 | 10 | add_subdirectory( 11 | taglib 12 | ) 13 | include_directories( 14 | taglib/taglib 15 | taglib/taglib/toolkit 16 | ) 17 | 18 | add_executable(taglib taglib.cpp) 19 | set_target_properties(taglib PROPERTIES SUFFIX ".wasm") 20 | target_compile_options(taglib PRIVATE --target=wasm32-wasi -g0 -O2) 21 | target_link_options(taglib PRIVATE -Wl,--allow-undefined -mexec-model=reactor) 22 | target_link_libraries(taglib PRIVATE tag) 23 | -------------------------------------------------------------------------------- /.github/workflows/repro.yaml: -------------------------------------------------------------------------------- 1 | name: Reproducible build 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags: 6 | - "*" 7 | jobs: 8 | build: 9 | name: Build and attest 10 | runs-on: ubuntu-latest 11 | permissions: 12 | id-token: write 13 | contents: read 14 | attestations: write 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Checkout submodules 19 | run: git submodule update --init --recursive 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v1 22 | - name: Compute original checksum 23 | run: sha256sum taglib.wasm > taglib.wasm.sum 24 | - name: Build taglib.wasm 25 | run: | 26 | rm taglib.wasm 27 | ./build/build-docker.sh 28 | - name: Verify checksum unchanged 29 | run: | 30 | sha256sum -c taglib.wasm.sum || exit 1 31 | - name: Attest build provenance 32 | uses: actions/attest-build-provenance@v3 33 | with: 34 | subject-path: | 35 | taglib.wasm 36 | -------------------------------------------------------------------------------- /taglib.cpp: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | #include 3 | #include 4 | #include 5 | 6 | #include "fileref.h" 7 | #include "tpropertymap.h" 8 | 9 | char *to_char_array(const TagLib::String &s) { 10 | const std::string str = s.to8Bit(true); 11 | return ::strdup(str.c_str()); 12 | } 13 | 14 | TagLib::String to_string(const char *s) { 15 | return TagLib::String(s, TagLib::String::UTF8); 16 | } 17 | 18 | __attribute__((export_name("malloc"))) void *exported_malloc(size_t size) { 19 | return malloc(size); 20 | } 21 | 22 | __attribute__((export_name("taglib_file_tags"))) char ** 23 | taglib_file_tags(const char *filename) { 24 | TagLib::FileRef file(filename); 25 | if (file.isNull()) 26 | return nullptr; 27 | 28 | auto properties = file.properties(); 29 | 30 | size_t len = 0; 31 | for (const auto &kvs : properties) 32 | len += kvs.second.size(); 33 | 34 | char **tags = static_cast(malloc(sizeof(char *) * (len + 1))); 35 | if (!tags) 36 | return nullptr; 37 | 38 | size_t i = 0; 39 | for (const auto &kvs : properties) 40 | for (const auto &v : kvs.second) { 41 | TagLib::String row = kvs.first + "\t" + v; 42 | tags[i] = to_char_array(row); 43 | i++; 44 | } 45 | tags[len] = nullptr; 46 | 47 | return tags; 48 | } 49 | 50 | static const uint8_t CLEAR = 1 << 0; 51 | 52 | __attribute__((export_name("taglib_file_write_tags"))) bool 53 | taglib_file_write_tags(const char *filename, const char **tags, uint8_t opts) { 54 | if (!filename || !tags) 55 | return false; 56 | 57 | TagLib::FileRef file(filename); 58 | if (file.isNull()) 59 | return false; 60 | 61 | auto properties = file.properties(); 62 | if (opts & CLEAR) 63 | properties.clear(); 64 | 65 | for (size_t i = 0; tags[i]; i++) { 66 | TagLib::String row(tags[i], TagLib::String::UTF8); 67 | if (auto ti = row.find("\t"); ti != -1) { 68 | auto key = row.substr(0, ti); 69 | auto value = row.substr(ti + 1); 70 | if (value.isEmpty()) 71 | properties.erase(key); 72 | else 73 | properties.replace(key, value.split("\v")); 74 | } 75 | } 76 | 77 | file.setProperties(properties); 78 | return file.save(); 79 | } 80 | 81 | struct FileProperties { 82 | uint32_t lengthInMilliseconds; 83 | uint32_t channels; 84 | uint32_t sampleRate; 85 | uint32_t bitrate; 86 | char **imageMetadata; 87 | }; 88 | 89 | __attribute__((export_name("taglib_file_read_properties"))) FileProperties * 90 | taglib_file_read_properties(const char *filename) { 91 | TagLib::FileRef file(filename); 92 | if (file.isNull() || !file.audioProperties()) 93 | return nullptr; 94 | 95 | FileProperties *props = 96 | static_cast(malloc(sizeof(FileProperties))); 97 | if (!props) 98 | return nullptr; 99 | 100 | auto audioProperties = file.audioProperties(); 101 | props->lengthInMilliseconds = audioProperties->lengthInMilliseconds(); 102 | props->channels = audioProperties->channels(); 103 | props->sampleRate = audioProperties->sampleRate(); 104 | props->bitrate = audioProperties->bitrate(); 105 | 106 | const auto &pictures = file.complexProperties("PICTURE"); 107 | 108 | props->imageMetadata = nullptr; 109 | if (pictures.isEmpty()) 110 | return props; 111 | 112 | size_t len = pictures.size(); 113 | char **imageMetadata = 114 | static_cast(malloc(sizeof(char *) * (len + 1))); 115 | if (!imageMetadata) 116 | return props; 117 | 118 | size_t i = 0; 119 | for (const auto &p : pictures) { 120 | TagLib::String type = p["pictureType"].toString(); 121 | TagLib::String desc = p["description"].toString(); 122 | TagLib::String mime = p["mimeType"].toString(); 123 | TagLib::String row = type + "\t" + desc + "\t" + mime; 124 | imageMetadata[i] = to_char_array(row); 125 | i++; 126 | } 127 | imageMetadata[len] = nullptr; 128 | 129 | props->imageMetadata = imageMetadata; 130 | 131 | return props; 132 | } 133 | 134 | struct ByteData { 135 | uint32_t length; 136 | char *data; 137 | }; 138 | 139 | __attribute__((export_name("taglib_file_read_image"))) ByteData * 140 | taglib_file_read_image(const char *filename, int index) { 141 | TagLib::FileRef file(filename); 142 | if (file.isNull()) 143 | return nullptr; 144 | 145 | const auto &pictures = file.complexProperties("PICTURE"); 146 | if (pictures.isEmpty()) 147 | return nullptr; 148 | 149 | if (index < 0 || index >= static_cast(pictures.size())) 150 | return nullptr; 151 | 152 | auto v = pictures[index]["data"].toByteVector(); 153 | ByteData *bd = static_cast(malloc(sizeof(ByteData))); 154 | if (!bd) 155 | return nullptr; 156 | 157 | bd->length = static_cast(v.size()); 158 | if (bd->length == 0) { 159 | bd->data = nullptr; 160 | return bd; 161 | } 162 | 163 | // allocate and copy into module memory to keep it valid for go to read 164 | char *buf = static_cast(malloc(bd->length)); 165 | if (!buf) 166 | return nullptr; 167 | 168 | memcpy(buf, v.data(), bd->length); 169 | bd->data = buf; 170 | 171 | return bd; 172 | } 173 | 174 | __attribute__((export_name("taglib_file_write_image"))) bool 175 | taglib_file_write_image(const char *filename, const char *buf, uint32_t length, 176 | int index, const char *pictureType, 177 | const char *description, const char *mimeType) { 178 | TagLib::FileRef file(filename); 179 | if (file.isNull()) 180 | return false; 181 | 182 | auto pictures = file.complexProperties("PICTURE"); 183 | 184 | if (length == 0) { 185 | // remove image at index if it exists 186 | if (index >= 0 && index < static_cast(pictures.size())) { 187 | auto it = pictures.begin(); 188 | std::advance(it, index); 189 | pictures.erase(it); 190 | if (!file.setComplexProperties("PICTURE", pictures)) 191 | return false; 192 | } 193 | return file.save(); 194 | } 195 | 196 | TagLib::VariantMap newPicture; 197 | newPicture["data"] = TagLib::ByteVector(buf, length); 198 | newPicture["pictureType"] = to_string(pictureType); 199 | newPicture["description"] = to_string(description); 200 | newPicture["mimeType"] = to_string(mimeType); 201 | 202 | // replace image at index, or append if index is out of range 203 | if (index >= 0 && index < static_cast(pictures.size())) 204 | pictures[index] = newPicture; 205 | else 206 | pictures.append(newPicture); 207 | 208 | if (!file.setComplexProperties("PICTURE", pictures)) 209 | return false; 210 | 211 | return file.save(); 212 | } 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-taglib 2 | 3 | This project is a Go library for reading and writing audio metadata tags. By packaging an embedded **Wasm** binary, the library needs no external dependencies or CGo. Meaning easy static builds and cross compilation. 4 | 5 | Current bundled TagLib version is [v2.1.1](https://github.com/taglib/taglib/releases/tag/v2.1.1). 6 | 7 | To reproduce or verify the bundled binary, see the [attestations](https://github.com/sentriz/go-taglib/attestations/). 8 | 9 | [![godoc](https://img.shields.io/badge/pkg.go.dev-doc-blue)](http://pkg.go.dev/go.senan.xyz/taglib) 10 | 11 | ## Features 12 | 13 | - **Read** and **write** metadata tags for audio files, including support for **multi-valued** tags. 14 | - **Read** and **write** embedded images (album artwork) from audio files. 15 | - Retrieve audio properties such as length, bitrate, sample rate, and channels. 16 | - Supports multiple audio formats including _MP3_, _FLAC_, _M4A_, _WAV_, _OGG_, _WMA_, and more. 17 | - Safe for concurrent use 18 | - [Reasonably fast](#performance) 19 | 20 | ## Usage 21 | 22 | Add the library to your project with `go get go.senan.xyz/taglib@latest` 23 | 24 | ### Reading metadata 25 | 26 | ```go 27 | func main() { 28 | tags, err := taglib.ReadTags("path/to/audiofile.mp3") 29 | // check(err) 30 | 31 | fmt.Printf("tags: %v\n", tags) // map[string][]string 32 | 33 | fmt.Printf("AlbumArtist: %q\n", tags[taglib.AlbumArtist]) 34 | fmt.Printf("Album: %q\n", tags[taglib.Album]) 35 | fmt.Printf("TrackNumber: %q\n", tags[taglib.TrackNumber]) 36 | } 37 | ``` 38 | 39 | ### Writing metadata 40 | 41 | ```go 42 | func main() { 43 | err := taglib.WriteTags("path/to/audiofile.mp3", map[string][]string{ 44 | // Multi-valued tags allowed 45 | taglib.AlbumArtist: {"David Byrne", "Brian Eno"}, 46 | taglib.Album: {"My Life in the Bush of Ghosts"}, 47 | taglib.TrackNumber: {"1"}, 48 | 49 | // Non-standard allowed too 50 | "ALBUMARTIST_CREDIT": {"Brian Eno & David Byrne"}, 51 | }, 0) 52 | // check(err) 53 | } 54 | ``` 55 | 56 | #### Options for writing 57 | 58 | The behaviour of writing can be configured with some bitset flags 59 | 60 | The options are 61 | 62 | - `Clear` which indicates that all existing tags not present in the new map should be removed 63 | 64 | The options can be combined the with the bitwise `OR` operator (`|`) 65 | 66 | ```go 67 | taglib.WriteTags(path, tags, taglib.Clear) 68 | taglib.WriteTags(path, tags, 0) 69 | ``` 70 | 71 | ### Reading properties 72 | 73 | ```go 74 | func main() { 75 | properties, err := taglib.ReadProperties("path/to/audiofile.mp3") 76 | // check(err) 77 | 78 | fmt.Printf("Length: %v\n", properties.Length) 79 | fmt.Printf("Bitrate: %d\n", properties.Bitrate) 80 | fmt.Printf("SampleRate: %d\n", properties.SampleRate) 81 | fmt.Printf("Channels: %d\n", properties.Channels) 82 | 83 | // Image metadata (without reading actual image data) 84 | for i, img := range properties.Images { 85 | fmt.Printf("Image %d - Type: %s, Description: %s, MIME type: %s\n", 86 | i, img.Type, img.Description, img.MIMEType) 87 | } 88 | } 89 | ``` 90 | 91 | ### Reading embedded images 92 | 93 | ```go 94 | import ( 95 | "bytes" 96 | "image" 97 | 98 | _ "image/gif" 99 | _ "image/jpeg" 100 | _ "image/png" 101 | 102 | // ... 103 | ) 104 | 105 | func main() { 106 | // Read first image (index 0) 107 | imageBytes, err := taglib.ReadImage("path/to/audiofile.mp3") 108 | // check(err) 109 | 110 | if imageBytes == nil { 111 | fmt.Printf("File contains no image") 112 | return 113 | } 114 | 115 | img, format, err := image.Decode(bytes.NewReader(imageBytes)) 116 | // check(err) 117 | 118 | fmt.Printf("format: %q\n", format) 119 | 120 | bounds := img.Bounds() 121 | fmt.Printf("width: %d\n", bounds.Dx()) 122 | fmt.Printf("height: %d\n", bounds.Dy()) 123 | 124 | // Read a specific image by index 125 | backCover, err := taglib.ReadImageOptions("path/to/audiofile.mp3", 1) 126 | // check(err) 127 | } 128 | ``` 129 | 130 | ### Writing embedded images 131 | 132 | ```go 133 | func main() { 134 | var imageBytes []byte // Some image data, from somewhere 135 | 136 | // Write as front cover with auto-detected MIME type 137 | err = taglib.WriteImage("path/to/audiofile.mp3", imageBytes) 138 | // check(err) 139 | 140 | // Write with custom options 141 | err = taglib.WriteImageOptions( 142 | "path/to/audiofile.mp3", 143 | imageBytes, 144 | 0, // replaces image at index; use higher index to append 145 | "Back Cover", // picture type 146 | "Back artwork", // description 147 | "image/jpeg", // MIME type 148 | ) 149 | // check(err) 150 | } 151 | ``` 152 | 153 | ## Manually Building and Using the Wasm Binary 154 | 155 | The binary is already included in the package. However if you want to manually build and override it, you can with WASI SDK and Go build flags 156 | 157 | 1. Clone this repository and Git submodules 158 | 159 | ```console 160 | $ git clone "https://github.com/sentriz/go-taglib.git" --recursive 161 | $ cd go-taglib 162 | ``` 163 | 164 | > [!NOTE] 165 | > Make sure to use the `--recursive` flag, without it there will be no TagLib submodule to build with 166 | 167 | 2. Generate the Wasm binary: 168 | 169 | ```console 170 | $ ./build/build-docker.sh 171 | $ # taglib.wasm created 172 | ``` 173 | 174 | Or, to build without Docker install [WASI SDK](https://github.com/WebAssembly/wasi-sdk) and [Binaryen](https://github.com/WebAssembly/binaryen), then 175 | 176 | ```console 177 | $ ./build/build.sh /path/to/wasi-sdk # eg /opt/wasi-sdk 178 | $ # taglib.wasm created 179 | ``` 180 | 181 | 3. Use the new binary in your project 182 | 183 | ```console 184 | $ CGO_ENABLED=0 go build -ldflags="-X 'go.senan.xyz/taglib.binaryPath=/path/to/taglib.wasm'" ./your/project/... 185 | ``` 186 | 187 | ### Performance 188 | 189 | In this example, tracks are read on average in `0.3 ms`, and written in `1.85 ms` 190 | 191 | ``` 192 | goos: linux 193 | goarch: amd64 194 | pkg: go.senan.xyz/taglib 195 | cpu: AMD Ryzen 7 7840U w/ Radeon 780M Graphics 196 | BenchmarkWrite-16 608 1847873 ns/op 197 | BenchmarkRead-16 3802 299247 ns/op 198 | ``` 199 | 200 | ## License 201 | 202 | This project is licensed under the GNU Lesser General Public License v2.1. See the [LICENSE](LICENSE) file for details. 203 | 204 | ## Contributing 205 | 206 | Contributions are welcome! Please open an issue or submit a pull request. 207 | 208 | ## Acknowledgments 209 | 210 | - [TagLib](https://taglib.org/) for the audio metadata library. 211 | - [Wazero](https://github.com/tetratelabs/wazero) for the WebAssembly runtime in Go. 212 | -------------------------------------------------------------------------------- /taglib_test.go: -------------------------------------------------------------------------------- 1 | package taglib_test 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "errors" 7 | "fmt" 8 | "image" 9 | "maps" 10 | "os" 11 | "path/filepath" 12 | "runtime" 13 | "slices" 14 | "strings" 15 | "sync" 16 | "testing" 17 | "time" 18 | 19 | _ "image/gif" 20 | _ "image/jpeg" 21 | _ "image/png" 22 | 23 | "go.senan.xyz/taglib" 24 | ) 25 | 26 | func TestInvalid(t *testing.T) { 27 | t.Parallel() 28 | 29 | path := tmpf(t, []byte("not a file"), "eg.flac") 30 | _, err := taglib.ReadTags(path) 31 | eq(t, err, taglib.ErrInvalidFile) 32 | } 33 | 34 | func TestClear(t *testing.T) { 35 | t.Parallel() 36 | 37 | paths := testPaths(t) 38 | for _, path := range paths { 39 | t.Run(filepath.Base(path), func(t *testing.T) { 40 | // set some tags first 41 | err := taglib.WriteTags(path, map[string][]string{ 42 | "ARTIST": {"Example A"}, 43 | "ALUMARTIST": {"Example"}, 44 | }, taglib.Clear) 45 | 46 | nilErr(t, err) 47 | 48 | // then clear 49 | err = taglib.WriteTags(path, nil, taglib.Clear) 50 | nilErr(t, err) 51 | 52 | got, err := taglib.ReadTags(path) 53 | nilErr(t, err) 54 | 55 | if len(got) > 0 { 56 | t.Fatalf("exp empty, got %v", got) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func TestReadWrite(t *testing.T) { 63 | t.Parallel() 64 | 65 | paths := testPaths(t) 66 | testTags := []map[string][]string{ 67 | { 68 | "ONE": {"one", "two", "three", "four"}, 69 | "FIVE": {"six", "seven"}, 70 | "NINE": {"nine"}, 71 | }, 72 | { 73 | "ARTIST": {"Example A", "Hello, 世界"}, 74 | "ALUMARTIST": {"Example"}, 75 | }, 76 | { 77 | "ARTIST": {"Example A", "Example B"}, 78 | "ALUMARTIST": {"Example"}, 79 | "TRACK": {"1"}, 80 | "TRACKNUMBER": {"1"}, 81 | }, 82 | { 83 | "ARTIST": {"Example A", "Example B"}, 84 | "ALUMARTIST": {"Example"}, 85 | }, 86 | { 87 | "ARTIST": {"Hello, 世界", "界世"}, 88 | }, 89 | { 90 | "ARTIST": {"Brian Eno—David Byrne"}, 91 | "ALBUM": {"My Life in the Bush of Ghosts"}, 92 | }, 93 | { 94 | "ARTIST": {"Hello, 世界", "界世"}, 95 | "ALBUM": {longString}, 96 | "ALBUMARTIST": {longString, longString}, 97 | "OTHER": {strings.Repeat(longString, 2)}, 98 | }, 99 | } 100 | 101 | for _, path := range paths { 102 | for i, tags := range testTags { 103 | t.Run(fmt.Sprintf("%s_tags_%d", filepath.Base(path), i), func(t *testing.T) { 104 | err := taglib.WriteTags(path, tags, taglib.Clear) 105 | nilErr(t, err) 106 | 107 | got, err := taglib.ReadTags(path) 108 | nilErr(t, err) 109 | 110 | tagEq(t, got, tags) 111 | }) 112 | } 113 | } 114 | } 115 | 116 | func TestMergeWrite(t *testing.T) { 117 | t.Parallel() 118 | 119 | paths := testPaths(t) 120 | 121 | cmp := func(t *testing.T, path string, want map[string][]string) { 122 | t.Helper() 123 | tags, err := taglib.ReadTags(path) 124 | nilErr(t, err) 125 | tagEq(t, tags, want) 126 | } 127 | 128 | for _, path := range paths { 129 | t.Run(filepath.Base(path), func(t *testing.T) { 130 | err := taglib.WriteTags(path, nil, taglib.Clear) 131 | nilErr(t, err) 132 | 133 | err = taglib.WriteTags(path, map[string][]string{ 134 | "ONE": {"one"}, 135 | }, 0) 136 | 137 | nilErr(t, err) 138 | cmp(t, path, map[string][]string{ 139 | "ONE": {"one"}, 140 | }) 141 | 142 | nilErr(t, err) 143 | err = taglib.WriteTags(path, map[string][]string{ 144 | "TWO": {"two", "two!"}, 145 | }, 0) 146 | 147 | nilErr(t, err) 148 | cmp(t, path, map[string][]string{ 149 | "ONE": {"one"}, 150 | "TWO": {"two", "two!"}, 151 | }) 152 | 153 | err = taglib.WriteTags(path, map[string][]string{ 154 | "THREE": {"three"}, 155 | }, 0) 156 | 157 | nilErr(t, err) 158 | cmp(t, path, map[string][]string{ 159 | "ONE": {"one"}, 160 | "TWO": {"two", "two!"}, 161 | "THREE": {"three"}, 162 | }) 163 | 164 | // change prev 165 | err = taglib.WriteTags(path, map[string][]string{ 166 | "ONE": {"one new"}, 167 | }, 0) 168 | 169 | nilErr(t, err) 170 | cmp(t, path, map[string][]string{ 171 | "ONE": {"one new"}, 172 | "TWO": {"two", "two!"}, 173 | "THREE": {"three"}, 174 | }) 175 | 176 | // change prev 177 | err = taglib.WriteTags(path, map[string][]string{ 178 | "ONE": {}, 179 | "THREE": {"three new!"}, 180 | }, 0) 181 | 182 | nilErr(t, err) 183 | cmp(t, path, map[string][]string{ 184 | "TWO": {"two", "two!"}, 185 | "THREE": {"three new!"}, 186 | }) 187 | }) 188 | } 189 | } 190 | 191 | func TestReadExistingUnicode(t *testing.T) { 192 | tags, err := taglib.ReadTags("testdata/normal.flac") 193 | nilErr(t, err) 194 | eq(t, len(tags[taglib.AlbumArtist]), 1) 195 | eq(t, tags[taglib.AlbumArtist][0], "Brian Eno—David Byrne") 196 | } 197 | 198 | func TestConcurrent(t *testing.T) { 199 | t.Parallel() 200 | 201 | paths := testPaths(t) 202 | 203 | c := 250 204 | pathErrors := make([]error, c) 205 | 206 | var wg sync.WaitGroup 207 | for i := range c { 208 | wg.Add(1) 209 | go func() { 210 | defer wg.Done() 211 | if _, err := taglib.ReadTags(paths[i%len(paths)]); err != nil { 212 | pathErrors[i] = fmt.Errorf("iter %d: %w", i, err) 213 | } 214 | }() 215 | } 216 | wg.Wait() 217 | 218 | err := errors.Join(pathErrors...) 219 | nilErr(t, err) 220 | } 221 | 222 | func TestProperties(t *testing.T) { 223 | t.Parallel() 224 | 225 | path := tmpf(t, egFLAC, "eg.flac") 226 | 227 | properties, err := taglib.ReadProperties(path) 228 | nilErr(t, err) 229 | 230 | eq(t, 1*time.Second, properties.Length) 231 | eq(t, 1460, properties.Bitrate) 232 | eq(t, 48_000, properties.SampleRate) 233 | eq(t, 2, properties.Channels) 234 | 235 | eq(t, len(properties.Images), 2) 236 | eq(t, properties.Images[0].Type, "Front Cover") 237 | eq(t, properties.Images[0].Description, "The first image") 238 | eq(t, properties.Images[0].MIMEType, "image/png") 239 | eq(t, properties.Images[1].Type, "Lead Artist") 240 | eq(t, properties.Images[1].Description, "The second image") 241 | eq(t, properties.Images[1].MIMEType, "image/jpeg") 242 | } 243 | 244 | func TestMultiOpen(t *testing.T) { 245 | t.Parallel() 246 | 247 | { 248 | path := tmpf(t, egFLAC, "eg.flac") 249 | _, err := taglib.ReadTags(path) 250 | nilErr(t, err) 251 | } 252 | { 253 | path := tmpf(t, egFLAC, "eg.flac") 254 | _, err := taglib.ReadTags(path) 255 | nilErr(t, err) 256 | } 257 | } 258 | 259 | func TestReadImage(t *testing.T) { 260 | path := tmpf(t, egFLAC, "eg.flac") 261 | 262 | properties, err := taglib.ReadProperties(path) 263 | nilErr(t, err) 264 | eq(t, len(properties.Images) > 0, true) 265 | 266 | imgBytes, err := taglib.ReadImage(path) 267 | nilErr(t, err) 268 | if imgBytes == nil { 269 | t.Fatalf("no image") 270 | } 271 | 272 | img, _, err := image.Decode(bytes.NewReader(imgBytes)) 273 | nilErr(t, err) 274 | 275 | b := img.Bounds() 276 | if b.Dx() != 700 || b.Dy() != 700 { 277 | t.Fatalf("bad image dimensions: %d, %d != 700, 700", b.Dx(), b.Dy()) 278 | } 279 | } 280 | 281 | func TestWriteImage(t *testing.T) { 282 | path := tmpf(t, egFLAC, "eg.flac") 283 | 284 | err := taglib.WriteImage(path, coverJPG) 285 | nilErr(t, err) 286 | 287 | imgBytes, err := taglib.ReadImage(path) 288 | nilErr(t, err) 289 | if imgBytes == nil { 290 | t.Fatalf("no written image") 291 | } 292 | 293 | img, _, err := image.Decode(bytes.NewReader(imgBytes)) 294 | nilErr(t, err) 295 | 296 | b := img.Bounds() 297 | if b.Dx() != 700 || b.Dy() != 700 { 298 | t.Fatalf("bad image dimensions: %d, %d != 700, 700", b.Dx(), b.Dy()) 299 | } 300 | } 301 | 302 | func TestClearImage(t *testing.T) { 303 | path := tmpf(t, egFLAC, "eg.flac") 304 | 305 | properties, err := taglib.ReadProperties(path) 306 | nilErr(t, err) 307 | eq(t, len(properties.Images) == 2, true) // have two imaages 308 | eq(t, properties.Images[0].Description, "The first image") 309 | 310 | img, err := taglib.ReadImage(path) 311 | nilErr(t, err) 312 | eq(t, len(img) > 0, true) 313 | 314 | nilErr(t, taglib.WriteImage(path, nil)) 315 | 316 | properties, err = taglib.ReadProperties(path) 317 | nilErr(t, err) 318 | eq(t, len(properties.Images) == 1, true) // have one images 319 | eq(t, properties.Images[0].Description, "The second image") 320 | 321 | nilErr(t, taglib.WriteImage(path, nil)) 322 | 323 | properties, err = taglib.ReadProperties(path) 324 | nilErr(t, err) 325 | eq(t, len(properties.Images) == 0, true) // have zero images 326 | 327 | img, err = taglib.ReadImage(path) 328 | nilErr(t, err) 329 | eq(t, len(img) == 0, true) 330 | } 331 | 332 | func TestClearImageReverse(t *testing.T) { 333 | path := tmpf(t, egFLAC, "eg.flac") 334 | 335 | properties, err := taglib.ReadProperties(path) 336 | nilErr(t, err) 337 | eq(t, len(properties.Images) == 2, true) // have two imaages 338 | eq(t, properties.Images[0].Description, "The first image") 339 | 340 | img, err := taglib.ReadImage(path) 341 | nilErr(t, err) 342 | eq(t, len(img) > 0, true) 343 | 344 | nilErr(t, taglib.WriteImageOptions(path, nil, 1, "", "", "")) // delete the second 345 | 346 | properties, err = taglib.ReadProperties(path) 347 | nilErr(t, err) 348 | eq(t, len(properties.Images) == 1, true) // have one images 349 | eq(t, properties.Images[0].Description, "The first image") // but it's the first one 350 | 351 | nilErr(t, taglib.WriteImage(path, nil)) 352 | 353 | properties, err = taglib.ReadProperties(path) 354 | nilErr(t, err) 355 | eq(t, len(properties.Images) == 0, true) // have zero images 356 | 357 | img, err = taglib.ReadImage(path) 358 | nilErr(t, err) 359 | eq(t, len(img) == 0, true) 360 | } 361 | 362 | func TestMemNew(t *testing.T) { 363 | t.Parallel() 364 | 365 | t.Skip("heavy") 366 | 367 | checkMem(t) 368 | 369 | for range 10_000 { 370 | path := tmpf(t, egFLAC, "eg.flac") 371 | _, err := taglib.ReadTags(path) 372 | nilErr(t, err) 373 | err = os.Remove(path) // don't blow up incase we're using tmpfs 374 | nilErr(t, err) 375 | } 376 | } 377 | 378 | func TestMemSameFile(t *testing.T) { 379 | t.Parallel() 380 | 381 | t.Skip("heavy") 382 | 383 | checkMem(t) 384 | 385 | path := tmpf(t, egFLAC, "eg.flac") 386 | for range 10_000 { 387 | _, err := taglib.ReadTags(path) 388 | nilErr(t, err) 389 | } 390 | 391 | var memStats runtime.MemStats 392 | runtime.ReadMemStats(&memStats) 393 | 394 | t.Logf("alloc = %v MiB", memStats.Alloc/1024/1024) 395 | } 396 | 397 | func BenchmarkWrite(b *testing.B) { 398 | path := tmpf(b, egFLAC, "eg.flac") 399 | b.ResetTimer() 400 | 401 | for range b.N { 402 | err := taglib.WriteTags(path, bigTags, taglib.Clear) 403 | nilErr(b, err) 404 | } 405 | } 406 | 407 | func BenchmarkRead(b *testing.B) { 408 | path := tmpf(b, egFLAC, "eg.flac") 409 | err := taglib.WriteTags(path, bigTags, taglib.Clear) 410 | nilErr(b, err) 411 | b.ResetTimer() 412 | 413 | for range b.N { 414 | _, err := taglib.ReadTags(path) 415 | nilErr(b, err) 416 | } 417 | } 418 | 419 | var ( 420 | //go:embed testdata/eg.flac 421 | egFLAC []byte 422 | //go:embed testdata/eg.mp3 423 | egMP3 []byte 424 | //go:embed testdata/eg.m4a 425 | egM4a []byte 426 | //go:embed testdata/eg.ogg 427 | egOgg []byte 428 | //go:embed testdata/eg.wav 429 | egWAV []byte 430 | //go:embed testdata/cover.jpg 431 | coverJPG []byte 432 | ) 433 | 434 | func testPaths(t testing.TB) []string { 435 | return []string{ 436 | tmpf(t, egFLAC, "eg.flac"), 437 | tmpf(t, egMP3, "eg.mp3"), 438 | tmpf(t, egM4a, "eg.m4a"), 439 | tmpf(t, egWAV, "eg.wav"), 440 | tmpf(t, egOgg, "eg.ogg"), 441 | } 442 | } 443 | 444 | func tmpf(t testing.TB, b []byte, name string) string { 445 | p := filepath.Join(t.TempDir(), name) 446 | err := os.WriteFile(p, b, os.ModePerm) 447 | nilErr(t, err) 448 | return p 449 | } 450 | 451 | func nilErr(t testing.TB, err error) { 452 | if err != nil { 453 | t.Helper() 454 | t.Fatalf("err: %v", err) 455 | } 456 | } 457 | func eq[T comparable](t testing.TB, a, b T) { 458 | if a != b { 459 | t.Helper() 460 | t.Fatalf("%v != %v", a, b) 461 | } 462 | } 463 | func tagEq(t testing.TB, a, b map[string][]string) { 464 | if !maps.EqualFunc(a, b, slices.Equal) { 465 | t.Helper() 466 | t.Fatalf("%q != %q", a, b) 467 | } 468 | } 469 | 470 | func checkMem(t testing.TB) { 471 | stop := make(chan struct{}) 472 | t.Cleanup(func() { 473 | stop <- struct{}{} 474 | }) 475 | 476 | go func() { 477 | ticker := time.Tick(100 * time.Millisecond) 478 | 479 | for { 480 | select { 481 | case <-stop: 482 | return 483 | 484 | case <-ticker: 485 | var memStats runtime.MemStats 486 | runtime.ReadMemStats(&memStats) 487 | t.Logf("alloc = %v MiB", memStats.Alloc/1024/1024) 488 | } 489 | } 490 | }() 491 | } 492 | 493 | var bigTags = map[string][]string{ 494 | "ALBUM": {"New Raceion"}, 495 | "ALBUMARTIST": {"Alan Vega"}, 496 | "ALBUMARTIST_CREDIT": {"Alan Vega"}, 497 | "ALBUMARTISTS": {"Alan Vega"}, 498 | "ALBUMARTISTS_CREDIT": {"Alan Vega"}, 499 | "ARTIST": {"Alan Vega"}, 500 | "ARTIST_CREDIT": {"Alan Vega"}, 501 | "ARTISTS": {"Alan Vega"}, 502 | "ARTISTS_CREDIT": {"Alan Vega"}, 503 | "DATE": {"1993-04-02"}, 504 | "DISCNUMBER": {"1"}, 505 | "GENRE": {"electronic"}, 506 | "GENRES": {"electronic", "industrial", "experimental", "proto-punk", "rock", "rockabilly"}, 507 | "LABEL": {"GM Editions"}, 508 | "MEDIA": {"Digital Media"}, 509 | "MUSICBRAINZ_ALBUMARTISTID": {"dd720ac8-1c68-4484-abb7-0546413a55e3"}, 510 | "MUSICBRAINZ_ALBUMID": {"c56a5905-2b3a-46f5-82c7-ce8eed01f876"}, 511 | "MUSICBRAINZ_ARTISTID": {"dd720ac8-1c68-4484-abb7-0546413a55e3"}, 512 | "MUSICBRAINZ_RELEASEGROUPID": {"373dcce2-63c4-3e8a-9c2c-bc58ec1bbbf3"}, 513 | "MUSICBRAINZ_TRACKID": {"2f1c8b43-7b4e-4bc8-aacf-760e5fb747a0"}, 514 | "ORIGINALDATE": {"1993-04-02"}, 515 | "REPLAYGAIN_ALBUM_GAIN": {"-4.58 dB"}, 516 | "REPLAYGAIN_ALBUM_PEAK": {"0.977692"}, 517 | "REPLAYGAIN_TRACK_GAIN": {"-5.29 dB"}, 518 | "REPLAYGAIN_TRACK_PEAK": {"0.977661"}, 519 | "TITLE": {"Christ Dice"}, 520 | "TRACKNUMBER": {"2"}, 521 | "UPC": {"3760271710486"}, 522 | } 523 | 524 | var longString = strings.Repeat("E", 1024) 525 | -------------------------------------------------------------------------------- /taglib.go: -------------------------------------------------------------------------------- 1 | package taglib 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | _ "embed" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/tetratelabs/wazero" 15 | "github.com/tetratelabs/wazero/api" 16 | "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" 17 | ) 18 | 19 | //go:embed taglib.wasm 20 | var binary []byte // WASM blob. To override, go build -ldflags="-X 'go.senan.xyz/taglib.binaryPath=/path/to/taglib.wasm'" 21 | var binaryPath string 22 | 23 | var ErrInvalidFile = fmt.Errorf("invalid file") 24 | var ErrSavingFile = fmt.Errorf("can't save file") 25 | 26 | // These constants define normalized tag keys used by TagLib's [property mapping]. 27 | // When using [ReadTags], the library will map format-specific metadata to these standardized keys. 28 | // Similarly, [WriteTags] will map these keys back to the appropriate format-specific fields. 29 | // 30 | // While these constants provide a consistent interface across different audio formats, 31 | // you can also use custom tag keys if the underlying format supports arbitrary tags. 32 | // 33 | // [property mapping]: https://taglib.org/api/p_propertymapping.html 34 | const ( 35 | AcoustIDFingerprint = "ACOUSTID_FINGERPRINT" 36 | AcoustIDID = "ACOUSTID_ID" 37 | Album = "ALBUM" 38 | AlbumArtist = "ALBUMARTIST" 39 | AlbumArtistSort = "ALBUMARTISTSORT" 40 | AlbumSort = "ALBUMSORT" 41 | Arranger = "ARRANGER" 42 | Artist = "ARTIST" 43 | Artists = "ARTISTS" 44 | ArtistSort = "ARTISTSORT" 45 | ArtistWebpage = "ARTISTWEBPAGE" 46 | ASIN = "ASIN" 47 | AudioSourceWebpage = "AUDIOSOURCEWEBPAGE" 48 | Barcode = "BARCODE" 49 | BPM = "BPM" 50 | CatalogNumber = "CATALOGNUMBER" 51 | Comment = "COMMENT" 52 | Compilation = "COMPILATION" 53 | Composer = "COMPOSER" 54 | ComposerSort = "COMPOSERSORT" 55 | Conductor = "CONDUCTOR" 56 | Copyright = "COPYRIGHT" 57 | CopyrightURL = "COPYRIGHTURL" 58 | Date = "DATE" 59 | DiscNumber = "DISCNUMBER" 60 | DiscSubtitle = "DISCSUBTITLE" 61 | DJMixer = "DJMIXER" 62 | EncodedBy = "ENCODEDBY" 63 | Encoding = "ENCODING" 64 | EncodingTime = "ENCODINGTIME" 65 | Engineer = "ENGINEER" 66 | FileType = "FILETYPE" 67 | FileWebpage = "FILEWEBPAGE" 68 | GaplessPlayback = "GAPLESSPLAYBACK" 69 | Genre = "GENRE" 70 | Grouping = "GROUPING" 71 | InitialKey = "INITIALKEY" 72 | InvolvedPeople = "INVOLVEDPEOPLE" 73 | ISRC = "ISRC" 74 | Label = "LABEL" 75 | Language = "LANGUAGE" 76 | Length = "LENGTH" 77 | License = "LICENSE" 78 | Lyricist = "LYRICIST" 79 | Lyrics = "LYRICS" 80 | Media = "MEDIA" 81 | Mixer = "MIXER" 82 | Mood = "MOOD" 83 | MovementCount = "MOVEMENTCOUNT" 84 | MovementName = "MOVEMENTNAME" 85 | MovementNumber = "MOVEMENTNUMBER" 86 | MusicBrainzAlbumID = "MUSICBRAINZ_ALBUMID" 87 | MusicBrainzAlbumArtistID = "MUSICBRAINZ_ALBUMARTISTID" 88 | MusicBrainzArtistID = "MUSICBRAINZ_ARTISTID" 89 | MusicBrainzReleaseGroupID = "MUSICBRAINZ_RELEASEGROUPID" 90 | MusicBrainzReleaseTrackID = "MUSICBRAINZ_RELEASETRACKID" 91 | MusicBrainzTrackID = "MUSICBRAINZ_TRACKID" 92 | MusicBrainzWorkID = "MUSICBRAINZ_WORKID" 93 | MusicianCredits = "MUSICIANCREDITS" 94 | MusicIPPUID = "MUSICIP_PUID" 95 | OriginalAlbum = "ORIGINALALBUM" 96 | OriginalArtist = "ORIGINALARTIST" 97 | OriginalDate = "ORIGINALDATE" 98 | OriginalFilename = "ORIGINALFILENAME" 99 | OriginalLyricist = "ORIGINALLYRICIST" 100 | Owner = "OWNER" 101 | PaymentWebpage = "PAYMENTWEBPAGE" 102 | Performer = "PERFORMER" 103 | PlaylistDelay = "PLAYLISTDELAY" 104 | Podcast = "PODCAST" 105 | PodcastCategory = "PODCASTCATEGORY" 106 | PodcastDesc = "PODCASTDESC" 107 | PodcastID = "PODCASTID" 108 | PodcastURL = "PODCASTURL" 109 | ProducedNotice = "PRODUCEDNOTICE" 110 | Producer = "PRODUCER" 111 | PublisherWebpage = "PUBLISHERWEBPAGE" 112 | RadioStation = "RADIOSTATION" 113 | RadioStationOwner = "RADIOSTATIONOWNER" 114 | RadioStationWebpage = "RADIOSTATIONWEBPAGE" 115 | ReleaseCountry = "RELEASECOUNTRY" 116 | ReleaseDate = "RELEASEDATE" 117 | ReleaseStatus = "RELEASESTATUS" 118 | ReleaseType = "RELEASETYPE" 119 | Remixer = "REMIXER" 120 | Script = "SCRIPT" 121 | ShowSort = "SHOWSORT" 122 | ShowWorkMovement = "SHOWWORKMOVEMENT" 123 | Subtitle = "SUBTITLE" 124 | TaggingDate = "TAGGINGDATE" 125 | Title = "TITLE" 126 | TitleSort = "TITLESORT" 127 | TrackNumber = "TRACKNUMBER" 128 | TVEpisode = "TVEPISODE" 129 | TVEpisodeID = "TVEPISODEID" 130 | TVNetwork = "TVNETWORK" 131 | TVSeason = "TVSEASON" 132 | TVShow = "TVSHOW" 133 | URL = "URL" 134 | Work = "WORK" 135 | ) 136 | 137 | // ReadTags reads all metadata tags from an audio file at the given path. 138 | func ReadTags(path string) (map[string][]string, error) { 139 | var err error 140 | path, err = filepath.Abs(path) 141 | if err != nil { 142 | return nil, fmt.Errorf("make path abs %w", err) 143 | } 144 | 145 | dir := filepath.Dir(path) 146 | mod, err := newModuleRO(dir) 147 | if err != nil { 148 | return nil, fmt.Errorf("init module: %w", err) 149 | } 150 | defer mod.close() 151 | 152 | var raw wasmStrings 153 | if err := mod.call("taglib_file_tags", &raw, wasmString(wasmPath(path))); err != nil { 154 | return nil, fmt.Errorf("call: %w", err) 155 | } 156 | if raw == nil { 157 | return nil, ErrInvalidFile 158 | } 159 | 160 | var tags = map[string][]string{} 161 | for _, row := range raw { 162 | k, v, ok := strings.Cut(row, "\t") 163 | if !ok { 164 | continue 165 | } 166 | tags[k] = append(tags[k], v) 167 | } 168 | return tags, nil 169 | } 170 | 171 | // Properties contains the audio properties of a media file. 172 | type Properties struct { 173 | // Length is the duration of the audio 174 | Length time.Duration 175 | // Channels is the number of audio channels 176 | Channels uint 177 | // SampleRate in Hz 178 | SampleRate uint 179 | // Bitrate in kbit/s 180 | Bitrate uint 181 | // Images contains metadata about all embedded images 182 | Images []ImageDesc 183 | } 184 | 185 | // ImageDesc contains metadata about an embedded image without the actual image data. 186 | type ImageDesc struct { 187 | // Type is the picture type (e.g., "Front Cover", "Back Cover") 188 | Type string 189 | // Description is a textual description of the image 190 | Description string 191 | // MIMEType is the MIME type of the image (e.g., "image/jpeg") 192 | MIMEType string 193 | } 194 | 195 | // ReadProperties reads the audio properties from a file at the given path. 196 | func ReadProperties(path string) (Properties, error) { 197 | var err error 198 | path, err = filepath.Abs(path) 199 | if err != nil { 200 | return Properties{}, fmt.Errorf("make path abs %w", err) 201 | } 202 | 203 | dir := filepath.Dir(path) 204 | mod, err := newModuleRO(dir) 205 | if err != nil { 206 | return Properties{}, fmt.Errorf("init module: %w", err) 207 | } 208 | defer mod.close() 209 | 210 | var raw wasmFileProperties 211 | if err := mod.call("taglib_file_read_properties", &raw, wasmString(wasmPath(path))); err != nil { 212 | return Properties{}, fmt.Errorf("call: %w", err) 213 | } 214 | 215 | var images []ImageDesc 216 | for _, row := range raw.imageDescs { 217 | parts := strings.SplitN(row, "\t", 3) 218 | if len(parts) != 3 { 219 | continue 220 | } 221 | images = append(images, ImageDesc{ 222 | Type: parts[0], 223 | Description: parts[1], 224 | MIMEType: parts[2], 225 | }) 226 | } 227 | 228 | return Properties{ 229 | Length: time.Duration(raw.lengthInMilliseconds) * time.Millisecond, 230 | Channels: uint(raw.channels), 231 | SampleRate: uint(raw.sampleRate), 232 | Bitrate: uint(raw.bitrate), 233 | Images: images, 234 | }, nil 235 | } 236 | 237 | // WriteOption configures the behavior of write operations. The can be passed to [WriteTags] and combined with the bitwise OR operator. 238 | type WriteOption uint8 239 | 240 | const ( 241 | // Clear indicates that all existing tags not present in the new map should be removed. 242 | Clear WriteOption = 1 << iota 243 | ) 244 | 245 | // WriteTags writes the metadata key-values pairs to path. The behavior can be controlled with [WriteOption]. 246 | func WriteTags(path string, tags map[string][]string, opts WriteOption) error { 247 | var err error 248 | path, err = filepath.Abs(path) 249 | if err != nil { 250 | return fmt.Errorf("make path abs %w", err) 251 | } 252 | 253 | dir := filepath.Dir(path) 254 | mod, err := newModule(dir) 255 | if err != nil { 256 | return fmt.Errorf("init module: %w", err) 257 | } 258 | defer mod.close() 259 | 260 | var raw []string 261 | for k, vs := range tags { 262 | raw = append(raw, fmt.Sprintf("%s\t%s", k, strings.Join(vs, "\v"))) 263 | } 264 | 265 | var out wasmBool 266 | if err := mod.call("taglib_file_write_tags", &out, wasmString(wasmPath(path)), wasmStrings(raw), wasmUint8(opts)); err != nil { 267 | return fmt.Errorf("call: %w", err) 268 | } 269 | if !out { 270 | return ErrSavingFile 271 | } 272 | return nil 273 | } 274 | 275 | // ReadImage reads the first embedded image from path. Returns empty byte slice if no images exist. 276 | func ReadImage(path string) ([]byte, error) { 277 | return ReadImageOptions(path, 0) 278 | } 279 | 280 | // WriteImage writes image as an embedded "Front Cover" at index 0 with auto-detected MIME type. 281 | // Set image to nil to clear the image at that index. 282 | func WriteImage(path string, image []byte) error { 283 | mimeType := "" 284 | if image != nil { 285 | mimeType = detectImageMIME(image) 286 | } 287 | return WriteImageOptions(path, image, 0, "Front Cover", "Added by go-taglib", mimeType) 288 | } 289 | 290 | // ReadImageOptions reads the embedded image at the specified index from path. 291 | // Index 0 is the first image. Returns empty byte slice if index is out of range. 292 | func ReadImageOptions(path string, index int) ([]byte, error) { 293 | var err error 294 | path, err = filepath.Abs(path) 295 | if err != nil { 296 | return nil, fmt.Errorf("make path abs %w", err) 297 | } 298 | 299 | mod, err := newModuleRO(filepath.Dir(path)) 300 | if err != nil { 301 | return nil, fmt.Errorf("init module: %w", err) 302 | } 303 | defer mod.close() 304 | 305 | var img wasmBytes 306 | if err := mod.call("taglib_file_read_image", &img, wasmString(wasmPath(path)), wasmInt(index)); err != nil { 307 | return nil, fmt.Errorf("call: %w", err) 308 | } 309 | 310 | return img, nil 311 | } 312 | 313 | // WriteImageOptions writes an image with custom metadata. 314 | // Index specifies which image slot to write to (0 = first image). 315 | // Set image to nil to clear the image at that index. 316 | func WriteImageOptions(path string, image []byte, index int, imageType, description, mimeType string) error { 317 | var err error 318 | path, err = filepath.Abs(path) 319 | if err != nil { 320 | return fmt.Errorf("make path abs %w", err) 321 | } 322 | 323 | mod, err := newModule(filepath.Dir(path)) 324 | if err != nil { 325 | return fmt.Errorf("init module: %w", err) 326 | } 327 | defer mod.close() 328 | 329 | var out wasmBool 330 | if err := mod.call("taglib_file_write_image", &out, wasmString(wasmPath(path)), wasmBytes(image), wasmInt(len(image)), wasmInt(index), wasmString(imageType), wasmString(description), wasmString(mimeType)); err != nil { 331 | return fmt.Errorf("call: %w", err) 332 | } 333 | if !out { 334 | return ErrSavingFile 335 | } 336 | return nil 337 | } 338 | 339 | type rc struct { 340 | wazero.Runtime 341 | wazero.CompiledModule 342 | } 343 | 344 | var getRuntimeOnce = sync.OnceValues(func() (rc, error) { 345 | ctx := context.Background() 346 | 347 | cacheDir := filepath.Join(os.TempDir(), "go-taglib-wasm") 348 | compilationCache, err := wazero.NewCompilationCacheWithDir(cacheDir) 349 | if err != nil { 350 | return rc{}, err 351 | } 352 | 353 | runtime := wazero.NewRuntimeWithConfig(ctx, 354 | wazero.NewRuntimeConfig(). 355 | WithCompilationCache(compilationCache), 356 | ) 357 | wasi_snapshot_preview1.MustInstantiate(ctx, runtime) 358 | 359 | _, err = runtime. 360 | NewHostModuleBuilder("env"). 361 | NewFunctionBuilder().WithFunc(func(int32) int32 { panic("__cxa_allocate_exception") }).Export("__cxa_allocate_exception"). 362 | NewFunctionBuilder().WithFunc(func(int32, int32, int32) { panic("__cxa_throw") }).Export("__cxa_throw"). 363 | Instantiate(ctx) 364 | if err != nil { 365 | return rc{}, err 366 | } 367 | 368 | var bin = binary 369 | if binaryPath != "" { 370 | bin, err = os.ReadFile(binaryPath) 371 | if err != nil { 372 | return rc{}, fmt.Errorf("read custom binary path: %w", err) 373 | } 374 | clear(binary) 375 | } 376 | 377 | compiled, err := runtime.CompileModule(ctx, bin) 378 | if err != nil { 379 | return rc{}, err 380 | } 381 | 382 | return rc{ 383 | Runtime: runtime, 384 | CompiledModule: compiled, 385 | }, nil 386 | }) 387 | 388 | type module struct { 389 | mod api.Module 390 | } 391 | 392 | func newModule(dir string) (module, error) { return newModuleOpt(dir, false) } 393 | func newModuleRO(dir string) (module, error) { return newModuleOpt(dir, true) } 394 | func newModuleOpt(dir string, readOnly bool) (module, error) { 395 | rt, err := getRuntimeOnce() 396 | if err != nil { 397 | return module{}, fmt.Errorf("get runtime once: %w", err) 398 | } 399 | 400 | fsConfig := wazero.NewFSConfig() 401 | if readOnly { 402 | fsConfig = fsConfig.WithReadOnlyDirMount(dir, wasmPath(dir)) 403 | } else { 404 | fsConfig = fsConfig.WithDirMount(dir, wasmPath(dir)) 405 | } 406 | 407 | cfg := wazero. 408 | NewModuleConfig(). 409 | WithName(""). 410 | WithStartFunctions("_initialize"). 411 | WithFSConfig(fsConfig) 412 | 413 | ctx := context.Background() 414 | mod, err := rt.InstantiateModule(ctx, rt.CompiledModule, cfg) 415 | if err != nil { 416 | return module{}, err 417 | } 418 | 419 | return module{ 420 | mod: mod, 421 | }, nil 422 | } 423 | 424 | func (m *module) malloc(size uint32) uint32 { 425 | var ptr wasmUint32 426 | if err := m.call("malloc", &ptr, wasmUint32(size)); err != nil { 427 | panic(err) 428 | } 429 | if ptr == 0 { 430 | panic("no ptr") 431 | } 432 | return uint32(ptr) 433 | } 434 | 435 | type wasmArg interface { 436 | encode(*module) uint64 437 | } 438 | 439 | type wasmResult interface { 440 | decode(*module, uint64) 441 | } 442 | 443 | type wasmBool bool 444 | 445 | func (b wasmBool) encode(*module) uint64 { 446 | if b { 447 | return 1 448 | } 449 | return 0 450 | } 451 | 452 | func (b *wasmBool) decode(_ *module, val uint64) { 453 | *b = val == 1 454 | } 455 | 456 | type wasmInt int 457 | 458 | func (i wasmInt) encode(*module) uint64 { return uint64(i) } 459 | func (i *wasmInt) decode(_ *module, val uint64) { 460 | *i = wasmInt(val) 461 | } 462 | 463 | type wasmUint8 uint8 464 | 465 | func (u wasmUint8) encode(*module) uint64 { return uint64(u) } 466 | 467 | type wasmUint32 uint32 468 | 469 | func (u wasmUint32) encode(*module) uint64 { return uint64(u) } 470 | func (u *wasmUint32) decode(_ *module, val uint64) { 471 | *u = wasmUint32(val) 472 | } 473 | 474 | type wasmString string 475 | 476 | func (s wasmString) encode(m *module) uint64 { 477 | b := append([]byte(s), 0) 478 | ptr := m.malloc(uint32(len(b))) 479 | if !m.mod.Memory().Write(ptr, b) { 480 | panic("failed to write to mod.module.Memory()") 481 | } 482 | return uint64(ptr) 483 | } 484 | func (s *wasmString) decode(m *module, val uint64) { 485 | if val != 0 { 486 | *s = wasmString(readString(m, uint32(val))) 487 | } 488 | } 489 | 490 | type wasmBytes []byte 491 | 492 | func (b wasmBytes) encode(m *module) uint64 { 493 | ptr := m.malloc(uint32(len(b))) 494 | if !m.mod.Memory().Write(ptr, b) { 495 | panic("failed to write to mod.module.Memory()") 496 | } 497 | return uint64(ptr) 498 | } 499 | func (b *wasmBytes) decode(m *module, val uint64) { 500 | if val != 0 { 501 | *b = readBytes(m, uint32(val)) 502 | } 503 | } 504 | 505 | type wasmStrings []string 506 | 507 | func (s wasmStrings) encode(m *module) uint64 { 508 | arrayPtr := m.malloc(uint32((len(s) + 1) * 4)) 509 | for i, str := range s { 510 | b := append([]byte(str), 0) 511 | ptr := m.malloc(uint32(len(b))) 512 | if !m.mod.Memory().Write(ptr, b) { 513 | panic("failed to write to mod.module.Memory()") 514 | } 515 | if !m.mod.Memory().WriteUint32Le(arrayPtr+uint32(i*4), ptr) { 516 | panic("failed to write pointer to mod.module.Memory()") 517 | } 518 | } 519 | if !m.mod.Memory().WriteUint32Le(arrayPtr+uint32(len(s)*4), 0) { 520 | panic("failed to write pointer to memory") 521 | } 522 | return uint64(arrayPtr) 523 | } 524 | func (s *wasmStrings) decode(m *module, val uint64) { 525 | if val != 0 { 526 | *s = readStrings(m, uint32(val)) 527 | } 528 | } 529 | 530 | type wasmFileProperties struct { 531 | lengthInMilliseconds uint32 532 | channels uint32 533 | sampleRate uint32 534 | bitrate uint32 535 | imageDescs []string 536 | } 537 | 538 | func (f *wasmFileProperties) decode(m *module, val uint64) { 539 | if val == 0 { 540 | return 541 | } 542 | ptr := uint32(val) 543 | 544 | f.lengthInMilliseconds, _ = m.mod.Memory().ReadUint32Le(ptr) 545 | f.channels, _ = m.mod.Memory().ReadUint32Le(ptr + 4) 546 | f.sampleRate, _ = m.mod.Memory().ReadUint32Le(ptr + 8) 547 | f.bitrate, _ = m.mod.Memory().ReadUint32Le(ptr + 12) 548 | 549 | imageMetadataPtr, _ := m.mod.Memory().ReadUint32Le(ptr + 16) 550 | if imageMetadataPtr != 0 { 551 | f.imageDescs = readStrings(m, imageMetadataPtr) 552 | } 553 | } 554 | 555 | func (m *module) call(name string, dest wasmResult, args ...wasmArg) error { 556 | params := make([]uint64, 0, len(args)) 557 | for _, a := range args { 558 | params = append(params, a.encode(m)) 559 | } 560 | 561 | results, err := m.mod.ExportedFunction(name).Call(context.Background(), params...) 562 | if err != nil { 563 | return fmt.Errorf("call %q: %w", name, err) 564 | } 565 | if len(results) == 0 { 566 | return nil 567 | } 568 | 569 | dest.decode(m, results[0]) 570 | return nil 571 | } 572 | 573 | func (m *module) close() { 574 | if err := m.mod.Close(context.Background()); err != nil { 575 | panic(err) 576 | } 577 | } 578 | 579 | func readStrings(m *module, ptr uint32) []string { 580 | strs := []string{} // non nil so call knows if it's just empty 581 | for { 582 | stringPtr, ok := m.mod.Memory().ReadUint32Le(ptr) 583 | if !ok { 584 | panic("memory error") 585 | } 586 | if stringPtr == 0 { 587 | break 588 | } 589 | str := readString(m, stringPtr) 590 | strs = append(strs, str) 591 | ptr += 4 592 | } 593 | return strs 594 | } 595 | 596 | func readString(m *module, ptr uint32) string { 597 | size := uint32(64) 598 | buf, ok := m.mod.Memory().Read(ptr, size) 599 | if !ok { 600 | panic("memory error") 601 | } 602 | if i := bytes.IndexByte(buf, 0); i >= 0 { 603 | return string(buf[:i]) 604 | } 605 | 606 | for { 607 | next, ok := m.mod.Memory().Read(ptr+size, size) 608 | if !ok { 609 | panic("memory error") 610 | } 611 | if i := bytes.IndexByte(next, 0); i >= 0 { 612 | return string(append(buf, next[:i]...)) 613 | } 614 | buf = append(buf, next...) 615 | size += size 616 | } 617 | } 618 | 619 | func readBytes(m *module, ptr uint32) []byte { 620 | ret := []byte{} // non nil so call knows if it's just empty 621 | 622 | size, ok := m.mod.Memory().ReadUint32Le(ptr) 623 | if !ok { 624 | panic("memory error") 625 | } 626 | if size == 0 { 627 | return ret 628 | } 629 | 630 | loc, _ := m.mod.Memory().ReadUint32Le(ptr + 4) 631 | b, ok := m.mod.Memory().Read(loc, size) 632 | if !ok { 633 | panic("memory error") 634 | } 635 | 636 | // copy the data, "this returns a view of the underlying memory, not a copy" per api.Memory.Read docs 637 | ret = make([]byte, size) 638 | copy(ret, b) 639 | 640 | return ret 641 | } 642 | 643 | // WASI uses POSIXy paths, even on Windows 644 | func wasmPath(p string) string { 645 | return filepath.ToSlash(p) 646 | } 647 | 648 | // detectImageMIME detects image MIME type from magic bytes. 649 | // Adapted from Go's net/http package to avoid the dependency. 650 | func detectImageMIME(data []byte) string { 651 | if len(data) < 2 { 652 | return "" 653 | } 654 | switch { 655 | case len(data) >= 4 && bytes.Equal(data[:4], []byte("\x00\x00\x01\x00")): 656 | return "image/x-icon" 657 | case len(data) >= 4 && bytes.Equal(data[:4], []byte("\x00\x00\x02\x00")): 658 | return "image/x-icon" 659 | case bytes.HasPrefix(data, []byte("BM")): 660 | return "image/bmp" 661 | case bytes.HasPrefix(data, []byte("GIF87a")): 662 | return "image/gif" 663 | case bytes.HasPrefix(data, []byte("GIF89a")): 664 | return "image/gif" 665 | case len(data) >= 8 && bytes.Equal(data[:8], []byte("\x89PNG\x0D\x0A\x1A\x0A")): 666 | return "image/png" 667 | case len(data) >= 3 && bytes.Equal(data[:3], []byte("\xFF\xD8\xFF")): 668 | return "image/jpeg" 669 | case len(data) >= 14 && bytes.Equal(data[:4], []byte("RIFF")) && bytes.Equal(data[8:14], []byte("WEBPVP")): 670 | return "image/webp" 671 | default: 672 | return "" 673 | } 674 | } 675 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | --------------------------------------------------------------------------------