]> git.sesse.net Git - casparcg/blobdiff - modules/psd/layer.cpp
Use namespace for isnan() in PSD module
[casparcg] / modules / psd / layer.cpp
index 40906e0ae2fb8475a2eb3a615bfe8e9aaeb22357..d62cf5119b83e9ff98c4383a5490af5cfb0cd2d5 100644 (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: Niklas P Andersson, niklas.p.andersson@svt.se
-*/
-
-#include "layer.h"
-#include "doc.h"
-#include "descriptor.h"
-#include <common/log.h>
-#include "util\pdf_reader.h"
-
-#include <algorithm>
-#include "../image/util/image_algorithms.h"
-#include "../image/util/image_view.h"
-
-#include <boost/property_tree/ptree.hpp>
-
-namespace caspar { namespace psd {
-
-void layer::layer_mask_info::read_mask_data(BEFileInputStream& stream)
-{
-       unsigned long length = stream.read_long();
-       switch(length)
-       {
-       case 0:
-               break;
-
-       case 20:
-       case 36:        //discard total user mask data
-               rect_.location.y = stream.read_long();
-               rect_.location.x = stream.read_long();
-               rect_.size.height = stream.read_long() - rect_.location.y;
-               rect_.size.width = stream.read_long() - rect_.location.x;
-
-               default_value_ = stream.read_byte();
-               flags_ = stream.read_byte();
-               stream.discard_bytes(2);
-               
-               if(length == 36)
-               {
-                       stream.discard_bytes(18);       //discard "total user mask" data
-                       //flags_ = stream.read_byte();
-                       //default_value_ = stream.read_byte();
-                       //rect_.location.y = stream.read_long();
-                       //rect_.location.x = stream.read_long();
-                       //rect_.size.height = stream.read_long() - rect_.location.y;
-                       //rect_.size.width = stream.read_long() - rect_.location.x;
-               }
-               break;
-
-       default:
-               //TODO: Log that we discard a mask that is not supported
-               stream.discard_bytes(length);
-               break;
-       };
-}
-
-struct layer::impl
-{
-       friend class layer;
-
-       impl() : blend_mode_(InvalidBlendMode), link_group_id_(0), opacity_(255), sheet_color_(0), baseClipping_(false), flags_(0), protection_flags_(0), masks_count_(0), text_scale_(1.0f)
-       {}
-
-private:
-       std::vector<channel>                    channels_;
-       blend_mode                                              blend_mode_;
-       int                                                             link_group_id_;
-       unsigned char                                   opacity_;
-       unsigned short                                  sheet_color_;
-       bool                                                    baseClipping_;
-       unsigned char                                   flags_;
-       int                                                             protection_flags_;
-       std::wstring                                    name_;
-       char                                                    masks_count_;
-       float                                                   text_scale_;
-
-       rect<long>                                              vector_mask_;
-       layer::layer_mask_info                  mask_;
-
-       rect<long>                                              bitmap_rect_;
-       image8bit_ptr                                   bitmap_;
-
-       boost::property_tree::wptree    text_layer_info_;
-       boost::property_tree::wptree    timeline_info_;
-
-       color<unsigned char>                    solid_color_;
-
-public:
-       void populate(BEFileInputStream& stream, const psd_document& doc)
-       {
-               bitmap_rect_.location.y = stream.read_long();
-               bitmap_rect_.location.x = stream.read_long();
-               bitmap_rect_.size.height = stream.read_long() - bitmap_rect_.location.y;
-               bitmap_rect_.size.width = stream.read_long() - bitmap_rect_.location.x;
-
-               //Get info about the channels in the layer
-               unsigned short channelCount = stream.read_short();
-               for(int channelIndex = 0; channelIndex < channelCount; ++channelIndex)
-               {
-                       short id = static_cast<short>(stream.read_short());
-                       channel c(id, stream.read_long());
-
-                       if(c.id < -1)
-                               masks_count_++;
-
-                       channels_.push_back(c);
-               }
-
-               unsigned long blendModeSignature = stream.read_long();
-               if(blendModeSignature != '8BIM')
-                       throw PSDFileFormatException();
-
-               blend_mode_ = int_to_blend_mode(stream.read_long());
-               opacity_ = stream.read_byte();
-               baseClipping_ = stream.read_byte() == 1 ? false : true;
-               flags_ = stream.read_byte();
-
-               stream.discard_bytes(1);        //padding
-
-               unsigned long extras_size = stream.read_long();
-               long position = stream.current_position();
-               mask_.read_mask_data(stream);
-               read_blending_ranges(stream);
-               name_ = stream.read_pascal_string(4);
-
-               //Aditional Layer Information
-               long end_of_layer_info = position + extras_size;
-               try
-               {
-                       while(stream.current_position() < end_of_layer_info)
-                       {
-                               read_chunk(stream, doc);
-                       }
-               }
-               catch(PSDFileFormatException&)
-               {
-                       stream.set_position(end_of_layer_info);
-               }
-       }
-
-       void read_chunk(BEFileInputStream& stream, const psd_document& doc, bool isMetadata = false)
-       {
-               unsigned long signature = stream.read_long();
-               if(signature != '8BIM' && signature != '8B64')
-                       throw PSDFileFormatException();
-
-               unsigned long key = stream.read_long();
-
-               if(isMetadata) stream.read_long();
-
-               unsigned long length = stream.read_long();
-               unsigned long end_of_chunk = stream.current_position() + length;
-
-               try
-               {
-                       switch(key)
-                       {
-                       case 'SoCo':
-                               read_solid_color(stream);
-                               break;
-
-                       case 'lspf':    //protection settings
-                               protection_flags_ = stream.read_long();
-                               break;
-
-                       case 'Txt2':    //text engine data
-                               break;
-
-                       case 'luni':
-                               name_ = stream.read_unicode_string();
-                               break;
-
-                       case 'TySh':    //type tool object settings
-                               read_text_data(stream);
-                               break;
-                               
-                       case 'shmd':    //metadata
-                               read_metadata(stream, doc);
-                               break;
-
-                       case 'lclr':
-                               sheet_color_ = stream.read_short();
-                               break;
-                               
-                       case 'lyvr':    //layer version
-                               break;
-
-                       case 'lnkD':    //linked layer
-                       case 'lnk2':    //linked layer
-                       case 'lnk3':    //linked layer
-                               break;
-
-                       case 'vmsk':
-                               read_vector_mask(length, stream, doc.width(), doc.height());
-                               break;
-                               
-                       case 'tmln':
-                               read_timeline_data(stream);
-                               break;
-
-                       default:
-                               break;
-                       }
-               }
-               catch(PSDFileFormatException& ex)
-               {
-                       //ignore failed chunks silently
-                       CASPAR_LOG(warning) << ex.what();
-               }
-
-               stream.set_position(end_of_chunk);
-       }
-
-       void read_solid_color(BEFileInputStream& stream)
-       {
-               if(stream.read_long() != 16)    //"descriptor version" should be 16
-                       throw PSDFileFormatException();
-
-               descriptor solid_descriptor;
-               if(!solid_descriptor.populate(stream))
-                       throw PSDFileFormatException("Failed to read solid color data");
-               else
-               {
-                       solid_color_.red = static_cast<unsigned char>(solid_descriptor.items().get(L"Clr .Rd  ", 0.0) + 0.5);
-                       solid_color_.green = static_cast<unsigned char>(solid_descriptor.items().get(L"Clr .Grn ", 0.0) + 0.5);
-                       solid_color_.blue = static_cast<unsigned char>(solid_descriptor.items().get(L"Clr .Bl  ", 0.0) + 0.5);
-                       solid_color_.alpha = 255;
-               }
-       }
-
-       void read_vector_mask(unsigned long length, BEFileInputStream& stream, long doc_width, long doc_height)
-       {
-               typedef std::pair<unsigned long, unsigned long> path_point;
-
-               unsigned long version = stream.read_long();
-               unsigned long flags = stream.read_long();
-               int path_records = (length-8) / 26;
-
-               long position = stream.current_position();
-
-               std::vector<path_point> knots;
-               for(int i=1; i <= path_records; ++i)
-               {
-                       unsigned short selector = stream.read_short();
-                       if(selector == 2)       //we only concern ourselves with closed paths 
-                       {
-                               unsigned long p_y = stream.read_long();
-                               unsigned long p_x = stream.read_long();
-                               path_point cp_prev(p_x, p_y);
-
-                               unsigned long a_y = stream.read_long();
-                               unsigned long a_x = stream.read_long();
-                               path_point anchor(a_x, a_y);
-
-                               unsigned long n_y = stream.read_long();
-                               unsigned long n_x = stream.read_long();
-                               path_point cp_next(n_x, n_y);
-
-                               if(anchor == cp_prev && anchor == cp_next)
-                                       knots.push_back(anchor);
-                               else
-                               {       //we can't handle smooth curves yet
-                                       throw PSDFileFormatException();
-                               }
-                       }
-
-                       stream.set_position(position + 26*i);
-               }
-
-               if(knots.size() != 4)   //we only support rectangular vector masks
-                       throw PSDFileFormatException();
-
-               //we only support rectangular vector masks
-               if(!(knots[0].first == knots[3].first && knots[1].first == knots[2].first && knots[0].second == knots[1].second && knots[2].second == knots[3].second))
-                       throw PSDFileFormatException();
-
-               //the path_points are given in fixed-point 8.24 as a ratio with regards to the width/height of the document. we need to divide by 16777215.0f to get the real ratio.
-               float x_ratio = doc_width / 16777215.0f;
-               float y_ratio = doc_height / 16777215.0f;
-               vector_mask_.location.x = static_cast<long>(knots[0].first * x_ratio +0.5f);                                                            //add .5 to get propper rounding when converting to integer
-               vector_mask_.location.y = static_cast<long>(knots[0].second * y_ratio +0.5f);                                                           //add .5 to get propper rounding when converting to integer
-               vector_mask_.size.width = static_cast<long>(knots[1].first * x_ratio +0.5f)     - vector_mask_.location.x;              //add .5 to get propper rounding when converting to integer
-               vector_mask_.size.height = static_cast<long>(knots[2].second * y_ratio +0.5f) - vector_mask_.location.y;        //add .5 to get propper rounding when converting to integer
-       }
-
-       void read_metadata(BEFileInputStream& stream, const psd_document& doc)
-       {
-               unsigned long count = stream.read_long();
-               for(unsigned long index = 0; index < count; ++index)
-                       read_chunk(stream, doc, true);
-       }
-
-       void read_timeline_data(BEFileInputStream& stream)
-       {
-               if(stream.read_long() != 16)    //"descriptor version" should be 16
-                       throw PSDFileFormatException();
-
-               descriptor timeline_descriptor;
-               if(!timeline_descriptor.populate(stream))
-                       throw PSDFileFormatException();
-               else
-               {
-                       timeline_info_.swap(timeline_descriptor.items());
-               }
-       }
-
-       void read_text_data(BEFileInputStream& stream)
-       {
-               std::wstring text;      //the text in the layer
-
-               stream.read_short();    //should be 1
-       
-               //transformation info
-               double xx = stream.read_double();
-               double xy = stream.read_double();
-               double yx = stream.read_double();
-               double yy = stream.read_double();
-               double tx = stream.read_double();
-               double ty = stream.read_double();
-               if(xx != yy || (xy != 0 && yx != 0))
-                       throw PSDFileFormatException("Rotation and non-uniform scaling of dynamic textfields is not supported yet");
-
-               text_scale_ = static_cast<float>(xx);
-
-               if(stream.read_short() != 50)   //"text version" should be 50
-                       throw PSDFileFormatException("invalid text version");
-
-               if(stream.read_long() != 16)    //"descriptor version" should be 16
-                       throw PSDFileFormatException("Invalid descriptor version while reading text-data");
-
-               descriptor text_descriptor;
-               if(!text_descriptor.populate(stream))
-                       throw PSDFileFormatException("Failed to read text-info");
-               else
-               {
-                       auto text_info = text_descriptor.items().get_optional<std::wstring>(L"EngineData");
-                       if(text_info.is_initialized())
-                       {
-                               std::string str(text_info.get().begin(), text_info.get().end());
-                               read_pdf(text_layer_info_, str);
-                       }
-               }
-
-               if(stream.read_short() != 1)    //"warp version" should be 1
-                       throw PSDFileFormatException("invalid warp version");
-
-               if(stream.read_long() != 16)    //"descriptor version" should be 16
-                       throw PSDFileFormatException("Invalid descriptor version while reading text warp-data");
-
-               descriptor warp_descriptor;
-               if(!warp_descriptor.populate(stream))
-                       throw PSDFileFormatException("Failed to read text warp-data");
-               else
-               {
-                       double w_top = stream.read_double();
-                       double w_left = stream.read_double();
-                       double w_right = stream.read_double();
-                       double w_bottom = stream.read_double();
-               }
-       }
-
-       //TODO: implement
-       void read_blending_ranges(BEFileInputStream& stream)
-       {
-               unsigned long length = stream.read_long();
-               stream.discard_bytes(length);
-       }
-
-       bool has_channel(channel_type type)
-       {
-               return std::find_if(channels_.begin(), channels_.end(), [=](const channel& c) { return c.id == type; }) != channels_.end();
-       }
-
-       void read_channel_data(BEFileInputStream& stream)
-       {
-               image8bit_ptr bitmap;
-               image8bit_ptr mask;
-
-               bool has_transparency = has_channel(psd::transparency);
-       
-               rect<long> clip_rect;
-               if(!bitmap_rect_.empty())
-               {
-                       clip_rect = bitmap_rect_;
-
-                       if(!vector_mask_.empty())
-                               clip_rect = vector_mask_;
-
-                       bitmap = std::make_shared<image8bit>(clip_rect.size.width, clip_rect.size.height, 4);
-
-                       if(!has_transparency)
-                               std::memset(bitmap->data(), 255, bitmap->width()*bitmap->height()*bitmap->channel_count());
-               }
-
-               if(masks_count_ > 0 && !mask_.rect_.empty())
-               {
-                       mask = std::make_shared<image8bit>(mask_.rect_.size.width, mask_.rect_.size.height, 1);
-               }
-
-               for(auto it = channels_.begin(); it != channels_.end(); ++it)
-               {
-                       psd::rect<long> src_rect;
-                       image8bit_ptr target;
-                       unsigned char offset;
-                       bool discard_channel = false;
-
-                       //determine target bitmap and offset
-                       if((*it).id >= 3)
-                               discard_channel = true; //discard channels that doesn't contribute to the final image
-                       else if((*it).id >= -1) //BGRA-data
-                       {
-                               target = bitmap;
-                               offset = ((*it).id >= 0) ? 2 - (*it).id : 3;
-                               src_rect = bitmap_rect_;
-                       }
-                       else if(mask)   //mask
-                       {
-                               if((*it).id == -3)      //discard the "total user mask"
-                                       discard_channel = true;
-                               else
-                               {
-                                       target = mask;
-                                       offset = 0;
-                                       src_rect = mask_.rect_;
-                               }
-                       }
-
-                       if(!target || src_rect.empty())
-                               discard_channel = true;
-
-                       unsigned long end_of_data = stream.current_position() + (*it).data_length;
-                       if(!discard_channel)
-                       {
-                               unsigned short encoding = stream.read_short();
-                               if(target)
-                               {
-                                       if(encoding == 0)
-                                               read_raw_image_data(stream, (*it).data_length-2, target, offset);
-                                       else if(encoding == 1)
-                                               read_rle_image_data(stream, src_rect, (target == bitmap) ? clip_rect : mask_.rect_, target, offset);
-                                       else
-                                               throw PSDFileFormatException();
-                               }
-                       }
-                       stream.set_position(end_of_data);
-               }
-
-               if(bitmap && has_transparency)
-               {
-                       caspar::image::image_view<caspar::image::bgra_pixel> view(bitmap->data(), bitmap->width(), bitmap->height());
-                       caspar::image::premultiply(view);
-               }
-
-               bitmap_ = bitmap;
-               bitmap_rect_ = clip_rect;
-               mask_.bitmap_ = mask;
-       }
-
-       void read_raw_image_data(BEFileInputStream& stream, unsigned long data_length, image8bit_ptr target, unsigned char offset)
-       {
-               unsigned long total_length = target->width() * target->height();
-               if(total_length != data_length)
-                       throw PSDFileFormatException();
-
-               unsigned char* data = target->data();
-
-               unsigned char stride = target->channel_count();
-               if(stride == 1)
-                       stream.read(reinterpret_cast<char*>(data + offset), total_length);
-               else
-               {
-                       for(unsigned long index=0; index < total_length; ++index)
-                               data[index*stride+offset] = stream.read_byte();
-               }
-       }
-
-       void read_rle_image_data(BEFileInputStream& stream, const rect<long>&src_rect, const rect<long>&clip_rect, image8bit_ptr target, unsigned char offset)
-       {
-               unsigned long width = src_rect.size.width;
-               unsigned long height = src_rect.size.height;
-               unsigned char stride = target->channel_count();
-
-               int offset_x = clip_rect.location.x - src_rect.location.x;
-               int offset_y = clip_rect.location.y - src_rect.location.y;
-
-               std::vector<unsigned short> scanline_lengths;
-               scanline_lengths.reserve(height);
-
-               for(unsigned long scanlineIndex=0; scanlineIndex < height; ++scanlineIndex)
-                       scanline_lengths.push_back(stream.read_short());
-
-               unsigned char *target_data = target->data();
-
-               std::vector<unsigned char> line(width);
-
-               for(long scanlineIndex=0; scanlineIndex < height; ++scanlineIndex)
-               {
-                       if(scanlineIndex >= target->height()+offset_y)
-                               break;
-
-                       unsigned long colIndex = 0;
-                       unsigned char length = 0;
-                       do
-                       {
-                               length = 0;
-
-                               //Get controlbyte
-                               char controlByte = static_cast<char>(stream.read_byte());
-                               if(controlByte >= 0)
-                               {
-                                       //Read uncompressed string
-                                       length = controlByte+1;
-                                       for(unsigned long index=0; index < length; ++index)
-                                               line[colIndex+index] = stream.read_byte();
-                               }
-                               else if(controlByte > -128)
-                               {
-                                       //Repeat next byte
-                                       length = -controlByte+1;
-                                       unsigned char value = stream.read_byte();
-                                       for(unsigned long index=0; index < length; ++index)
-                                               line[colIndex+index] = value;
-                               }
-
-                               colIndex += length;
-                       }
-                       while(colIndex < width);
-
-                       //use line to populate target
-                       if(scanlineIndex >= offset_y)
-                               for(int index = offset_x; index < target->width(); ++index)
-                               {
-                                       target_data[((scanlineIndex-offset_y)*target->width()+index-offset_x) * stride + offset] = line[index];
-                               }
-               }
-       }
-};
-
-layer::layer() : impl_(spl::make_shared<impl>()) {}
-
-void layer::populate(BEFileInputStream& stream, const psd_document& doc) { impl_->populate(stream, doc); }
-void layer::read_channel_data(BEFileInputStream& stream) { impl_->read_channel_data(stream); }
-
-const std::wstring& layer::name() const { return impl_->name_; }
-unsigned char layer::opacity() const { return impl_->opacity_; }
-unsigned short layer::sheet_color() const { return impl_->sheet_color_; }
-
-bool layer::is_visible() { return (impl_->flags_ & 2) == 0; }  //the (PSD file-format) documentation is is saying the opposite but what the heck
-bool layer::is_position_protected() { return (impl_->protection_flags_& 4) == 4; }
-
-float layer::text_scale() const { return impl_->text_scale_; }
-bool layer::is_text() const { return !impl_->text_layer_info_.empty(); }
-const boost::property_tree::wptree& layer::text_data() const { return impl_->text_layer_info_; }
-
-bool layer::has_timeline() const { return !impl_->timeline_info_.empty(); }
-const boost::property_tree::wptree& layer::timeline_data() const { return impl_->timeline_info_; }
-
-bool layer::is_solid() const { return impl_->solid_color_.alpha != 0; }
-color<unsigned char> layer::solid_color() const { return impl_->solid_color_; }
-
-const point<long>& layer::location() const { return impl_->bitmap_rect_.location; }
-const image8bit_ptr& layer::bitmap() const { return impl_->bitmap_; }
-
-int layer::link_group_id() const { return impl_->link_group_id_; }
-void layer::set_link_group_id(int id) { impl_->link_group_id_ = id; }
-
-}      //namespace psd
-}      //namespace caspar
\ No newline at end of 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: Niklas P Andersson, niklas.p.andersson@svt.se\r
+*/\r
+\r
+#include "layer.h"\r
+#include "psd_document.h"\r
+#include "descriptor.h"\r
+#include "util/pdf_reader.h"\r
+\r
+#include "../image/util/image_algorithms.h"\r
+#include "../image/util/image_view.h"\r
+\r
+#include <common/log.h>\r
+\r
+#include <boost/property_tree/ptree.hpp>\r
+#include <boost/algorithm/string.hpp>\r
+#include <boost/lexical_cast.hpp>\r
+\r
+#include <algorithm>\r
+\r
+namespace caspar { namespace psd {\r
+\r
+void layer::mask_info::read_mask_data(bigendian_file_input_stream& stream)\r
+{\r
+       auto length = stream.read_long();\r
+       switch(length)\r
+       {\r
+       case 0:\r
+               break;\r
+\r
+       case 20:\r
+       case 36:\r
+               rect_.location.y = stream.read_long();\r
+               rect_.location.x = stream.read_long();\r
+               rect_.size.height = stream.read_long() - rect_.location.y;\r
+               rect_.size.width = stream.read_long() - rect_.location.x;\r
+\r
+               default_value_ = stream.read_byte();\r
+               flags_ = stream.read_byte();\r
+               stream.discard_bytes(2);\r
+               \r
+               if(length == 36)\r
+               {\r
+                       //we save the information about the total mask in case the vector-mask has an unsupported shape\r
+                       total_mask_.reset(new layer::mask_info);\r
+                       total_mask_->flags_ = stream.read_byte();\r
+                       total_mask_->default_value_ = stream.read_byte();\r
+                       total_mask_->rect_.location.y = stream.read_long();\r
+                       total_mask_->rect_.location.x = stream.read_long();\r
+                       total_mask_->rect_.size.height = stream.read_long() - rect_.location.y;\r
+                       total_mask_->rect_.size.width = stream.read_long() - rect_.location.x;\r
+               }\r
+               break;\r
+\r
+       default:\r
+               //TODO: Log that we discard a mask that is not supported\r
+               stream.discard_bytes(length);\r
+               break;\r
+       };\r
+}\r
+\r
+bool layer::vector_mask_info::populate(int length, bigendian_file_input_stream& stream, int doc_width, int doc_height)\r
+{\r
+       std::vector<point<int>> knots;\r
+       bool smooth_curve = false;\r
+\r
+       stream.read_long(); // version\r
+       this->flags_ = static_cast<std::uint8_t>(stream.read_long()); // flags\r
+       int path_records = (length - 8) / 26;\r
+\r
+       auto position = stream.current_position();\r
+\r
+       const int SELECTOR_SIZE = 2;\r
+       const int PATH_POINT_SIZE = 4 + 4;\r
+       const int PATH_POINT_RECORD_SIZE = SELECTOR_SIZE + (3 * PATH_POINT_SIZE);\r
+\r
+       for (int i = 1; i <= path_records; ++i)\r
+       {\r
+               auto selector = stream.read_short();\r
+               if (selector == 2)      //we only concern ourselves with closed paths \r
+               {\r
+                       auto p_y = stream.read_long();\r
+                       auto p_x = stream.read_long();\r
+                       point<int> prev{ static_cast<int>(p_x), static_cast<int>(p_y) };\r
+\r
+                       auto a_y = stream.read_long();\r
+                       auto a_x = stream.read_long();\r
+                       point<int> anchor{ static_cast<int>(a_x), static_cast<int>(a_y) };\r
+\r
+                       auto n_y = stream.read_long();\r
+                       auto n_x = stream.read_long();\r
+                       point<int> next{ static_cast<int>(n_x), static_cast<int>(n_y) };\r
+\r
+                       if (anchor == prev && anchor == next)\r
+                               knots.push_back(anchor);\r
+                       else \r
+                       {\r
+                               //note that we've got a smooth curve, but continue to iterate through the data\r
+                               smooth_curve = true;\r
+                       }\r
+               }\r
+\r
+               auto offset = PATH_POINT_RECORD_SIZE * i;\r
+               stream.set_position(position + offset);\r
+       }\r
+\r
+       if (smooth_curve || knots.size() != 4)  //we can't handle smooth-curves yet and we only support quad-gons\r
+       {\r
+               rect_.clear();\r
+               flags_ = static_cast<std::uint8_t>(flags::unsupported | flags::disabled);\r
+               return false;\r
+       }\r
+\r
+       //the path_points are given in fixed-point 8.24 as a ratio with regards to the width/height of the document. we need to divide by 16777215.0f to get the real ratio.\r
+       float x_ratio = doc_width / 16777215.0f;\r
+       float y_ratio = doc_height / 16777215.0f;\r
+       rect_.clear();\r
+       knots_.clear();\r
+\r
+       //is it an orthogonal rectangle\r
+       if (knots[0].x == knots[3].x && knots[1].x == knots[2].x && knots[0].y == knots[1].y && knots[2].y == knots[3].y)\r
+       {\r
+               rect_.location.x = static_cast<int>(knots[0].x * x_ratio + 0.5f);                                               //add .5 to get propper rounding when converting to integer\r
+               rect_.location.y = static_cast<int>(knots[0].y * y_ratio + 0.5f);                                               //add .5 to get propper rounding when converting to integer\r
+               rect_.size.width = static_cast<int>(knots[1].x * x_ratio + 0.5f) - rect_.location.x;    //add .5 to get propper rounding when converting to integer\r
+               rect_.size.height = static_cast<int>(knots[2].y * y_ratio + 0.5f) - rect_.location.y;   //add .5 to get propper rounding when converting to integer\r
+       }\r
+       else //it's could be any kind of quad-gon\r
+       {\r
+               for (auto& k : knots)\r
+                       knots_.push_back(psd::point<int>{static_cast<int>(k.x * x_ratio + 0.5f), static_cast<int>(k.y * y_ratio + 0.5f)});\r
+       }\r
+\r
+       return true;\r
+}\r
+\r
+struct layer::impl\r
+{\r
+       friend class layer;\r
+\r
+       impl() : blend_mode_(caspar::core::blend_mode::normal), layer_type_(layer_type::content), link_group_id_(0), opacity_(255), sheet_color_(0), baseClipping_(false), flags_(0), protection_flags_(0), masks_count_(0), scale_{ 1.0, 1.0 }, angle_(0), shear_(0), tags_(layer_tag::none)\r
+       {}\r
+\r
+private:\r
+       std::vector<channel>                    channels_;\r
+       caspar::core::blend_mode                blend_mode_;\r
+       layer_type                                              layer_type_;\r
+       int                                                             link_group_id_;\r
+       int                                                             opacity_;\r
+       int                                                             sheet_color_;\r
+       bool                                                    baseClipping_;\r
+       std::uint8_t                                    flags_;\r
+       std::uint32_t                                   protection_flags_;\r
+       std::wstring                                    name_;\r
+       int                                                             masks_count_;\r
+       psd::point<double>                              text_pos_;\r
+       psd::point<double>                              scale_;\r
+       double                                                  angle_;\r
+       double                                                  shear_;\r
+\r
+       layer::mask_info                                mask_;\r
+\r
+       rect<int>                                               bitmap_rect_;\r
+       image8bit_ptr                                   bitmap_;\r
+\r
+       boost::property_tree::wptree    text_layer_info_;\r
+       boost::property_tree::wptree    timeline_info_;\r
+\r
+       color<std::uint8_t>                             solid_color_;\r
+\r
+       layer_tag                                               tags_;\r
+\r
+public:\r
+       void populate(bigendian_file_input_stream& stream, const psd_document& doc)\r
+       {\r
+               bitmap_rect_.location.y = stream.read_long();\r
+               bitmap_rect_.location.x = stream.read_long();\r
+               bitmap_rect_.size.height = stream.read_long() - bitmap_rect_.location.y;\r
+               bitmap_rect_.size.width = stream.read_long() - bitmap_rect_.location.x;\r
+\r
+               //Get info about the channels in the layer\r
+               auto channelCount = stream.read_short();\r
+               for(int channelIndex = 0; channelIndex < channelCount; ++channelIndex)\r
+               {\r
+                       auto id = static_cast<std::int16_t>(stream.read_short());\r
+                       channel c(id, stream.read_long());\r
+\r
+                       if(c.id < -1)\r
+                               masks_count_++;\r
+\r
+                       channels_.push_back(c);\r
+               }\r
+\r
+               auto blendModeSignature = stream.read_long();\r
+               if(blendModeSignature != '8BIM')\r
+                       CASPAR_THROW_EXCEPTION(psd_file_format_exception() << msg_info("blendModeSignature != '8BIM'"));\r
+\r
+               blend_mode_ = int_to_blend_mode(stream.read_long());\r
+               opacity_ = stream.read_byte();\r
+               baseClipping_ = stream.read_byte() == 1 ? false : true;\r
+               flags_ = stream.read_byte();\r
+\r
+               stream.discard_bytes(1);        //padding\r
+\r
+               auto extras_size = stream.read_long();\r
+               auto position = stream.current_position();\r
+               mask_.read_mask_data(stream);\r
+               read_blending_ranges(stream);\r
+\r
+               stream.read_pascal_string(4);   //throw this away. We'll read the unicode version of the name later\r
+\r
+               //Aditional Layer Information\r
+               auto end_of_layer_info = position + extras_size;\r
+               try\r
+               {\r
+                       while(stream.current_position() < end_of_layer_info)\r
+                       {\r
+                               read_chunk(stream, doc);\r
+                       }\r
+               }\r
+               catch(psd_file_format_exception&)\r
+               {\r
+                       stream.set_position(end_of_layer_info);\r
+               }\r
+       }\r
+\r
+       void read_chunk(bigendian_file_input_stream& stream, const psd_document& doc, bool isMetadata = false)\r
+       {\r
+               auto signature = stream.read_long();\r
+               if(signature != '8BIM' && signature != '8B64')\r
+                       CASPAR_THROW_EXCEPTION(psd_file_format_exception() << msg_info("signature != '8BIM' && signature != '8B64'"));\r
+\r
+               auto key = stream.read_long();\r
+\r
+               if(isMetadata) stream.read_long();\r
+\r
+               auto length = stream.read_long();\r
+               auto end_of_chunk = stream.current_position() + length;\r
+\r
+               try\r
+               {\r
+                       switch(key)\r
+                       {\r
+                       case 'SoCo':\r
+                               read_solid_color(stream);\r
+                               break;\r
+\r
+                       case 'lsct':    //group settings (folders)\r
+                               read_group_settings(stream, length);\r
+\r
+                       case 'lspf':    //protection settings\r
+                               protection_flags_ = stream.read_long();\r
+                               break;\r
+\r
+                       case 'Txt2':    //text engine data\r
+                               break;\r
+\r
+                       case 'luni':\r
+                               set_name_and_tags(stream.read_unicode_string());\r
+                               break;\r
+\r
+                       case 'TySh':    //type tool object settings\r
+                               read_text_data(stream);\r
+                               break;\r
+                               \r
+                       case 'shmd':    //metadata\r
+                               read_metadata(stream, doc);\r
+                               break;\r
+\r
+                       case 'lclr':\r
+                               sheet_color_ = static_cast<std::int16_t>(stream.read_short());\r
+                               break;\r
+                               \r
+                       case 'lyvr':    //layer version\r
+                               break;\r
+\r
+                       case 'lnkD':    //linked layer\r
+                       case 'lnk2':    //linked layer\r
+                       case 'lnk3':    //linked layer\r
+                               break;\r
+\r
+                       case 'vsms':\r
+                       case 'vmsk':\r
+                               mask_.read_vector_mask_data(length, stream, doc.width(), doc.height());\r
+                               break;\r
+                               \r
+                       case 'tmln':\r
+                               read_timeline_data(stream);\r
+                               break;\r
+\r
+                       default:\r
+                               break;\r
+                       }\r
+               }\r
+               catch(psd_file_format_exception& ex)\r
+               {\r
+                       //ignore failed chunks silently\r
+                       CASPAR_LOG(warning) << ex.what();\r
+               }\r
+\r
+               stream.set_position(end_of_chunk);\r
+       }\r
+         \r
+       void set_name_and_tags(const std::wstring& name) {\r
+               auto start_bracket = name.find_first_of(L'[');\r
+               auto end_bracket = name.find_first_of(L']');\r
+               if (start_bracket == std::wstring::npos && end_bracket == std::wstring::npos) {\r
+                       //no flags\r
+                       name_ = name;\r
+               }\r
+               else if (start_bracket != std::wstring::npos && end_bracket > start_bracket) {\r
+                       //we have tags\r
+                       tags_ = string_to_layer_tags(name.substr(start_bracket+1, end_bracket-start_bracket-1));\r
+                       name_ = name.substr(end_bracket+1);\r
+               }\r
+               else {\r
+                       //missmatch\r
+                       name_ = name;\r
+                       CASPAR_LOG(warning) << "Mismatching tag-brackets in layer name";\r
+               }\r
+\r
+               boost::trim(name_);\r
+       }\r
+\r
+       void read_solid_color(bigendian_file_input_stream& stream)\r
+       {\r
+               if(stream.read_long() != 16)    //"descriptor version" should be 16\r
+                       CASPAR_THROW_EXCEPTION(psd_file_format_exception() << msg_info("descriptor version should be 16"));\r
+\r
+               descriptor solid_descriptor(L"solid_color");\r
+               solid_descriptor.populate(stream);\r
+               solid_color_.red = static_cast<std::uint8_t>(solid_descriptor.items().get(L"Clr .Rd  ", 0.0) + 0.5);\r
+               solid_color_.green = static_cast<std::uint8_t>(solid_descriptor.items().get(L"Clr .Grn ", 0.0) + 0.5);\r
+               solid_color_.blue = static_cast<std::uint8_t>(solid_descriptor.items().get(L"Clr .Bl  ", 0.0) + 0.5);\r
+               solid_color_.alpha = 255;\r
+       }\r
+\r
+       void read_group_settings(bigendian_file_input_stream& stream, unsigned int length) \r
+       {\r
+               auto type = stream.read_long();\r
+               unsigned int sub_type = 0;\r
+\r
+               if (length >= 12)\r
+               {\r
+                       auto signature = stream.read_long();\r
+                       if (signature != '8BIM')\r
+                               CASPAR_THROW_EXCEPTION(psd_file_format_exception() << msg_info("signature != '8BIM'"));\r
+\r
+                       blend_mode_ = int_to_blend_mode(stream.read_long());\r
+                       \r
+                       if (length >= 16)\r
+                               sub_type = stream.read_long();\r
+               }\r
+\r
+               layer_type_ = int_to_layer_type(type, sub_type);\r
+       }\r
+\r
+       void read_metadata(bigendian_file_input_stream& stream, const psd_document& doc)\r
+       {\r
+               int count = stream.read_long();\r
+               for(int index = 0; index < count; ++index)\r
+                       read_chunk(stream, doc, true);\r
+       }\r
+\r
+       void read_timeline_data(bigendian_file_input_stream& stream)\r
+       {\r
+               if(stream.read_long() != 16)    //"descriptor version" should be 16\r
+                       CASPAR_THROW_EXCEPTION(psd_file_format_exception() << msg_info("descriptor version should be 16"));\r
+\r
+               descriptor timeline_descriptor(L"timeline");\r
+               timeline_descriptor.populate(stream);\r
+               timeline_info_.swap(timeline_descriptor.items());\r
+       }\r
+\r
+       void read_text_data(bigendian_file_input_stream& stream)\r
+       {\r
+               std::wstring text;      //the text in the layer\r
+\r
+               stream.read_short();    //should be 1\r
+       \r
+               //transformation info\r
+               auto xx = stream.read_double();\r
+               auto xy = stream.read_double();\r
+               auto yx = stream.read_double();\r
+               auto yy = stream.read_double();\r
+               auto tx = stream.read_double(); // tx\r
+               auto ty = stream.read_double(); // ty\r
+\r
+               text_pos_.x = tx;\r
+               text_pos_.y = ty;\r
+\r
+               if(stream.read_short() != 50)   //"text version" should be 50\r
+                       CASPAR_THROW_EXCEPTION(psd_file_format_exception() << msg_info("invalid text version"));\r
+\r
+               if(stream.read_long() != 16)    //"descriptor version" should be 16\r
+                       CASPAR_THROW_EXCEPTION(psd_file_format_exception() << msg_info("Invalid descriptor version while reading text-data"));\r
+\r
+               descriptor text_descriptor(L"text");\r
+               text_descriptor.populate(stream);\r
+               auto text_info = text_descriptor.items().get_optional<std::wstring>(L"EngineData");\r
+               \r
+               if (text_info)\r
+               {\r
+                       std::string str(text_info->begin(), text_info->end());\r
+                       read_pdf(text_layer_info_, str);\r
+                       log::print_child(boost::log::trivial::trace, L"", L"text_layer_info", text_layer_info_);\r
+               }\r
+\r
+               if(stream.read_short() != 1)    //"warp version" should be 1\r
+                       CASPAR_THROW_EXCEPTION(psd_file_format_exception() << msg_info("invalid warp version"));\r
+\r
+               if(stream.read_long() != 16)    //"descriptor version" should be 16\r
+                       CASPAR_THROW_EXCEPTION(psd_file_format_exception() << msg_info("Invalid descriptor version while reading text warp-data"));\r
+\r
+               descriptor warp_descriptor(L"warp");\r
+               warp_descriptor.populate(stream);\r
+               stream.read_double(); // w_left\r
+               stream.read_double();  // w_top\r
+               stream.read_double();  // w_right\r
+               stream.read_double();  // w_bottom\r
+\r
+\r
+               //extract scale, angle and shear factor from transformation matrix \r
+               const double PI = 3.141592653589793;\r
+               auto angle = atan2(xy, xx);\r
+\r
+               auto c = cos(angle);\r
+               auto s = sin(angle);\r
+               auto scale_x = (abs(c) > 0.1) ? xx / c : xy / s;\r
+\r
+               if (xx / scale_x < 0) { //fel kvadrant\r
+                       angle += PI;\r
+                       c = cos(angle);\r
+                       s = sin(angle);\r
+                       scale_x = (abs(c) > 0.1) ? xx / c : xy / s;\r
+               }\r
+\r
+               auto shear_factor = (yx*c + yy*s) / (yy*c - yx * s);\r
+               auto scale_y = 1.0;\r
+               if (abs(shear_factor) < 0.0001 || std::isnan(shear_factor)) {\r
+                       shear_factor = 0;\r
+                       scale_y = (abs(c) > 0.1) ? yy / c : yx / -s;\r
+               }\r
+               else {\r
+                       scale_y = yx / (c*shear_factor - s);\r
+               }\r
+\r
+               scale_.x = scale_x;\r
+               scale_.y = scale_y;\r
+               angle_ = angle * 180 / PI;\r
+               shear_ = shear_factor;\r
+\r
+               \r
+\r
+\r
+       }\r
+\r
+       //TODO: implement\r
+       void read_blending_ranges(bigendian_file_input_stream& stream)\r
+       {\r
+               auto length = stream.read_long();\r
+               stream.discard_bytes(length);\r
+       }\r
+\r
+       bool has_channel(channel_type type)\r
+       {\r
+               return std::find_if(channels_.begin(), channels_.end(), [=](const channel& c) { return c.id == static_cast<int>(type); }) != channels_.end();\r
+       }\r
+\r
+       void read_channel_data(bigendian_file_input_stream& stream)\r
+       {\r
+               image8bit_ptr bitmap;\r
+\r
+               bool has_transparency = has_channel(channel_type::transparency);\r
+       \r
+               if(!bitmap_rect_.empty())\r
+               {\r
+                       bitmap = std::make_shared<image8bit>(bitmap_rect_.size.width, bitmap_rect_.size.height, 4);\r
+                       if(!has_transparency)\r
+                               std::memset(bitmap->data(), 255, bitmap->width()*bitmap->height()*bitmap->channel_count());\r
+               }\r
+\r
+               for(auto it = channels_.begin(); it != channels_.end(); ++it)\r
+               {\r
+                       auto channel = (*it);\r
+                       image8bit_ptr target;\r
+                       int offset = 0;\r
+                       bool discard_channel = false;\r
+\r
+                       //determine target bitmap and offset\r
+                       if(channel.id >= 3)\r
+                               discard_channel = true; //discard channels that doesn't contribute to the final image\r
+                       else if(channel.id >= -1)       //BGRA-data\r
+                       {\r
+                               target = bitmap;\r
+                               offset = (channel.id >= 0) ? 2 - channel.id : 3;\r
+                       }\r
+                       else    //mask\r
+                       {\r
+                               offset = 0;\r
+                               if (channel.id == -2)\r
+                               {\r
+                                       mask_.create_bitmap();\r
+                                       target = mask_.bitmap_;\r
+                               }\r
+                               else if (channel.id == -3)      //total_mask\r
+                               {\r
+                                       mask_.total_mask_->create_bitmap();\r
+                                       target = mask_.total_mask_->bitmap_;\r
+                                       offset = 0;\r
+                               }\r
+                       }\r
+\r
+                       if(!target)\r
+                               discard_channel = true;\r
+\r
+                       auto end_of_data = stream.current_position() + channel.data_length;\r
+                       if(!discard_channel)\r
+                       {\r
+                               auto encoding = stream.read_short();\r
+                               if(target)\r
+                               {\r
+                                       if(encoding == 0)\r
+                                               read_raw_image_data(stream, channel.data_length-2, target, offset);\r
+                                       else if(encoding == 1)\r
+                                               read_rle_image_data(stream, target, offset);\r
+                                       else\r
+                                               CASPAR_THROW_EXCEPTION(psd_file_format_exception() << msg_info("Unhandled image data encoding: " + boost::lexical_cast<std::string>(encoding)));\r
+                               }\r
+                       }\r
+                       stream.set_position(end_of_data);\r
+               }\r
+\r
+               if(bitmap && has_transparency)\r
+               {\r
+                       caspar::image::image_view<caspar::image::bgra_pixel> view(bitmap->data(), bitmap->width(), bitmap->height());\r
+                       caspar::image::premultiply(view);\r
+               }\r
+\r
+               bitmap_ = bitmap;\r
+       }\r
+\r
+       void read_raw_image_data(bigendian_file_input_stream& stream, int data_length, image8bit_ptr target, int offset)\r
+       {\r
+               auto total_length = target->width() * target->height();\r
+               if (total_length != data_length)\r
+                       CASPAR_THROW_EXCEPTION(psd_file_format_exception() << msg_info("total_length != data_length"));\r
+\r
+               auto data = target->data();\r
+               auto stride = target->channel_count();\r
+\r
+               if (stride == 1)\r
+                       stream.read(reinterpret_cast<char*>(data + offset), total_length);\r
+               else\r
+               {\r
+                       for(int index = 0; index < total_length; ++index)\r
+                               data[index * stride + offset] = stream.read_byte();\r
+               }\r
+       }\r
+\r
+       void read_rle_image_data(bigendian_file_input_stream& stream, image8bit_ptr target, int offset)\r
+       {\r
+               auto width = target->width();\r
+               auto height = target->height();\r
+               auto stride = target->channel_count();\r
+\r
+               std::vector<int> scanline_lengths;\r
+               scanline_lengths.reserve(height);\r
+\r
+               for (int scanlineIndex = 0; scanlineIndex < height; ++scanlineIndex)\r
+                       scanline_lengths.push_back(static_cast<std::int16_t>(stream.read_short()));\r
+\r
+               auto target_data = target->data();\r
+\r
+               std::vector<std::uint8_t> line(width);\r
+\r
+               for(int scanlineIndex=0; scanlineIndex < height; ++scanlineIndex)\r
+               {\r
+                       int colIndex = 0;\r
+\r
+                       do\r
+                       {\r
+                               int length = 0;\r
+\r
+                               //Get controlbyte\r
+                               char controlByte = static_cast<char>(stream.read_byte());\r
+                               if(controlByte >= 0)\r
+                               {\r
+                                       //Read uncompressed string\r
+                                       length = controlByte+1;\r
+                                       for(int index=0; index < length; ++index)\r
+                                               line[colIndex+index] = stream.read_byte();\r
+                               }\r
+                               else if(controlByte > -128)\r
+                               {\r
+                                       //Repeat next byte\r
+                                       length = -controlByte+1;\r
+                                       auto value = stream.read_byte();\r
+                                       for(int index=0; index < length; ++index)\r
+                                               line[colIndex+index] = value;\r
+                               }\r
+\r
+                               colIndex += length;\r
+                       }\r
+                       while(colIndex < width);\r
+\r
+                       //use line to populate target\r
+                       for(int index = 0; index < width; ++index)\r
+                       {\r
+                               target_data[(scanlineIndex*width+index) * stride + offset] = line[index];\r
+                       }\r
+               }\r
+       }\r
+};\r
+\r
+layer::layer() : impl_(spl::make_shared<impl>()) {}\r
+\r
+void layer::populate(bigendian_file_input_stream& stream, const psd_document& doc) { impl_->populate(stream, doc); }\r
+void layer::read_channel_data(bigendian_file_input_stream& stream) { impl_->read_channel_data(stream); }\r
+\r
+const std::wstring& layer::name() const { return impl_->name_; }\r
+int layer::opacity() const { return impl_->opacity_; }\r
+caspar::core::blend_mode layer::blend_mode() const { return impl_->blend_mode_; }\r
+int layer::sheet_color() const { return impl_->sheet_color_; }\r
+\r
+bool layer::is_visible() { return (impl_->flags_ & 2) == 0; }  //the (PSD file-format) documentation is is saying the opposite but what the heck\r
+bool layer::is_position_protected() { return (impl_->protection_flags_& 4) == 4; }\r
+\r
+const layer::mask_info& layer::mask() const { return impl_->mask_; }\r
+\r
+const psd::point<double>& layer::text_pos() const { return impl_->text_pos_; }\r
+const psd::point<double>& layer::scale() const { return impl_->scale_; }\r
+const double layer::angle() const { return impl_->angle_; }\r
+const double layer::shear() const { return impl_->shear_; }\r
+\r
+bool layer::is_text() const { return !impl_->text_layer_info_.empty(); }\r
+const boost::property_tree::wptree& layer::text_data() const { return impl_->text_layer_info_; }\r
+\r
+bool layer::has_timeline() const { return !impl_->timeline_info_.empty(); }\r
+const boost::property_tree::wptree& layer::timeline_data() const { return impl_->timeline_info_; }\r
+\r
+bool layer::is_solid() const { return impl_->solid_color_.alpha != 0; }\r
+color<std::uint8_t> layer::solid_color() const { return impl_->solid_color_; }\r
+\r
+const point<int>& layer::location() const { return impl_->bitmap_rect_.location; }\r
+const size<int>& layer::size() const { return impl_->bitmap_rect_.size; }\r
+const image8bit_ptr& layer::bitmap() const { return impl_->bitmap_; }\r
+\r
+layer_type layer::group_mode() const { return impl_->layer_type_; }\r
+int layer::link_group_id() const { return impl_->link_group_id_; }\r
+void layer::set_link_group_id(int id) { impl_->link_group_id_ = id; }\r
+\r
+bool layer::is_explicit_dynamic() const { return (impl_->tags_ & layer_tag::explicit_dynamic) == layer_tag::explicit_dynamic; }\r
+bool layer::is_static() const { return (impl_->tags_ & layer_tag::rasterized) == layer_tag::rasterized; }\r
+bool layer::is_movable() const { return (impl_->tags_ & layer_tag::moveable) == layer_tag::moveable; }\r
+bool layer::is_resizable() const { return (impl_->tags_ & layer_tag::resizable) == layer_tag::resizable; }\r
+bool layer::is_placeholder() const { return (impl_->tags_ & layer_tag::placeholder) == layer_tag::placeholder; }\r
+bool layer::is_cornerpin() const { return (impl_->tags_ & layer_tag::cornerpin) == layer_tag::cornerpin; }\r
+\r
+layer_tag layer::tags() const { return impl_->tags_; }\r
+\r
+}      //namespace psd\r
+}      //namespace caspar\r