Add option --set-cover

This commit is contained in:
Frédéric Mangano 2023-02-28 16:16:34 +09:00
parent 74e42ee917
commit 558160d5c3
11 changed files with 133 additions and 4 deletions

View File

@ -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.

View File

@ -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

View File

@ -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 }) {

View File

@ -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());
}

View File

@ -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);
/** \} */
/***********************************************************************************************//**

View File

@ -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);

View File

@ -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

View File

@ -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;
}

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -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;