lib/origami/filters/predictors.rb



=begin

    This file is part of Origami, PDF manipulation framework for Ruby
    Copyright (C) 2016	Guillaume Delugré.

    Origami is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Origami 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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with Origami.  If not, see <http://www.gnu.org/licenses/>.

=end

module Origami

    module Filter

        class PredictorError < Error #:nodoc:
        end

        module Predictor

            NONE = 1
            TIFF = 2
            PNG_NONE = 10
            PNG_SUB = 11
            PNG_UP = 12
            PNG_AVERAGE = 13
            PNG_PAETH = 14
            PNG_OPTIMUM = 15

            class DecodeParms < Dictionary
                include StandardObject

                field   :Predictor,         :Type => Integer, :Default => 1
                field   :Colors,            :Type => Integer, :Default => 1
                field   :BitsPerComponent,  :Type => Integer, :Default => 8
                field   :Columns,           :Type => Integer, :Default => 1
            end

            def self.included(receiver)
                raise TypeError, "Predictors only applies to Filters" unless receiver.include?(Filter)
            end

            #
            # Create a new predictive Filter.
            # _parameters_:: A hash of filter options.
            #
            def initialize(parameters = {})
                super(DecodeParms.new(parameters))
            end

            private

            def pre_prediction(data)
                return data unless @params.Predictor.is_a?(Integer)

                apply_pre_prediction(data, **prediction_parameters)
            end

            def post_prediction(data)
                return data unless @params.Predictor.is_a?(Integer)

                apply_post_prediction(data, **prediction_parameters)
            end

            def prediction_parameters
                {
                    predictor:  @params.Predictor.to_i,
                    colors:     @params.Colors.is_a?(Integer) ? @params.Colors.to_i : 1,
                    bpc:        @params.BitsPerComponent.is_a?(Integer) ? @params.BitsPerComponent.to_i : 8,
                    columns:    @params.Columns.is_a?(Integer) ? @params.Columns.to_i : 1,
                }
            end

            def apply_pre_prediction(data, predictor: NONE, colors: 1, bpc: 8, columns: 1)
                return data if data.empty? or predictor == NONE

                bpp, bpr = compute_bpp_bpr(data, columns, colors, bpc)

                unless data.size % bpr == 0
                    raise PredictorError.new("Invalid data size #{data.size}, should be multiple of bpr=#{bpr}",
                                             input_data: data)
                end

                if predictor == TIFF
                    tiff_pre_prediction(data, colors, bpc, columns)
                elsif predictor >= 10 # PNG
                    png_pre_prediction(data, predictor, bpp, bpr)
                else
                    raise PredictorError.new("Unknown predictor : #{predictor}", input_data: data)
                end
            end

            def apply_post_prediction(data, predictor: NONE, colors: 1, bpc: 8, columns: 1)
                return data if data.empty? or predictor == NONE

                bpp, bpr = compute_bpp_bpr(data, columns, colors, bpc)

                if predictor == TIFF
                    tiff_post_prediction(data, colors, bpc, columns)
                elsif predictor >= 10 # PNG
                    # Each line has an extra predictor byte.
                    png_post_prediction(data, bpp, bpr + 1)
                else
                    raise PredictorError.new("Unknown predictor : #{predictor}", input_data: data)
                end
            end

            #
            # Computes the number of bytes per pixel and number of bytes per row.
            #
            def compute_bpp_bpr(data, columns, colors, bpc)
                unless colors.between?(1, 4)
                    raise PredictorError.new("Colors must be between 1 and 4", input_data: data)
                end

                unless [1,2,4,8,16].include?(bpc)
                    raise PredictorError.new("BitsPerComponent must be in 1, 2, 4, 8 or 16", input_data: data)
                end

                # components per line
                nvals = columns * colors

                # bytes per pixel
                bpp = (colors * bpc + 7) >> 3

                # bytes per row
                bpr = (nvals * bpc + 7) >> 3

                [ bpp, bpr ]
            end

            #
            # Decodes the PNG input data.
            # Each line should be prepended by a byte identifying a PNG predictor.
            #
            def png_post_prediction(data, bpp, bpr)
                result = ::String.new
                uprow = "\0" * bpr
                thisrow = "\0" * bpr
                nrows = (data.size + bpr - 1) / bpr

                nrows.times do |irow|
                    line = data[irow * bpr, bpr]
                    predictor = 10 + line[0].ord
                    line[0] = "\0"

                    for i in (1..line.size-1)
                        up = uprow[i].ord

                        if bpp > i
                            left = upleft = 0
                        else
                            left = line[i - bpp].ord
                            upleft = uprow[i - bpp].ord
                        end

                        begin
                            thisrow[i] = png_apply_prediction(predictor, line[i].ord, up, left, upleft, &:+)
                        rescue PredictorError => error
                            thisrow[i] = line[i] if Origami::OPTIONS[:ignore_png_errors]

                            error.input_data = data
                            error.decoded_data = result
                            raise(error)
                        end
                    end

                    result << thisrow[1..-1]
                    uprow = thisrow
                end

                result
            end

            #
            # Encodes the input data given a PNG predictor.
            #
            def png_pre_prediction(data, predictor, bpp, bpr)
                result = ::String.new
                nrows = data.size / bpr

                line = "\0" + data[-bpr, bpr]

                (nrows - 1).downto(0) do |irow|
                    uprow =
                    if irow == 0
                        "\0" * (bpr + 1)
                    else
                        "\0" + data[(irow - 1) * bpr, bpr]
                    end

                    bpr.downto(1) do |i|
                        up = uprow[i].ord
                        left = line[i - bpp].ord
                        upleft = uprow[i - bpp].ord

                        line[i] = png_apply_prediction(predictor, line[i].ord, up, left, upleft, &:-)
                    end

                    line[0] = (predictor - 10).chr
                    result = line + result

                    line = uprow
                end

                result
            end

            #
            # Computes the next component value given a predictor and adjacent components.
            # A block must be passed to apply the operation.
            #
            def png_apply_prediction(predictor, value, up, left, upleft)

                result =
                    case predictor
                    when PNG_NONE
                        value
                    when PNG_SUB
                        yield(value, left)
                    when PNG_UP
                        yield(value, up)
                    when PNG_AVERAGE
                        yield(value, (left + up) / 2)
                    when PNG_PAETH
                        yield(value, png_paeth_choose(up, left, upleft))
                    else
                        raise PredictorError, "Unsupported PNG predictor : #{predictor}"
                    end

                (result & 0xFF).chr
            end

            #
            # Choose the preferred value in a PNG paeth predictor given the left, up and up left samples.
            #
            def png_paeth_choose(left, up, upleft)
                p = left + up - upleft
                pa, pb, pc = (p - left).abs, (p - up).abs, (p - upleft).abs

                case [pa, pb, pc].min
                when pa then left
                when pb then up
                when pc then upleft
                end
            end

            def tiff_post_prediction(data, colors, bpc, columns) #:nodoc:
                tiff_apply_prediction(data, colors, bpc, columns, &:+)
            end

            def tiff_pre_prediction(data, colors, bpc, columns) #:nodoc:
                tiff_apply_prediction(data, colors, bpc, columns, &:-)
            end

            def tiff_apply_prediction(data, colors, bpc, columns) #:nodoc:
                bpr = (colors * bpc * columns + 7) >> 3
                nrows = data.size / bpr
                bitmask = (1 << bpc) - 1
                result = Utils::BitWriter.new

                nrows.times do |irow|
                    line = Utils::BitReader.new(data[irow * bpr, bpr])

                    diffpixel = ::Array.new(colors, 0)
                    columns.times do
                        pixel = ::Array.new(colors) { line.read(bpc) }
                        diffpixel = diffpixel.zip(pixel).map!{|diff, c| yield(c, diff) & bitmask}

                        diffpixel.each do |c|
                            result.write(c, bpc)
                        end
                    end

                    result.final
                end

                result.final.to_s
            end
        end

    end
end