lib/psd/layer.rb in psd-1.3.2 vs lib/psd/layer.rb in psd-1.3.3

- old
+ new

@@ -1,354 +1,76 @@ class PSD # Represents a single layer and all of the data associated with # that layer. class Layer include Section + include BlendModes + include BlendingRanges + include ChannelImage + include Exporting + include Helpers + include Info + include Mask + include Name + include PathComponents + include PositionAndChannels - attr_reader :id, :mask, :blending_ranges, :adjustments, :channels_info - attr_reader :blend_mode, :layer_type, :blending_mode, :opacity, :fill_opacity - attr_reader :channels, :image + attr_reader :id, :info_keys + attr_accessor :group_layer, :node, :file - attr_accessor :group_layer - attr_accessor :top, :left, :bottom, :right, :rows, :cols, :ref_x, :ref_y, :node, :file - - alias :info :adjustments - alias :width :cols - alias :height :rows - - # All of the extra layer info sections that we know how to parse. - LAYER_INFO = { - type: TypeTool, - legacy_type: LegacyTypeTool, - metadata: MetadataSetting, - layer_name_source: LayerNameSource, - object_effects: ObjectEffects, - name: UnicodeName, - section_divider: LayerSectionDivider, - reference_point: ReferencePoint, - layer_id: LayerID, - fill_opacity: FillOpacity, - placed_layer: PlacedLayer, - vector_mask: VectorMask - } - # Initializes all of the defaults for the layer. def initialize(file) @file = file - @image = nil + @mask = {} @blending_ranges = {} @adjustments = {} @channels_info = [] @blend_mode = {} @group_layer = nil - @layer_type = 'normal' @blending_mode = 'normal' @opacity = 255 - @fill_opacity = 255 # Just used for tracking which layer adjustments we're parsing. # Not essential. @info_keys = [] end # Parse the layer and all of it's sub-sections. def parse(index=nil) start_section - @idx = index + @id = index - parse_info + parse_position_and_channels parse_blend_modes extra_len = @file.read_int @layer_end = @file.tell + extra_len parse_mask_data parse_blending_ranges parse_legacy_layer_name - parse_extra_data + parse_layer_info PSD.logger.debug "Layer name = #{name}" @file.seek @layer_end # Skip over any filler zeros end_section return self end - # Export the layer to file. May or may not work. - def export(outfile) - export_info(outfile) - - @blend_mode.write(outfile) - @file.seek(@blend_mode.num_bytes, IO::SEEK_CUR) - - export_mask_data(outfile) - export_blending_ranges(outfile) - export_legacy_layer_name(outfile) - export_extra_data(outfile) - - outfile.write @file.read(end_of_section - @file.tell) - end - # We just delegate this to a normal method call. def [](val) self.send(val) end - def parse_channel_image!(header, parse) - @image = ChannelImage.new(@file, header, self) - parse ? @image.parse : @image.skip - end - - # Does this layer represent the start of a folder section? - def folder? - return false unless @adjustments.has_key?(:section_divider) - @adjustments[:section_divider].is_folder - end - - # Does this layer represent the end of a folder section? - def folder_end? - return false unless @adjustments.has_key?(:section_divider) - @adjustments[:section_divider].is_hidden - end - - # Is this layer visible? - def visible? - @visible - end - - # Is this layer hidden? - def hidden? - !@visible - end - - # Attempt to translate this layer and modify the document. - def translate(x=0, y=0) - @left += x - @right += x - @top += y - @bottom += y - - @path_components.each{ |p| p.translate(x,y) } if @path_components - end - - # Attempt to scale the path components within this layer. - def scale_path_components(xr, yr) - return unless @path_components - - @path_components.each{ |p| p.scale(xr, yr) } - end - - # Helper that exports the text data in this layer, if any. - def text - return nil unless @adjustments[:type] - @adjustments[:type].to_hash - end - - # Gets the name of this layer. If the PSD file is from an even remotely - # recent version of Photoshop, this data is stored as extra layer info and - # as a UTF-16 name. Otherwise, it's stored in a legacy block. - def name - if @adjustments.has_key?(:name) - return @adjustments[:name].data - end - - return @legacy_name - end - # We delegate all missing method calls to the extra layer info to make it easier # to access that data. def method_missing(method, *args, &block) return @adjustments[method] if @adjustments.has_key?(method) super - end - - private - - def parse_info - start_section(:info) - - @top = @file.read_int - @left = @file.read_int - @bottom = @file.read_int - @right = @file.read_int - @channels = @file.read_short - - @rows = @bottom - @top - @cols = @right - @left - - @channels.times do - channel_id = @file.read_short - channel_length = @file.read_int - - @channels_info << {id: channel_id, length: channel_length} - end - - end_section(:info) - end - - def export_info(outfile) - [@top, @left, @bottom, @right].each { |val| outfile.write_int(val) } - outfile.write_short(@channels) - - @channels_info.each do |channel_info| - outfile.write_short channel_info[:id] - outfile.write_int channel_info[:length] - end - - @file.seek end_of_section(:info) - end - - def export_mask_data(outfile) - outfile.write @file.read(@mask_end - @mask_begin + 4) - end - - def export_blending_ranges(outfile) - length = 4 * 2 # greys - length += @blending_ranges[:num_channels] * 8 - outfile.write_int length - - outfile.write_short @blending_ranges[:grey][:source][:black] - outfile.write_short @blending_ranges[:grey][:source][:white] - outfile.write_short @blending_ranges[:grey][:dest][:black] - outfile.write_short @blending_ranges[:grey][:dest][:white] - - @blending_ranges[:num_channels].times do |i| - outfile.write_short @blending_ranges[:channels][i][:source][:black] - outfile.write_short @blending_ranges[:channels][i][:source][:white] - outfile.write_short @blending_ranges[:channels][i][:dest][:black] - outfile.write_short @blending_ranges[:channels][i][:dest][:white] - end - - @file.seek length + 4, IO::SEEK_CUR - end - - def export_legacy_layer_name(outfile) - outfile.write @file.read(@legacy_name_end - @legacy_name_start) - end - - def export_extra_data(outfile) - outfile.write @file.read(@extra_data_end - @extra_data_begin) - if @path_components && !@path_components.empty? - outfile.seek @vector_mask_begin - @file.seek @vector_mask_begin - - write_vector_mask(outfile) - @file.seek outfile.tell - end - end - - def parse_blend_modes - @blend_mode = BlendMode.read(@file) - - @blending_mode = @blend_mode.mode - @opacity = @blend_mode.opacity - @visible = @blend_mode.visible - end - - def parse_mask_data - @mask_begin = @file.tell - @mask = Mask.read(@file) - @mask_end = @file.tell - end - - def parse_blending_ranges - length = @file.read_int - - @blending_ranges[:grey] = { - source: { - black: @file.read_short, - white: @file.read_short - }, - dest: { - black: @file.read_short, - white: @file.read_short - } - } - - @blending_ranges[:num_channels] = (length - 8) / 8 - - @blending_ranges[:channels] = [] - @blending_ranges[:num_channels].times do - @blending_ranges[:channels] << { - source: { - black: @file.read_short, - white: @file.read_short - }, - dest: { - black: @file.read_short, - white: @file.read_short - } - } - end - end - - # The old school layer names are encoded in MacRoman format, - # not UTF-8. Luckily Ruby kicks ass at character conversion. - def parse_legacy_layer_name - @legacy_name_start = @file.tell - - len = Util.pad4 @file.read(1).bytes.to_a[0] - @legacy_name = @file.read_string(len) - - @legacy_name_end = @file.tell - end - - # This section is a bit tricky to parse because it represents all of the - # extra data that describes this layer. - def parse_extra_data - @extra_data_begin = @file.tell - - while @file.tell < @layer_end - # Signature, don't need - @file.seek 4, IO::SEEK_CUR - - # Key, very important - key = @file.read_string(4) - @info_keys << key - - length = Util.pad2 @file.read_int - pos = @file.tell - - info_parsed = false - LAYER_INFO.each do |name, info| - next unless info.key == key - - PSD.logger.debug "Layer Info: key = #{key}, start = #{pos}, length = #{length}" - - begin - i = info.new(@file, length) - i.parse - - @adjustments[name] = i - info_parsed = true - rescue Exception => e - PSD.logger.error "Parsing error: key = #{key}, message = #{e.message}" - PSD.logger.error e.backtrace.join("\n") - end - - break - end - - if !info_parsed - PSD.logger.debug "Skipping: key = #{key}, pos = #{@file.tell}, length = #{length}" - @file.seek pos + length - end - - @file.seek pos + length if @file.tell != (pos + length) - end - - @extra_data_end = @file.tell - end - - def write_vector_mask(outfile) - outfile.write @file.read(8) - # outfile.write_int 3 - # outfile.write_int @vector_tag - - @path_components.each{ |pc| pc.write(outfile); @file.seek(26, IO::SEEK_CUR) } end end end \ No newline at end of file