Skip to content

Commit

Permalink
Support reading the cover art from a stream
Browse files Browse the repository at this point in the history
  • Loading branch information
fmang committed Mar 2, 2023
1 parent 46cc78b commit befae72
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 30 deletions.
1 change: 1 addition & 0 deletions opustags.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 17 additions & 11 deletions src/cli.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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);
Expand Down
49 changes: 35 additions & 14 deletions src/system.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 let’s 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;
}
Expand Down
3 changes: 1 addition & 2 deletions t/cli.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
10 changes: 7 additions & 3 deletions t/opustags.t
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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

0 comments on commit befae72

Please sign in to comment.