Support reading the cover art from a stream

This commit is contained in:
Frédéric Mangano 2023-03-02 16:18:27 +09:00
parent 46cc78bfff
commit befae72d2a
5 changed files with 61 additions and 30 deletions

View File

@ -118,6 +118,7 @@ You can specify \fB-\fP for standard output, in which case the regular output wi
.TP
.B \-\-set-cover \fIFILE\fP
Replace or set the cover art to the specified picture.
Specify \fB-\fP to read the picture from standard input.
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

View File

@ -136,17 +136,20 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input)
return opt;
// All non-option arguments are input files.
bool stdin_as_input = false;
size_t stdin_uses = 0;
for (int i = optind; i < argc; i++) {
stdin_as_input = stdin_as_input || strcmp(argv[i], "-") == 0;
if (strcmp(argv[i], "-") == 0)
++stdin_uses;
opt.paths_in.emplace_back(argv[i]);
}
bool stdin_as_input = stdin_uses > 0;
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));
}
if (set_cover == "-")
++stdin_uses;
if (set_all)
++stdin_uses;
if (stdin_uses > 1)
throw status { st::bad_arguments, "Cannot use standard input more than once." };
// Convert arguments to UTF-8.
if (!opt.raw) {
@ -169,10 +172,7 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input)
if ((!opt.in_place || opt.edit_interactively) && opt.paths_in.size() != 1)
throw status {st::bad_arguments, "Exactly one input file must be specified."};
if (set_all && stdin_as_input)
throw status {st::bad_arguments, "Cannot use standard input as input file when --set-all is specified."};
if (opt.edit_interactively && (stdin_as_input || opt.path_out == "-"))
if (opt.edit_interactively && (stdin_as_input || opt.path_out == "-" || opt.cover_out == "-"))
throw status {st::bad_arguments, "Cannot edit interactively when standard input or standard output are already used."};
if (opt.edit_interactively && !opt.path_out.has_value() && !opt.in_place)
@ -187,6 +187,12 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input)
if (opt.cover_out && opt.paths_in.size() > 1)
throw status {st::bad_arguments, "Cannot use --output-cover with multiple input files."};
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));
}
if (set_all) {
// Read comments from stdin and prepend them to opt.to_add.
std::list<std::string> comments = read_comments(comments_input, opt.raw);

View File

@ -98,26 +98,47 @@ void ot::partial_file::abort()
remove(temporary_name.c_str());
}
/** \todo Support non-seekable files like streams. */
/**
* Determine the file size, in bytes, of the given file. Return -1 on for streams.
*/
static long get_file_size(FILE* f)
{
if (fseek(f, 0L, SEEK_END) != 0) {
clearerr(f); // Recover.
return -1;
}
long file_size = ftell(f);
rewind(f);
return file_size;
}
ot::byte_string ot::slurp_binary_file(const char* filename)
{
std::ifstream file(filename, std::ios::binary | std::ios::ate);
if (file.fail())
file f = strcmp(filename, "-") == 0 ? freopen(nullptr, "rb", stdin)
: fopen(filename, "rb");
if (f == nullptr)
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) + "." };
long file_size = get_file_size(f.get());
if (file_size == -1) {
// Read the input stream block by block and resize the output byte string as needed.
uint8_t buffer[4096];
while (!feof(f.get())) {
size_t read_len = fread(buffer, 1, sizeof(buffer), f.get());
content.append(buffer, read_len);
if (ferror(f.get()))
throw status { st::standard_error,
"Could not read '"s + filename + "': " + strerror(errno) + "." };
}
} else {
// Lucky! We know the file size, so lets slurp it at once.
content.resize(file_size);
if (fread(content.data(), 1, file_size, f.get()) < file_size)
throw status { st::standard_error,
"Could not read '"s + filename + "': " + strerror(errno) + "." };
}
return content;
}

View File

@ -159,8 +159,7 @@ void check_bad_arguments()
error_case({"opustags", "--derp=y"}, "Unrecognized option '--derp=y'.", "unrecognized long option with value");
error_case({"opustags", "-aX=Y"}, "Exactly one input file must be specified.", "no input file");
error_case({"opustags", "-i", "-o", "/dev/null", "-"}, "Cannot combine --in-place and --output.", "in-place + output");
error_case({"opustags", "-S", "-"}, "Cannot use standard input as input file when --set-all is specified.",
"set all and read opus from stdin");
error_case({"opustags", "-S", "-"}, "Cannot use standard input more than once.", "set all and read opus from stdin");
error_case({"opustags", "-i", "-"}, "Cannot modify standard input in place.", "write stdin in-place");
error_case({"opustags", "-o", "x", "--output", "y", "z"},
"Cannot specify --output more than once.", "double output");

View File

@ -4,7 +4,7 @@ use strict;
use warnings;
use utf8;
use Test::More tests => 58;
use Test::More tests => 59;
use Digest::MD5;
use File::Basename;
@ -331,10 +331,14 @@ 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');
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=AAAAAwAAAAlpbWFnZS9wbmcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEWJUE5HDQoaCgAAAA1JSERSAAAAAQAAAAEIAgAAAJB3U94AAAAMSURBVAjXY/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');
is_deeply(opustags(qw(-D --set-cover - gobble.opus), { in => "GIF8 x" }), [<<'END_OUT', '', 0], 'read the cover from stdin');
METADATA_BLOCK_PICTURE=AAAAAwAAAAlpbWFnZS9naWYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZHSUY4IHg=
END_OUT