diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp index dea90b2e6..e93e434e5 100644 --- a/app/codec/oiio/oiiodecoder.cpp +++ b/app/codec/oiio/oiiodecoder.cpp @@ -72,22 +72,44 @@ FootageDescription OIIODecoder::Probe(const QString &filename, const QAtomicInt* return desc; } - VideoParams video_params; + bool stream_enabled = true; - video_params.set_stream_index(0); - video_params.set_width(in->spec().width); - video_params.set_height(in->spec().height); - video_params.set_format(OIIOUtils::GetFormatFromOIIOBasetype(static_cast(in->spec().format.basetype))); - video_params.set_channel_count(in->spec().nchannels); - video_params.set_pixel_aspect_ratio(OIIOUtils::GetPixelAspectRatioFromOIIO(in->spec())); - video_params.set_video_type(VideoParams::kVideoTypeStill); + for (int i=0; in->seek_subimage(i, 0); i++) { + VideoParams video_params; - // OIIO automatically premultiplies alpha - // FIXME: We usually disassociate the alpha for the color management later, for 8-bit images this - // likely reduces the fidelity? - video_params.set_premultiplied_alpha(true); + OIIO::ImageSpec spec = in->spec(); - desc.AddVideoStream(video_params); + video_params.set_stream_index(i); + video_params.set_width(spec.width); + video_params.set_height(spec.height); + video_params.set_format(OIIOUtils::GetFormatFromOIIOBasetype(static_cast(spec.format.basetype))); + video_params.set_channel_count(spec.nchannels); + video_params.set_pixel_aspect_ratio(OIIOUtils::GetPixelAspectRatioFromOIIO(spec)); + video_params.set_video_type(VideoParams::kVideoTypeStill); + + if (i > 1) { + // This is a multilayer image and this image might have an offset + OIIO::ImageSpec root_spec = in->spec(0); + + float norm_x = spec.x + float(spec.width)*0.5f - float(root_spec.width)*0.5f; + float norm_y = spec.y + float(spec.height)*0.5f - float(root_spec.height)*0.5f; + + video_params.set_x(norm_x); + video_params.set_y(norm_y); + } + + // By default, only enable the first subimage (presumably the combined image). Later we will + // ask the user if they want to enable the layers instead. + video_params.set_enabled(stream_enabled); + stream_enabled = false; + + // OIIO automatically premultiplies alpha + // FIXME: We usually disassociate the alpha for the color management later, for 8-bit images this + // likely reduces the fidelity? + video_params.set_premultiplied_alpha(true); + + desc.AddVideoStream(video_params); + } // If we're here, we have a successful image open in->close(); @@ -98,7 +120,7 @@ FootageDescription OIIODecoder::Probe(const QString &filename, const QAtomicInt* bool OIIODecoder::OpenInternal() { // If we can open the filename provided, assume everything is working - return OpenImageHandler(stream().filename()); + return OpenImageHandler(stream().filename(), stream().stream()); } FramePtr OIIODecoder::RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams ÷r, const QAtomicInt *cancelled) @@ -108,13 +130,15 @@ FramePtr OIIODecoder::RetrieveVideoInternal(const rational &timecode, const Retr FramePtr frame = Frame::Create(); - frame->set_video_params(VideoParams(buffer_->spec().width, - buffer_->spec().height, - pix_fmt_, - channel_count_, - OIIOUtils::GetPixelAspectRatioFromOIIO(buffer_->spec()), - VideoParams::kInterlaceNone, // FIXME: Does OIIO deinterlace for us? - divider.divider)); + VideoParams vp(buffer_->spec().width, + buffer_->spec().height, + pix_fmt_, + channel_count_, + OIIOUtils::GetPixelAspectRatioFromOIIO(buffer_->spec()), + VideoParams::kInterlaceNone, // FIXME: Does OIIO deinterlace for us? + divider.divider); + + frame->set_video_params(vp); frame->allocate(); if (divider.divider == 1) { @@ -167,7 +191,7 @@ bool OIIODecoder::FileTypeIsSupported(const QString& fn) return true; } -bool OIIODecoder::OpenImageHandler(const QString &fn) +bool OIIODecoder::OpenImageHandler(const QString &fn, int subimage) { image_ = OIIO::ImageInput::open(fn.toStdString()); @@ -175,6 +199,10 @@ bool OIIODecoder::OpenImageHandler(const QString &fn) return false; } + if (!image_->seek_subimage(subimage, 0)) { + return false; + } + // Check if we can work with this pixel format const OIIO::ImageSpec& spec = image_->spec(); diff --git a/app/codec/oiio/oiiodecoder.h b/app/codec/oiio/oiiodecoder.h index 4232cb2d1..a6039871d 100644 --- a/app/codec/oiio/oiiodecoder.h +++ b/app/codec/oiio/oiiodecoder.h @@ -52,7 +52,7 @@ private: static bool FileTypeIsSupported(const QString& fn); - bool OpenImageHandler(const QString& fn); + bool OpenImageHandler(const QString& fn, int subimage); void CloseImageHandle(); diff --git a/app/core.cpp b/app/core.cpp index c86f1a9e5..f13960cf1 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -533,6 +533,49 @@ void Core::ImportTaskComplete(Task* task) MultiUndoCommand *command = import_task->GetCommand(); + foreach (Footage *f, import_task->GetImportedFootage()) { + // Look for multi-layer images + if (f->GetAudioStreamCount() == 0 && f->GetVideoStreamCount() > 1) { + bool all_stills = true; + + for (int i=0; iGetVideoStreamCount(); i++) { + const VideoParams &vs = f->GetVideoParams(i); + if (!(vs.video_type() == VideoParams::kVideoTypeStill && vs.enabled() == (i == 0))) { + all_stills = false; + } + } + + if (all_stills) { + QMessageBox d(main_window()); + + d.setIcon(QMessageBox::Question); + d.setWindowTitle(tr("Multi-Layer Image")); + d.setText(tr("The file '%1' has multiple layers. Would you like these layers to be " + "separated across multiple tracks or merged into a single image?").arg(f->filename())); + + auto multi_btn = d.addButton(tr("Multiple Layers"), QMessageBox::YesRole); + auto single_btn = d.addButton(tr("Single Layer"), QMessageBox::NoRole); + auto cancel_btn = d.addButton(QMessageBox::Cancel); + + d.exec(); + + if (d.clickedButton() == multi_btn) { + for (int i=0; iGetVideoStreamCount(); i++) { + VideoParams vs = f->GetVideoParams(i); + vs.set_enabled(!vs.enabled()); + f->SetVideoParams(vs, i); + } + } else if (d.clickedButton() == single_btn) { + // Do nothing, footage will already be set up this way + } else if (d.clickedButton() == cancel_btn) { + // Cancel import + delete command; + return; + } + } + } + } + if (import_task->HasInvalidFiles()) { ProjectImportErrorDialog d(import_task->GetInvalidFiles(), main_window_); d.exec(); diff --git a/app/node/distort/transform/transformdistortnode.cpp b/app/node/distort/transform/transformdistortnode.cpp index d2eb794ea..1b5cad90f 100644 --- a/app/node/distort/transform/transformdistortnode.cpp +++ b/app/node/distort/transform/transformdistortnode.cpp @@ -318,7 +318,7 @@ void TransformDistortNode::GizmoDragMove(double x, double y, const Qt::KeyboardM } } -QMatrix4x4 TransformDistortNode::AdjustMatrixByResolutions(const QMatrix4x4 &mat, const QVector2D &sequence_res, const QVector2D &texture_res, AutoScaleType autoscale_type) +QMatrix4x4 TransformDistortNode::AdjustMatrixByResolutions(const QMatrix4x4 &mat, const QVector2D &sequence_res, const QVector2D &texture_res, const QVector2D &offset, AutoScaleType autoscale_type) { // First, create an identity matrix QMatrix4x4 adjusted_matrix; @@ -326,6 +326,9 @@ QMatrix4x4 TransformDistortNode::AdjustMatrixByResolutions(const QMatrix4x4 &mat // Scale it to a square based on the sequence's resolution adjusted_matrix.scale(2.0 / sequence_res.x(), 2.0 / sequence_res.y(), 1.0); + // Apply offset if applicable + adjusted_matrix.translate(offset); + // Adjust by the matrix we generated earlier adjusted_matrix *= mat; @@ -378,6 +381,7 @@ void TransformDistortNode::UpdateGizmoPositions(const NodeValueRow &row, const N // GizmoTraverser just returns the sizes of the textures and no other data VideoParams tex_params = tex->params(); QVector2D tex_sz(tex_params.square_pixel_width(), tex_params.height()); + QVector2D tex_offset = tex_params.offset(); // Retrieve autoscale value AutoScaleType autoscale = static_cast(row[kAutoscaleInput].data().toInt()); @@ -388,6 +392,7 @@ void TransformDistortNode::UpdateGizmoPositions(const NodeValueRow &row, const N rectangle_matrix *= AdjustMatrixByResolutions(GenerateMatrix(row, false, false, false, false), sequence_res, tex_sz, + tex_offset, autoscale); // Create rect and transform it @@ -407,6 +412,7 @@ void TransformDistortNode::UpdateGizmoPositions(const NodeValueRow &row, const N anchor_matrix *= AdjustMatrixByResolutions(GenerateMatrix(row, false, true, false, false), sequence_res, tex_sz, + tex_offset, autoscale); anchor_gizmo_->SetPoint(anchor_matrix.toTransform().map(QPointF(0, 0)) + sequence_half_res_pt); @@ -422,7 +428,7 @@ void TransformDistortNode::UpdateGizmoPositions(const NodeValueRow &row, const N // Use offsets to make the appearance of values that start in the top left, even though we // really anchor around the center - SetInputProperty(kPositionInput, QStringLiteral("offset"), sequence_half_res); + SetInputProperty(kPositionInput, QStringLiteral("offset"), sequence_half_res + tex_offset); SetInputProperty(kAnchorInput, QStringLiteral("offset"), tex_sz * 0.5); } @@ -440,6 +446,7 @@ QMatrix4x4 TransformDistortNode::GenerateAutoScaledMatrix(const QMatrix4x4& gene return AdjustMatrixByResolutions(generated_matrix, sequence_res, texture_res, + texture_params.offset(), autoscale); } diff --git a/app/node/distort/transform/transformdistortnode.h b/app/node/distort/transform/transformdistortnode.h index a2ca55919..f584ff8e6 100644 --- a/app/node/distort/transform/transformdistortnode.h +++ b/app/node/distort/transform/transformdistortnode.h @@ -82,6 +82,7 @@ public: static QMatrix4x4 AdjustMatrixByResolutions(const QMatrix4x4& mat, const QVector2D& sequence_res, const QVector2D& texture_res, + const QVector2D& offset, AutoScaleType autoscale_type = kAutoScaleNone); virtual void UpdateGizmoPositions(const NodeValueRow &row, const NodeGlobals &globals) override; diff --git a/app/node/math/math/mathbase.cpp b/app/node/math/math/mathbase.cpp index d21e0e613..51ef80060 100644 --- a/app/node/math/math/mathbase.cpp +++ b/app/node/math/math/mathbase.cpp @@ -363,6 +363,7 @@ void MathNodeBase::ValueInternal(Operation operation, Pairing pairing, const QSt QMatrix4x4 adjusted_matrix = TransformDistortNode::AdjustMatrixByResolutions(number_val.data().value(), sequence_res, + texture->params().offset(), texture_res); if (operation != kOpMultiply || adjusted_matrix.isIdentity()) { diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index ee458d7d5..3c01d17e2 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -475,7 +475,9 @@ void ViewerOutput::set_parameters_from_footage(const QVector foo QVector video_streams = f->GetEnabledVideoStreams(); QVector audio_streams = f->GetEnabledAudioStreams(); - foreach (const VideoParams& s, video_streams) { + for (int i=0; i foo // If this is a still image, we'll use it's resolution but won't set // `found_video_params` in case something with a frame rate comes along which we'll // prioritize + if (i > 0) { + // Ignore still images past stream 0 + continue; + } + using_timebase = GetVideoParams().time_base(); } else { using_timebase = s.frame_rate_as_time_base(); diff --git a/app/render/videoparams.cpp b/app/render/videoparams.cpp index 032357aea..405deb371 100644 --- a/app/render/videoparams.cpp +++ b/app/render/videoparams.cpp @@ -259,6 +259,8 @@ void VideoParams::set_defaults_for_footage() start_time_ = 0; duration_ = 0; premultiplied_alpha_ = false; + x_ = 0; + y_ = 0; } void VideoParams::calculate_square_pixel_width() @@ -327,6 +329,8 @@ QByteArray VideoParams::toBytes() const hasher.addData(reinterpret_cast(&interlacing_), sizeof(interlacing_)); hasher.addData(reinterpret_cast(÷r_), sizeof(divider_)); hasher.addData(reinterpret_cast(&enabled_), sizeof(enabled_)); + hasher.addData(reinterpret_cast(&x_), sizeof(x_)); + hasher.addData(reinterpret_cast(&y_), sizeof(y_)); hasher.addData(reinterpret_cast(&stream_index_), sizeof(stream_index_)); hasher.addData(reinterpret_cast(&video_type_), sizeof(video_type_)); hasher.addData(reinterpret_cast(&frame_rate_), sizeof(frame_rate_)); @@ -370,6 +374,10 @@ void VideoParams::Load(QXmlStreamReader *reader) set_divider(reader->readElementText().toInt()); } else if (reader->name() == QStringLiteral("enabled")) { set_enabled(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("x")) { + set_x(reader->readElementText().toFloat()); + } else if (reader->name() == QStringLiteral("y")) { + set_y(reader->readElementText().toFloat()); } else if (reader->name() == QStringLiteral("streamindex")) { set_stream_index(reader->readElementText().toInt()); } else if (reader->name() == QStringLiteral("videotype")) { @@ -402,6 +410,8 @@ void VideoParams::Save(QXmlStreamWriter *writer) const writer->writeTextElement(QStringLiteral("interlacing"), QString::number(interlacing_)); writer->writeTextElement(QStringLiteral("divider"), QString::number(divider_)); writer->writeTextElement(QStringLiteral("enabled"), QString::number(enabled_)); + writer->writeTextElement(QStringLiteral("x"), QString::number(x_)); + writer->writeTextElement(QStringLiteral("y"), QString::number(y_)); writer->writeTextElement(QStringLiteral("streamindex"), QString::number(stream_index_)); writer->writeTextElement(QStringLiteral("videotype"), QString::number(video_type_)); writer->writeTextElement(QStringLiteral("framerate"), frame_rate_.toString()); diff --git a/app/render/videoparams.h b/app/render/videoparams.h index 4179e7ea0..c86e77d4c 100644 --- a/app/render/videoparams.h +++ b/app/render/videoparams.h @@ -21,6 +21,7 @@ #ifndef VIDEOPARAMS_H #define VIDEOPARAMS_H +#include #include #include @@ -273,6 +274,12 @@ public: enabled_ = e; } + float x() const { return x_; } + void set_x(float x) { x_ = x; } + float y() const { return y_; } + void set_y(float y) { y_ = y; } + QVector2D offset() const { return QVector2D(x_, y_); } + int stream_index() const { return stream_index_; @@ -387,6 +394,8 @@ private: int64_t duration_; bool premultiplied_alpha_; QString colorspace_; + float x_; + float y_; };