mirror of
https://github.com/fmang/opustags.git
synced 2025-01-15 12:43:17 +01:00
Add option --set-cover
This commit is contained in:
parent
74e42ee917
commit
558160d5c3
@ -65,6 +65,8 @@ Documentation
|
||||
-s, --set FIELD=VALUE replace a comment
|
||||
-S, --set-all import comments from standard input
|
||||
-e, --edit edit tags interactively in VISUAL/EDITOR
|
||||
--output-cover FILE extract and save the cover art, if any
|
||||
--set-cover FILE sets the cover art
|
||||
--raw disable encoding conversion
|
||||
|
||||
See the man page, `opustags.1`, for extensive documentation.
|
||||
|
@ -107,7 +107,7 @@ environment variable. The allowed format is the same as \fB--set-all\fP.
|
||||
If TERM and VISUAL are set, VISUAL takes precedence over EDITOR.
|
||||
.TP
|
||||
.B \-\-output-cover \fIFILE\fP
|
||||
Save the cover art of the input file at the specified location.
|
||||
Save the cover art of the input Opus file to the specified location.
|
||||
If the input file does not contain any cover art, this option has no effect.
|
||||
To allow overwriting the target location, specify \fB--overwrite\fP.
|
||||
In the case of multiple pictures embedded in the Opus tags, only the first one is saved.
|
||||
@ -116,6 +116,11 @@ and rely on the magic number to deduce the type. opustags does not add or check
|
||||
extension.
|
||||
You can specify \fB-\fP for standard output, in which case the regular output will be suppressed.
|
||||
.TP
|
||||
.B \-\-set-cover \fIFILE\fP
|
||||
Replace or set the cover art to the specified picture.
|
||||
In theory, an Opus file may contain multiple pictures with different roles, though in practice only
|
||||
the front cover really matters. opustags can currently only handle one front cover and nothing else.
|
||||
.TP
|
||||
.B \-\-raw
|
||||
OpusTags metadata should always be encoded in UTF-8, as per RFC 7845. However, some files may be
|
||||
corrupted or possibly even contain intentional binary data. In that case, --raw lets you edit that
|
||||
|
14
src/cli.cc
14
src/cli.cc
@ -37,6 +37,7 @@ Options:
|
||||
-S, --set-all import comments from standard input
|
||||
-e, --edit edit tags interactively in VISUAL/EDITOR
|
||||
--output-cover FILE extract and save the cover art, if any
|
||||
--set-cover FILE sets the cover art
|
||||
--raw disable encoding conversion
|
||||
|
||||
See the man page for extensive documentation.
|
||||
@ -54,6 +55,7 @@ static struct option getopt_options[] = {
|
||||
{"set-all", no_argument, 0, 'S'},
|
||||
{"edit", no_argument, 0, 'e'},
|
||||
{"output-cover", required_argument, 0, 'c'},
|
||||
{"set-cover", required_argument, 0, 'C'},
|
||||
{"raw", no_argument, 0, 'r'},
|
||||
{NULL, 0, 0, 0}
|
||||
};
|
||||
@ -65,6 +67,7 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input)
|
||||
const char* equal;
|
||||
ot::status rc;
|
||||
bool set_all = false;
|
||||
std::optional<std::string> set_cover;
|
||||
opt = {};
|
||||
if (argc == 1)
|
||||
throw status {st::bad_arguments, "No arguments specified. Use -h for help."};
|
||||
@ -114,6 +117,11 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input)
|
||||
throw status {st::bad_arguments, "Cannot specify --output-cover more than once."};
|
||||
opt.cover_out = optarg;
|
||||
break;
|
||||
case 'C':
|
||||
if (set_cover)
|
||||
throw status {st::bad_arguments, "Cannot specify --set-cover more than once."};
|
||||
set_cover = optarg;
|
||||
break;
|
||||
case 'r':
|
||||
opt.raw = true;
|
||||
break;
|
||||
@ -134,6 +142,12 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input)
|
||||
opt.paths_in.emplace_back(argv[i]);
|
||||
}
|
||||
|
||||
if (set_cover) {
|
||||
byte_string picture_data = ot::slurp_binary_file(set_cover->c_str());
|
||||
opt.to_delete.push_back("METADATA_BLOCK_PICTURE");
|
||||
opt.to_add.push_back(ot::make_cover(picture_data));
|
||||
}
|
||||
|
||||
// Convert arguments to UTF-8.
|
||||
if (!opt.raw) {
|
||||
for (std::list<std::string>* args : { &opt.to_add, &opt.to_delete }) {
|
||||
|
22
src/opus.cc
22
src/opus.cc
@ -147,6 +147,20 @@ ot::picture::picture(ot::byte_string block)
|
||||
picture_data = byte_string_view(&storage[pic_offset + 4], pic_size);
|
||||
}
|
||||
|
||||
ot::byte_string ot::picture::serialize() const
|
||||
{
|
||||
ot::byte_string bytes;
|
||||
size_t mime_offset = 4;
|
||||
size_t pic_offset = mime_offset + 4 + mime_type.size() + 4 + 0 + 16;
|
||||
bytes.resize(pic_offset + 4 + picture_data.size());
|
||||
*reinterpret_cast<uint32_t*>(&bytes[0]) = htobe32(3); // Picture type: front cover.
|
||||
*reinterpret_cast<uint32_t*>(&bytes[mime_offset]) = htobe32(mime_type.size());
|
||||
std::copy(mime_type.begin(), mime_type.end(), std::next(bytes.begin(), mime_offset + 4));
|
||||
*reinterpret_cast<uint32_t*>(&bytes[pic_offset]) = htobe32(picture_data.size());
|
||||
std::copy(picture_data.begin(), picture_data.end(), std::next(bytes.begin(), pic_offset + 4));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* \todo Take into account the picture types (first 4 bytes of the tag value).
|
||||
*/
|
||||
@ -167,3 +181,11 @@ std::optional<ot::picture> ot::extract_cover(const ot::opus_tags& tags)
|
||||
cover_value.remove_prefix(prefix.size());
|
||||
return picture(decode_base64(cover_value));
|
||||
}
|
||||
|
||||
std::string ot::make_cover(ot::byte_string_view picture_data)
|
||||
{
|
||||
picture pic;
|
||||
pic.mime_type = "application/octet-stream"_bsv;
|
||||
pic.picture_data = picture_data;
|
||||
return "METADATA_BLOCK_PICTURE=" + encode_base64(pic.serialize());
|
||||
}
|
||||
|
@ -157,6 +157,9 @@ private:
|
||||
ot::file file;
|
||||
};
|
||||
|
||||
/** Read a whole file into memory and return the read content. */
|
||||
byte_string slurp_binary_file(const char* filename);
|
||||
|
||||
/** C++ wrapper for iconv. */
|
||||
class encoding_converter {
|
||||
public:
|
||||
@ -412,10 +415,19 @@ dynamic_ogg_packet render_tags(const opus_tags& tags);
|
||||
* pictures have picture type 3 (front cover), and empty metadata.
|
||||
*/
|
||||
struct picture {
|
||||
picture() = default;
|
||||
|
||||
/** Extract the picture information from serialized binary data.*/
|
||||
picture(byte_string block);
|
||||
byte_string_view mime_type;
|
||||
byte_string_view picture_data;
|
||||
|
||||
/**
|
||||
* Encode the picture attributes (mime_type, picture_data) into a binary block to be stored
|
||||
* into METADATA_BLOCK_PICTURE.
|
||||
*/
|
||||
byte_string serialize() const;
|
||||
|
||||
/** To avoid needless copies of the picture data, move the original data block there. The
|
||||
* string_view attributes will refer to it. */
|
||||
byte_string storage;
|
||||
@ -424,6 +436,12 @@ struct picture {
|
||||
/** Extract the first picture embedded in the tags, regardless of its type. */
|
||||
std::optional<picture> extract_cover(const opus_tags& tags);
|
||||
|
||||
/**
|
||||
* Return a METADATA_BLOCK_PICTURE tag defining the front cover art to the given picture data (JPEG,
|
||||
* PNG). The MIME type is deduced from the magic number.
|
||||
*/
|
||||
std::string make_cover(byte_string_view picture_data);
|
||||
|
||||
/** \} */
|
||||
|
||||
/***********************************************************************************************//**
|
||||
|
@ -12,6 +12,7 @@
|
||||
#include <opustags.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <fstream>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
@ -97,6 +98,30 @@ void ot::partial_file::abort()
|
||||
remove(temporary_name.c_str());
|
||||
}
|
||||
|
||||
/** \todo Support non-seekable files like streams. */
|
||||
ot::byte_string ot::slurp_binary_file(const char* filename)
|
||||
{
|
||||
std::ifstream file(filename, std::ios::binary | std::ios::ate);
|
||||
if (file.fail())
|
||||
throw status { st::standard_error,
|
||||
"Could not open '"s + filename + "': " + strerror(errno) + "." };
|
||||
|
||||
auto file_size = file.tellg();
|
||||
if (file_size == decltype(file)::pos_type(-1))
|
||||
throw status { st::standard_error,
|
||||
"Could not determine the size of '"s + filename + "': " + strerror(errno) + "." };
|
||||
|
||||
byte_string content;
|
||||
content.resize(file_size);
|
||||
file.seekg(0);
|
||||
file.read(reinterpret_cast<char*>(content.data()), file_size);
|
||||
if (file.fail())
|
||||
throw status { st::standard_error,
|
||||
"Could not read '"s + filename + "': " + strerror(errno) + "." };
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
ot::encoding_converter::encoding_converter(const char* from, const char* to)
|
||||
{
|
||||
cd = iconv_open(to, from);
|
||||
|
@ -17,6 +17,7 @@ add_executable(oggdump EXCLUDE_FROM_ALL oggdump.cc)
|
||||
target_link_libraries(oggdump ot)
|
||||
|
||||
configure_file(gobble.opus . COPYONLY)
|
||||
configure_file(pixel.png . COPYONLY)
|
||||
|
||||
add_custom_target(
|
||||
check
|
||||
|
19
t/opus.cc
19
t/opus.cc
@ -165,13 +165,30 @@ static void extract_cover()
|
||||
} catch (const ot::status& rc) {}
|
||||
}
|
||||
|
||||
static void make_cover()
|
||||
{
|
||||
ot::byte_string_view picture_block = ""_bsv
|
||||
"\x00\x00\x00\x03" // Picture type 3.
|
||||
"\x00\x00\x00\x18" "application/octet-stream" // MIME type.
|
||||
"\x00\x00\x00\x00" "" // Description.
|
||||
"\x00\x00\x00\x00" // Width.
|
||||
"\x00\x00\x00\x00" // Height.
|
||||
"\x00\x00\x00\x00" // Color depth.
|
||||
"\x00\x00\x00\x00" // Palette size.
|
||||
"\x00\x00\x00\x0C" "Picture data";
|
||||
|
||||
std::string expected = "METADATA_BLOCK_PICTURE=" + ot::encode_base64(picture_block);
|
||||
is(ot::make_cover("Picture data"_bsv), expected, "build the picture tag");
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
std::cout << "1..5\n";
|
||||
std::cout << "1..6\n";
|
||||
run(parse_standard, "parse a standard OpusTags packet");
|
||||
run(parse_corrupted, "correctly reject invalid packets");
|
||||
run(recode_standard, "recode a standard OpusTags packet");
|
||||
run(recode_padding, "recode a OpusTags packet with padding");
|
||||
run(extract_cover, "extract the cover art");
|
||||
run(make_cover, "encode the cover art");
|
||||
return 0;
|
||||
}
|
||||
|
14
t/opustags.t
14
t/opustags.t
@ -4,7 +4,7 @@ use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Test::More tests => 55;
|
||||
use Test::More tests => 58;
|
||||
|
||||
use Digest::MD5;
|
||||
use File::Basename;
|
||||
@ -73,6 +73,7 @@ Options:
|
||||
-S, --set-all import comments from standard input
|
||||
-e, --edit edit tags interactively in VISUAL/EDITOR
|
||||
--output-cover FILE extract and save the cover art, if any
|
||||
--set-cover FILE sets the cover art
|
||||
--raw disable encoding conversion
|
||||
|
||||
See the man page for extensive documentation.
|
||||
@ -326,3 +327,14 @@ is_deeply(opustags('out.opus'), [$big_tags, '', 0], 'read multi-page header');
|
||||
is_deeply(opustags(qw(out.opus -i -D -a), 'encoder=Lavc58.18.100 libopus'), ['', '', 0], 'shrink the header');
|
||||
is(md5('out.opus'), '111a483596ac32352fbce4d14d16abd2', 'the result is identical to the original file');
|
||||
unlink('out.opus');
|
||||
|
||||
####################################################################################################
|
||||
# Cover arts
|
||||
|
||||
is_deeply(opustags(qw(-D --set-cover pixel.png gobble.opus -o out.opus), ), ['', '', 0], 'set the cover');
|
||||
is_deeply(opustags(qw(--output-cover out.png out.opus), ), [<<'END_OUT', '', 0], 'extract the cover');
|
||||
METADATA_BLOCK_PICTURE=AAAAAwAAABhhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEWJUE5HDQoaCgAAAA1JSERSAAAAAQAAAAEIAgAAAJB3U94AAAAMSURBVAjXY/j//z8ABf4C/tzMWecAAAAASUVORK5CYII=
|
||||
END_OUT
|
||||
is(md5('out.png'), md5('pixel.png'), 'the extracted cover is identical to the one set');
|
||||
unlink('out.opus');
|
||||
unlink('out.png');
|
||||
|
BIN
t/pixel.png
Normal file
BIN
t/pixel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 B |
15
t/system.cc
15
t/system.cc
@ -34,6 +34,18 @@ void check_partial_files()
|
||||
is(remove(result), 0, "remove the result file");
|
||||
}
|
||||
|
||||
void check_slurp()
|
||||
{
|
||||
static const ot::byte_string_view pixel = ""_bsv
|
||||
"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d"
|
||||
"\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01"
|
||||
"\x08\x02\x00\x00\x00\x90\x77\x53\xde\x00\x00\x00"
|
||||
"\x0c\x49\x44\x41\x54\x08\xd7\x63\xf8\xff\xff\x3f"
|
||||
"\x00\x05\xfe\x02\xfe\xdc\xcc\x59\xe7\x00\x00\x00"
|
||||
"\x00\x49\x45\x4e\x44\xae\x42\x60\x82";
|
||||
opaque_is(ot::slurp_binary_file("pixel.png"), pixel, "loads a whole file");
|
||||
}
|
||||
|
||||
void check_converter()
|
||||
{
|
||||
const char* ephemere_iso = "\xc9\x70\x68\xe9\x6d\xe8\x72\x65";
|
||||
@ -59,8 +71,9 @@ void check_shell_esape()
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
plan(3);
|
||||
plan(4);
|
||||
run(check_partial_files, "test partial files");
|
||||
run(check_slurp, "file slurping");
|
||||
run(check_converter, "test encoding converter");
|
||||
run(check_shell_esape, "test shell escaping");
|
||||
return 0;
|
||||
|
Loading…
x
Reference in New Issue
Block a user