]> git.sesse.net Git - casparcg/blobdiff - modules/image/producer/image_scroll_producer.cpp
Fixed problem with image_scroll_producer where CALL SPEED was reversed with regard...
[casparcg] / modules / image / producer / image_scroll_producer.cpp
index 41e68f71f982155d158ed95f1dc3923b5198c699..6bef3da8ee693c19fadb2507b262e948b0f9f56c 100644 (file)
-/*\r
-* Copyright (c) 2011 Sveriges Television AB <info@casparcg.com>\r
-*\r
-* This file is part of CasparCG (www.casparcg.com).\r
-*\r
-* CasparCG is free software: you can redistribute it and/or modify\r
-* it under the terms of the GNU General Public License as published by\r
-* the Free Software Foundation, either version 3 of the License, or\r
-* (at your option) any later version.\r
-*\r
-* CasparCG is distributed in the hope that it will be useful,\r
-* but WITHOUT ANY WARRANTY; without even the implied warranty of\r
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
-* GNU General Public License for more details.\r
-*\r
-* You should have received a copy of the GNU General Public License\r
-* along with CasparCG. If not, see <http://www.gnu.org/licenses/>.\r
-*\r
-* Author: Robert Nagy, ronag89@gmail.com\r
-*/\r
-\r
-#include "image_scroll_producer.h"\r
-\r
-#include "../util/image_loader.h"\r
-\r
-#include <core/video_format.h>\r
-\r
-#include <core/frame/frame.h>\r
-#include <core/frame/draw_frame.h>\r
-#include <core/frame/frame_factory.h>\r
-#include <core/frame/frame_transform.h>\r
-#include <core/frame/pixel_format.h>\r
-#include <core/monitor/monitor.h>\r
-\r
-#include <common/env.h>\r
-#include <common/log.h>\r
-#include <common/except.h>\r
-#include <common/memory/array.h>\r
-\r
-#include <boost/assign.hpp>\r
-#include <boost/filesystem.hpp>\r
-#include <boost/foreach.hpp>\r
-#include <boost/lexical_cast.hpp>\r
-#include <boost/property_tree/ptree.hpp>\r
-\r
-#include <algorithm>\r
-#include <array>\r
-\r
-using namespace boost::assign;\r
-\r
-namespace caspar { namespace image {\r
-               \r
-struct image_scroll_producer : public core::frame_producer\r
-{      \r
-       monitor::basic_subject                  event_subject_;\r
-\r
-       const std::wstring                              filename_;\r
-       std::vector<core::draw_frame>   frames_;\r
-       core::video_format_desc                 format_desc_;\r
-       int                                                             width_;\r
-       int                                                             height_;\r
-\r
-       int                                                             delta_;\r
-       int                                                             speed_;\r
-\r
-       std::array<double, 2>                   start_offset_;\r
-\r
-       core::draw_frame                                last_frame_;\r
-\r
-       explicit image_scroll_producer(const spl::shared_ptr<core::frame_factory>& frame_factory, const core::video_format_desc& format_desc, const std::wstring& filename, int speed) \r
-               : filename_(filename)\r
-               , delta_(0)\r
-               , format_desc_(format_desc)\r
-               , speed_(speed)\r
-               , last_frame_(core::draw_frame::empty())\r
-       {\r
-               start_offset_.assign(0.0);\r
-\r
-               auto bitmap = load_image(filename_);\r
-               FreeImage_FlipVertical(bitmap.get());\r
-\r
-               width_  = FreeImage_GetWidth(bitmap.get());\r
-               height_ = FreeImage_GetHeight(bitmap.get());\r
-\r
-               auto bytes = FreeImage_GetBits(bitmap.get());\r
-               auto count = width_*height_*4;\r
-\r
-               if(height_ > format_desc_.height)\r
-               {\r
-                       while(count > 0)\r
-                       {\r
-                               core::pixel_format_desc desc = core::pixel_format::bgra;\r
-                               desc.planes.push_back(core::pixel_format_desc::plane(width_, format_desc_.height, 4));\r
-                               auto frame = frame_factory->create_frame(reinterpret_cast<void*>(rand()), desc);\r
-\r
-                               if(count >= frame.image_data(0).size())\r
-                               {       \r
-                                       std::copy_n(bytes + count - frame.image_data(0).size(), frame.image_data(0).size(), frame.image_data(0).begin());\r
-                                       count -= static_cast<int>(frame.image_data(0).size());\r
-                               }\r
-                               else\r
-                               {\r
-                                       memset(frame.image_data(0).begin(), 0, frame.image_data(0).size());     \r
-                                       std::copy_n(bytes, count, frame.image_data(0).begin() + format_desc_.size - count);\r
-                                       count = 0;\r
-                               }\r
-                       \r
-                               frames_.push_back(core::draw_frame(std::move(frame)));\r
-                       }\r
-                       \r
-                       if(speed_ < 0.0)\r
-                       {\r
-                               auto offset = format_desc_.height - (height_ % format_desc_.height);\r
-                               auto offset2 = offset * 0.5/static_cast<double>(format_desc_.height);\r
-                               start_offset_[1] = (std::ceil(static_cast<double>(height_) / static_cast<double>(format_desc_.height)) + 1.0) * 0.5 - offset2;// - 1.5;\r
-                       }\r
-               }\r
-               else\r
-               {\r
-                       int i = 0;\r
-                       while(count > 0)\r
-                       {\r
-                               core::pixel_format_desc desc = core::pixel_format::bgra;\r
-                               desc.planes.push_back(core::pixel_format_desc::plane(format_desc_.width, height_, 4));\r
-                               auto frame = frame_factory->create_frame(reinterpret_cast<void*>(rand()), desc);\r
-                               if(count >= frame.image_data(0).size())\r
-                               {       \r
-                                       for(int y = 0; y < height_; ++y)\r
-                                               std::copy_n(bytes + i * format_desc_.width*4 + y * width_*4, format_desc_.width*4, frame.image_data(0).begin() + y * format_desc_.width*4);\r
-                                       \r
-                                       ++i;\r
-                                       count -= static_cast<int>(frame.image_data(0).size());\r
-                               }\r
-                               else\r
-                               {\r
-                                       memset(frame.image_data(0).begin(), 0, frame.image_data(0).size());     \r
-                                       auto width2 = width_ % format_desc_.width;\r
-                                       for(int y = 0; y < height_; ++y)\r
-                                               std::copy_n(bytes + i * format_desc_.width*4 + y * width_*4, width2*4, frame.image_data(0).begin() + y * format_desc_.width*4);\r
-\r
-                                       count = 0;\r
-                               }\r
-                       \r
-                               frames_.push_back(core::draw_frame(std::move(frame)));\r
-                       }\r
-\r
-                       std::reverse(frames_.begin(), frames_.end());\r
-\r
-                       if(speed_ > 0.0)\r
-                       {\r
-                               auto offset = format_desc_.width - (width_ % format_desc_.width);\r
-                               start_offset_[0] = offset * 0.5/static_cast<double>(format_desc_.width);\r
-                       }\r
-                       else\r
-                       {\r
-                               start_offset_[0] = (std::ceil(static_cast<double>(width_) / static_cast<double>(format_desc_.width)) + 1.0) * 0.5;// - 1.5;\r
-                       }\r
-               }\r
-\r
-               CASPAR_LOG(info) << print() << L" Initialized";\r
-       }\r
-       \r
-       // frame_producer\r
-\r
-       virtual core::draw_frame receive(int) override\r
-       {               \r
-               delta_ += speed_;\r
-\r
-               if(frames_.empty())\r
-                       return core::draw_frame::eof();\r
-               \r
-               if(height_ > format_desc_.height)\r
-               {\r
-                       if(static_cast<int>(std::abs(delta_)) >= height_ - format_desc_.height)\r
-                               return core::draw_frame::eof();\r
-\r
-                       for(int n = 0; n < frames_.size(); ++n)\r
-                       {\r
-                               frames_[n].transform().image_transform.fill_translation[0] = start_offset_[0];\r
-                               frames_[n].transform().image_transform.fill_translation[1] =    start_offset_[1] - (n+1) + delta_ * 0.5/static_cast<double>(format_desc_.height);\r
-                       }\r
-               }\r
-               else\r
-               {\r
-                       if(static_cast<int>(std::abs(delta_)) >= width_ - format_desc_.width)\r
-                               return core::draw_frame::eof();\r
-\r
-                       for(int n = 0; n < frames_.size(); ++n)\r
-                       {\r
-                               frames_[n].transform().image_transform.fill_translation[0] = start_offset_[0] - (n+1) + delta_ * 0.5/static_cast<double>(format_desc_.width);                           \r
-                               frames_[n].transform().image_transform.fill_translation[1] = start_offset_[1];\r
-                       }\r
-               }\r
-               \r
-               event_subject_ << monitor::event("file/path") % filename_\r
-                                          << monitor::event("delta") % delta_ \r
-                                          << monitor::event("speed") % speed_;\r
-\r
-               return last_frame_ = core::draw_frame(frames_);\r
-       }\r
-\r
-       virtual core::draw_frame last_frame() const override\r
-       {\r
-               return core::draw_frame::still(last_frame_);\r
-       }\r
-                       \r
-       virtual std::wstring print() const override\r
-       {\r
-               return L"image_scroll_producer[" + filename_ + L"]";\r
-       }\r
-\r
-       virtual std::wstring name() const override\r
-       {\r
-               return L"image-scroll";\r
-       }\r
-\r
-       virtual boost::property_tree::wptree info() const override\r
-       {\r
-               boost::property_tree::wptree info;\r
-               info.add(L"type", L"image-scroll");\r
-               info.add(L"filename", filename_);\r
-               return info;\r
-       }\r
-\r
-       virtual uint32_t nb_frames() const override\r
-       {\r
-               if(height_ > format_desc_.height)\r
-               {\r
-                       auto length = (height_ - format_desc_.height);\r
-                       return length/std::abs(speed_);// + length % std::abs(delta_));\r
-               }\r
-               else\r
-               {\r
-                       auto length = (width_ - format_desc_.width);\r
-                       auto result = length/std::abs(speed_);// + length % std::abs(delta_));\r
-                       return result;\r
-               }\r
-       }\r
-\r
-       virtual void subscribe(const monitor::observable::observer_ptr& o) override                                                                                                                     \r
-       {\r
-               return event_subject_.subscribe(o);\r
-       }\r
-\r
-       virtual void unsubscribe(const monitor::observable::observer_ptr& o) override           \r
-       {\r
-               return event_subject_.unsubscribe(o);\r
-       }\r
-};\r
-\r
-spl::shared_ptr<core::frame_producer> create_scroll_producer(const spl::shared_ptr<core::frame_factory>& frame_factory, const core::video_format_desc& format_desc, const std::vector<std::wstring>& params)\r
-{\r
-       static const std::vector<std::wstring> extensions = list_of(L"png")(L"tga")(L"bmp")(L"jpg")(L"jpeg")(L"gif")(L"tiff")(L"tif")(L"jp2")(L"jpx")(L"j2k")(L"j2c");\r
-       std::wstring filename = env::media_folder() + L"\\" + params[0];\r
-       \r
-       auto ext = std::find_if(extensions.begin(), extensions.end(), [&](const std::wstring& ex) -> bool\r
-               {                                       \r
-                       return boost::filesystem::is_regular_file(boost::filesystem::wpath(filename).replace_extension(ex));\r
-               });\r
-\r
-       if(ext == extensions.end())\r
-               return core::frame_producer::empty();\r
-       \r
-       int speed = 0;\r
-       auto speed_it = std::find(params.begin(), params.end(), L"SPEED");\r
-       if(speed_it != params.end())\r
-       {\r
-               if(++speed_it != params.end())\r
-                       speed = boost::lexical_cast<int>(*speed_it);\r
-       }\r
-\r
-       if(speed == 0)\r
-               return core::frame_producer::empty();\r
-\r
-       return spl::make_shared<image_scroll_producer>(frame_factory, format_desc, filename + L"." + *ext, speed);\r
-}\r
-\r
-}}
\ No newline at end of file
+/*
+* Copyright (c) 2011 Sveriges Television AB <info@casparcg.com>
+*
+* This file is part of CasparCG (www.casparcg.com).
+*
+* CasparCG is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* CasparCG is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with CasparCG. If not, see <http://www.gnu.org/licenses/>.
+*
+* Author: Robert Nagy, ronag89@gmail.com
+* Author: Helge Norberg, helge.norberg@svt.se
+*/
+
+#include "image_scroll_producer.h"
+
+#include "../util/image_loader.h"
+#include "../util/image_view.h"
+#include "../util/image_algorithms.h"
+
+#include <core/video_format.h>
+
+#include <core/frame/frame.h>
+#include <core/frame/draw_frame.h>
+#include <core/frame/frame_factory.h>
+#include <core/frame/frame_transform.h>
+#include <core/frame/pixel_format.h>
+#include <core/frame/audio_channel_layout.h>
+#include <core/monitor/monitor.h>
+#include <core/help/help_sink.h>
+#include <core/help/help_repository.h>
+
+#include <common/env.h>
+#include <common/log.h>
+#include <common/except.h>
+#include <common/array.h>
+#include <common/tweener.h>
+#include <common/param.h>
+#include <common/os/filesystem.h>
+#include <common/future.h>
+
+#include <boost/filesystem.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/scoped_array.hpp>
+#include <boost/date_time.hpp>
+#include <boost/date_time/posix_time/ptime.hpp>
+
+#include <algorithm>
+#include <array>
+#include <cstdint>
+
+namespace caspar { namespace image {
+               
+// Like tweened_transform but for speed
+class speed_tweener
+{
+       double  source_         = 0.0;
+       double  dest_           = 0.0;
+       int             duration_       = 0;
+       int             time_           = 0;
+       tweener tweener_;
+public:
+       speed_tweener() = default;
+       speed_tweener(
+                       double source,
+                       double dest,
+                       int duration,
+                       const tweener& tween)
+               : source_(source)
+               , dest_(dest)
+               , duration_(duration)
+               , time_(0)
+               , tweener_(tween)
+       {
+       }
+
+       double dest() const
+       {
+               return dest_;
+       }
+
+       double fetch() const
+       {
+               if (time_ == duration_)
+                       return dest_;
+
+               double delta = dest_ - source_;
+               double result = tweener_(time_, source_, delta, duration_);
+
+               return result;
+       }
+
+       double fetch_and_tick()
+       {
+               time_ = std::min(time_ + 1, duration_);
+               return fetch();
+       }
+};
+       
+struct image_scroll_producer : public core::frame_producer_base
+{      
+       core::monitor::subject                                          monitor_subject_;
+
+       const std::wstring                                                      filename_;
+       std::vector<core::draw_frame>                           frames_;
+       core::video_format_desc                                         format_desc_;
+       int                                                                                     width_;
+       int                                                                                     height_;
+       core::constraints                                                       constraints_;
+
+       double                                                                          delta_                          = 0.0;
+       speed_tweener                                                           speed_;
+       boost::optional<boost::posix_time::ptime>       end_time_;
+
+       int                                                                                     start_offset_x_         = 0;
+       int                                                                                     start_offset_y_         = 0;
+       bool                                                                            progressive_;
+       
+       explicit image_scroll_producer(
+                       const spl::shared_ptr<core::frame_factory>& frame_factory,
+                       const core::video_format_desc& format_desc,
+                       const std::wstring& filename,
+                       double s,
+                       double duration,
+                       boost::optional<boost::posix_time::ptime> end_time,
+                       int motion_blur_px = 0,
+                       bool premultiply_with_alpha = false,
+                       bool progressive = false)
+               : filename_(filename)
+               , format_desc_(format_desc)
+               , end_time_(std::move(end_time))
+               , progressive_(progressive)
+       {
+               double speed = s;
+
+               if (end_time_)
+                       speed = -1.0;
+
+               auto bitmap = load_image(filename_);
+               FreeImage_FlipVertical(bitmap.get());
+
+               width_  = FreeImage_GetWidth(bitmap.get());
+               height_ = FreeImage_GetHeight(bitmap.get());
+               constraints_.width.set(width_);
+               constraints_.height.set(height_);
+
+               bool vertical = width_ == format_desc_.width;
+               bool horizontal = height_ == format_desc_.height;
+
+               if (!vertical && !horizontal)
+                       CASPAR_THROW_EXCEPTION(caspar::user_error()
+                                       << msg_info("Neither width nor height matched the video resolution"));
+
+               if (duration != 0.0)
+                       speed = speed_from_duration(duration);
+
+               if (vertical)
+               {
+                       if (speed < 0.0)
+                       {
+                               start_offset_y_ = height_ + format_desc_.height;
+                       }
+               }
+               else
+               {
+                       if (speed > 0.0)
+                               start_offset_x_ = format_desc_.width - (width_ % format_desc_.width);
+                       else
+                               start_offset_x_ = format_desc_.width - (width_ % format_desc_.width) + width_ + format_desc_.width;
+               }
+
+               speed_ = speed_tweener(speed, speed, 0, tweener(L"linear"));
+
+               auto bytes = FreeImage_GetBits(bitmap.get());
+               auto count = width_*height_*4;
+               image_view<bgra_pixel> original_view(bytes, width_, height_);
+
+               if (premultiply_with_alpha)
+                       premultiply(original_view);
+
+               boost::scoped_array<uint8_t> blurred_copy;
+
+               if (motion_blur_px > 0)
+               {
+                       double angle = 3.14159265 / 2; // Up
+
+                       if (horizontal && speed < 0)
+                               angle *= 2; // Left
+                       else if (vertical && speed > 0)
+                               angle *= 3; // Down
+                       else if (horizontal && speed  > 0)
+                               angle = 0.0; // Right
+
+                       blurred_copy.reset(new uint8_t[count]);
+                       image_view<bgra_pixel> blurred_view(blurred_copy.get(), width_, height_);
+                       caspar::tweener blur_tweener(L"easeInQuad");
+                       blur(original_view, blurred_view, angle, motion_blur_px, blur_tweener);
+                       bytes = blurred_copy.get();
+                       bitmap.reset();
+               }
+
+               if (vertical)
+               {
+                       int n = 1;
+
+                       while(count > 0)
+                       {
+                               core::pixel_format_desc desc = core::pixel_format::bgra;
+                               desc.planes.push_back(core::pixel_format_desc::plane(width_, format_desc_.height, 4));
+                               auto frame = frame_factory->create_frame(this, desc, core::audio_channel_layout::invalid());
+
+                               if(count >= frame.image_data(0).size())
+                               {       
+                                       std::copy_n(bytes + count - frame.image_data(0).size(), frame.image_data(0).size(), frame.image_data(0).begin());
+                                       count -= static_cast<int>(frame.image_data(0).size());
+                               }
+                               else
+                               {
+                                       memset(frame.image_data(0).begin(), 0, frame.image_data(0).size());     
+                                       std::copy_n(bytes, count, frame.image_data(0).begin() + format_desc_.size - count);
+                                       count = 0;
+                               }
+
+                               core::draw_frame draw_frame(std::move(frame));
+
+                               // Set the relative position to the other image fragments
+                               draw_frame.transform().image_transform.fill_translation[1] = - n++;
+
+                               frames_.push_back(draw_frame);
+                       }
+               }
+               else if (horizontal)
+               {
+                       int i = 0;
+                       while(count > 0)
+                       {
+                               core::pixel_format_desc desc = core::pixel_format::bgra;
+                               desc.planes.push_back(core::pixel_format_desc::plane(format_desc_.width, height_, 4));
+                               auto frame = frame_factory->create_frame(this, desc, core::audio_channel_layout::invalid());
+                               if(count >= frame.image_data(0).size())
+                               {       
+                                       for(int y = 0; y < height_; ++y)
+                                               std::copy_n(bytes + i * format_desc_.width*4 + y * width_*4, format_desc_.width*4, frame.image_data(0).begin() + y * format_desc_.width*4);
+                                       
+                                       ++i;
+                                       count -= static_cast<int>(frame.image_data(0).size());
+                               }
+                               else
+                               {
+                                       memset(frame.image_data(0).begin(), 0, frame.image_data(0).size());     
+                                       auto width2 = width_ % format_desc_.width;
+                                       for(int y = 0; y < height_; ++y)
+                                               std::copy_n(bytes + i * format_desc_.width*4 + y * width_*4, width2*4, frame.image_data(0).begin() + y * format_desc_.width*4);
+
+                                       count = 0;
+                               }
+                       
+                               frames_.push_back(core::draw_frame(std::move(frame)));
+                       }
+
+                       std::reverse(frames_.begin(), frames_.end());
+
+                       // Set the relative positions of the image fragments.
+                       for (size_t n = 0; n < frames_.size(); ++n)
+                       {
+                               double translation = - (static_cast<double>(n) + 1.0);
+                               frames_[n].transform().image_transform.fill_translation[0] = translation;
+                       }
+               }
+
+               CASPAR_LOG(info) << print() << L" Initialized";
+       }
+
+       double get_total_num_pixels() const
+       {
+               bool vertical = width_ == format_desc_.width;
+
+               if (vertical)
+                       return height_ + format_desc_.height;
+               else
+                       return width_ + format_desc_.width;
+       }
+
+       double speed_from_duration(double duration_seconds) const
+       {
+               return get_total_num_pixels() / (duration_seconds * format_desc_.fps * static_cast<double>(format_desc_.field_count));
+       }
+
+       std::future<std::wstring> call(const std::vector<std::wstring>& params) override
+       {
+               auto cmd = params.at(0);
+
+               if (boost::iequals(cmd, L"SPEED"))
+               {
+                       if (params.size() == 1)
+                               return make_ready_future(boost::lexical_cast<std::wstring>(-speed_.fetch()));
+
+                       auto val = boost::lexical_cast<double>(params.at(1));
+                       int duration = params.size() > 2 ? boost::lexical_cast<int>(params.at(2)) : 0;
+                       std::wstring tween = params.size() > 3 ? params.at(3) : L"linear";
+                       speed_ = speed_tweener(speed_.fetch(), -val, duration, tween);
+               }
+
+               return make_ready_future<std::wstring>(L"");
+       }
+
+       std::vector<core::draw_frame> get_visible()
+       {
+               std::vector<core::draw_frame> result;
+               result.reserve(frames_.size());
+
+               for (auto& frame : frames_)
+               {
+                       auto& fill_translation = frame.transform().image_transform.fill_translation;
+
+                       if (width_ == format_desc_.width)
+                       {
+                               auto motion_offset_in_screens = (static_cast<double>(start_offset_y_) + delta_) / static_cast<double>(format_desc_.height);
+                               auto vertical_offset = fill_translation[1] + motion_offset_in_screens;
+
+                               if (vertical_offset < -1.0 || vertical_offset > 1.0)
+                               {
+                                       continue;
+                               }
+                       }
+                       else
+                       {
+                               auto motion_offset_in_screens = (static_cast<double>(start_offset_x_) + delta_) / static_cast<double>(format_desc_.width);
+                               auto horizontal_offset = fill_translation[0] + motion_offset_in_screens;
+
+                               if (horizontal_offset < -1.0 || horizontal_offset > 1.0)
+                               {
+                                       continue;
+                               }
+                       }
+
+                       result.push_back(frame);
+               }
+
+               return std::move(result);
+       }
+       
+       // frame_producer
+       core::draw_frame render_frame(bool allow_eof)
+       {
+               if(frames_.empty())
+                       return core::draw_frame::empty();
+               
+               core::draw_frame result(get_visible());
+               auto& fill_translation = result.transform().image_transform.fill_translation;
+
+               if (width_ == format_desc_.width)
+               {
+                       if (static_cast<size_t>(std::abs(delta_)) >= height_ + format_desc_.height && allow_eof)
+                               return core::draw_frame::empty();
+
+                       fill_translation[1] = 
+                               static_cast<double>(start_offset_y_) / static_cast<double>(format_desc_.height)
+                               + delta_ / static_cast<double>(format_desc_.height);
+               }
+               else
+               {
+                       if (static_cast<size_t>(std::abs(delta_)) >= width_ + format_desc_.width && allow_eof)
+                               return core::draw_frame::empty();
+
+                       fill_translation[0] = 
+                               static_cast<double>(start_offset_x_) / static_cast<double>(format_desc_.width)
+                               + (delta_) / static_cast<double>(format_desc_.width);
+               }
+
+               return result;
+       }
+
+       core::draw_frame render_frame(bool allow_eof, bool advance_delta)
+       {
+               auto result = render_frame(allow_eof);
+
+               if (advance_delta)
+               {
+                       advance();
+               }
+
+               return result;
+       }
+
+       void advance()
+       {
+               if (end_time_)
+               {
+                       boost::posix_time::ptime now(boost::posix_time::second_clock::local_time());
+
+                       auto diff = *end_time_ - now;
+                       auto seconds = diff.total_seconds();
+
+                       set_speed(-speed_from_duration(seconds));
+                       end_time_ = boost::none;
+               }
+               else
+                       delta_ += speed_.fetch_and_tick();
+       }
+
+       void set_speed(double speed)
+       {
+               speed_ = speed_tweener(speed, speed, 0, tweener(L"linear"));
+       }
+
+       core::draw_frame receive_impl() override
+       {
+               core::draw_frame result;
+
+               if (format_desc_.field_mode == core::field_mode::progressive || progressive_)
+               {
+                       result = render_frame(true, true);
+               }
+               else
+               {
+                       auto field1 = render_frame(true, true);
+                       auto field2 = render_frame(true, false);
+
+                       if (field1 != core::draw_frame::empty() && field2 == core::draw_frame::empty())
+                       {
+                               field2 = render_frame(false, true);
+                       }
+                       else
+                       {
+                               advance();
+                       }
+
+                       result = core::draw_frame::interlace(field1, field2, format_desc_.field_mode);
+               }
+               
+               monitor_subject_ << core::monitor::message("/file/path") % filename_
+                                                << core::monitor::message("/delta") % delta_ 
+                                                << core::monitor::message("/speed") % speed_.fetch();
+
+               return result;
+       }
+
+       core::constraints& pixel_constraints() override
+       {
+               return constraints_;
+       }
+                               
+       std::wstring print() const override
+       {
+               return L"image_scroll_producer[" + filename_ + L"]";
+       }
+
+       std::wstring name() const override
+       {
+               return L"image-scroll";
+       }
+
+       boost::property_tree::wptree info() const override
+       {
+               boost::property_tree::wptree info;
+               info.add(L"type", L"image-scroll");
+               info.add(L"filename", filename_);
+               info.add(L"speed", speed_.fetch());
+               return info;
+       }
+
+       uint32_t nb_frames() const override
+       {
+               if(width_ == format_desc_.width)
+               {
+                       auto length = (height_ + format_desc_.height * 2);
+                       return static_cast<uint32_t>(length / std::abs(speed_.fetch()));// + length % std::abs(delta_));
+               }
+               else
+               {
+                       auto length = (width_ + format_desc_.width * 2);
+                       return static_cast<uint32_t>(length / std::abs(speed_.fetch()));// + length % std::abs(delta_));
+               }
+       }
+
+       core::monitor::subject& monitor_output()
+       {
+               return monitor_subject_;
+       }
+};
+
+void describe_scroll_producer(core::help_sink& sink, const core::help_repository& repo)
+{
+       sink.short_description(L"Scrolls an image either horizontally or vertically.");
+       sink.syntax(L"[image_file:string] SPEED [speed:float] {BLUR [blur_px:int]} {[premultiply:PREMULTIPLY]} {[progressive:PROGRESSIVE]}");
+       sink.syntax(L"[image_file:string] DURATION [duration:float] {BLUR [blur_px:int]} {[premultiply:PREMULTIPLY]} {[progressive:PROGRESSIVE]}");
+       sink.syntax(L"[image_file:string] END_TIME [end_time:string] {BLUR [blur_px:int]} {[premultiply:PREMULTIPLY]} {[progressive:PROGRESSIVE]}");
+       sink.para()
+               ->text(L"Scrolls an image either horizontally or vertically. ")
+               ->text(L"It is the image dimensions that decide if it will be a vertical scroll or a horizontal scroll. ")
+               ->text(L"A horizontal scroll will be selected if the image height is exactly the same as the video format height. ")
+               ->text(L"A vertical scroll will be selected if the image width is exactly the same as the video format width.");
+       sink.definitions()
+               ->item(L"image_file", L"The image without extension. The file has to have either the same width or the same height as the video format.")
+               ->item(L"speed", L"A positive or negative float defining how many pixels to move the image each frame.")
+               ->item(L"duration", L"A positive or negative float defining how many seconds the scroll should take.")
+               ->item(L"end_time", L"An absolute date in the format yyyy-MM-dd HH:mm:ss for when the scroll should end, based on the system clock.")
+               ->item(L"blur_px", L"If specified, will do a directional blur in the scrolling direction by the given number of pixels.")
+               ->item(L"premultiply", L"If the image is in straight alpha, use this option to make it display correctly in CasparCG.")
+               ->item(L"progressive", L"When an interlaced video format is used, by default the image is moved every field. This can be overridden by specifying this option, causing the image to only move on full frames.");
+       sink.para()->text(L"If ")->code(L"SPEED [speed]")->text(L" is ommitted, the ordinary ")->see(L"Image Producer")->text(L" will be used instead.");
+       sink.example(L">> PLAY 1-10 cred_1280 SPEED 8 BLUR 2", L"Given that cred_1280 is a as wide as the video mode, this will create a rolling end credits with a little bit of blur and a speed of 8 pixels per frame.");
+       sink.example(L">> PLAY 1-10 cred_1280 DURATION 60", L"Will adjust the speed to make the scroll take 60 seconds.");
+       sink.example(L">> PLAY 1-10 cred_1920 END_TIME \"2016-05-15 00:46:17\"", L"Will adjust the speed to make the scroll end at the specific date and time 2016-05-15 00:46:17.");
+}
+
+spl::shared_ptr<core::frame_producer> create_scroll_producer(const core::frame_producer_dependencies& dependencies, const std::vector<std::wstring>& params)
+{
+       static const auto extensions = {
+               L".png",
+               L".tga",
+               L".bmp",
+               L".jpg",
+               L".jpeg",
+               L".gif",
+               L".tiff",
+               L".tif",
+               L".jp2",
+               L".jpx",
+               L".j2k",
+               L".j2c"
+       };
+       std::wstring filename = env::media_folder() + params.at(0);
+       
+       auto ext = std::find_if(extensions.begin(), extensions.end(), [&](const std::wstring& ex) -> bool
+       {
+               auto file = caspar::find_case_insensitive(boost::filesystem::path(filename).replace_extension(ex).wstring());
+
+               return static_cast<bool>(file);
+       });
+
+       if(ext == extensions.end())
+               return core::frame_producer::empty();
+       
+       double duration = 0.0;
+       double speed = get_param(L"SPEED", params, 0.0);
+       boost::optional<boost::posix_time::ptime> end_time;
+
+       if (speed == 0)
+               duration = get_param(L"DURATION", params, 0.0);
+
+       if (duration == 0)
+       {
+               auto end_time_str = get_param(L"END_TIME", params);
+
+               if (!end_time_str.empty())
+               {
+                       end_time = boost::posix_time::time_from_string(u8(end_time_str));
+               }
+       }
+
+       if(speed == 0 && duration == 0 && !end_time)
+               return core::frame_producer::empty();
+
+       int motion_blur_px = get_param(L"BLUR", params, 0);
+
+       bool premultiply_with_alpha = contains_param(L"PREMULTIPLY", params);
+       bool progressive = contains_param(L"PROGRESSIVE", params);
+
+       return core::create_destroy_proxy(spl::make_shared<image_scroll_producer>(
+                       dependencies.frame_factory,
+                       dependencies.format_desc,
+                       *caspar::find_case_insensitive(filename + *ext),
+                       -speed,
+                       -duration,
+                       end_time,
+                       motion_blur_px,
+                       premultiply_with_alpha,
+                       progressive));
+}
+
+}}