# -*- encoding: utf-8; frozen_string_literal: true -*- # #-- # This file is part of HexaPDF. # # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby # Copyright (C) 2014-2019 Thomas Leitner # # HexaPDF is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License version 3 as # published by the Free Software Foundation with the addition of the # following permission added to Section 15 as permitted in Section 7(a): # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON # INFRINGEMENT OF THIRD PARTY RIGHTS. # # HexaPDF 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 Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with HexaPDF. If not, see . # # The interactive user interfaces in modified source and object code # versions of HexaPDF must display Appropriate Legal Notices, as required # under Section 5 of the GNU Affero General Public License version 3. # # In accordance with Section 7(b) of the GNU Affero General Public # License, a covered work must retain the producer line in every PDF that # is created or manipulated using HexaPDF. # # If the GNU Affero General Public License doesn't fit your need, # commercial licenses are available at . #++ require 'hexapdf/error' require 'hexapdf/dictionary' require 'hexapdf/filter' module HexaPDF # Container for stream data that is more complex than a string. # # This helper class wraps all information necessary to read stream data by using a Fiber object # (see HexaPDF::Filter). The underlying data either comes from an IO object, a file represented by # its file name or a Fiber defined via a Proc object. # # Additionally, the #filter and #decode_parms can be set to indicate that the data returned from # the Fiber needs to be post-processed. The +filter+ and +decode_parms+ are automatically # normalized to arrays on assignment to ease further processing. class StreamData # The filter(s) that need to be applied for getting the decoded stream data. attr_reader :filter # The decoding parameters associated with the +filter+(s). attr_reader :decode_parms # :call-seq: # StreamData.new(io) -> stream_data # StreamData.new(str) -> stream_data # StreamData.new(proc) -> stream_data # StreamData.new { block } -> stream_data # # Creates a new StreamData object for the given +source+ and with the given options. # # The +source+ can be: # # * An IO stream which is read starting from a specific +offset+ for a specific +length+ # # * A string which is interpreted as a file name and read starting from a specific +offset+ # * and for a specific +length+ # # * A Proc object (that is converted to a Fiber when needed) in which case the +offset+ and # value is ignored. The Proc object can also be passed by using a block. def initialize(source = nil, offset: nil, length: nil, filter: nil, decode_parms: nil, &block) if source.nil? && !block_given? raise ArgumentError, "Either a source object or a block must be given" end @source = source || block @offset = offset @length = length @filter = [filter].flatten.compact @decode_parms = [decode_parms].flatten freeze end # Returns a Fiber for getting at the data of the stream represented by this object. def fiber(chunk_size = 0) if @source.kind_of?(Proc) FiberWithLength.new(@length, &@source) elsif @source.kind_of?(String) HexaPDF::Filter.source_from_file(@source, pos: @offset || 0, length: @length || -1, chunk_size: chunk_size) else HexaPDF::Filter.source_from_io(@source, pos: @offset || 0, length: @length || -1, chunk_size: chunk_size) end end # Returns whether this stream data object is equal to the other stream data object. def ==(other) other.kind_of?(StreamData) && source == other.source && offset == other.offset && length == other.length && filter == other.filter && decode_parms == other.decode_parms end protected # The source. attr_reader :source # The optional offset into the bytes provided by source. attr_reader :offset # The optional number of bytes to use starting from offset. attr_reader :length end # Implements Stream objects of the PDF object system. # # == Stream Objects # # A stream may also be associated with a PDF object but only if the value is a PDF dictionary. # This associated dictionary further describes the stream, like its length or how it is encoded. # # Such a stream object in PDF contains string data but of possibly unlimited length. Therefore # it is used for large amounts of data like images, page descriptions or embedded files. # # The basic Object class cannot hold stream data, only this subclass contains the necessary # methods to conveniently work with the stream data! # # Note that support for external streams (/F, /FFilter, /FDecodeParms) is not yet implemented! # # See: PDF1.7 s7.3.8, Dictionary class Stream < Dictionary define_field :Length, type: Integer # not required, will be auto-filled when writing define_field :Filter, type: [Symbol, PDFArray] define_field :DecodeParms, type: [Dictionary, PDFArray] define_field :F, type: :Filespec, version: '1.2' define_field :FFilter, type: [Symbol, PDFArray], version: '1.2' define_field :FDecodeParms, type: [Dictionary, PDFArray], version: '1.2' define_field :DL, type: Integer # Stream objects must always be indirect. def must_be_indirect? true end # Assigns a new stream data object. # # The +stream+ argument can be a HexaPDF::StreamData object, a String object or +nil+. # # If +stream+ is +nil+, an empty binary string is used instead. def stream=(stream) data.stream = stream after_data_change end # Returns the (possibly decoded) stream data as string. # # Note that modifications done to the returned string are not reflected in the Stream object # itself. The modified string must explicitly be assigned via #stream= to take effect. def stream if data.stream.kind_of?(String) data.stream.dup else HexaPDF::Filter.string_from_source(stream_decoder) end end # Returns the raw stream object. # # The returned value can be of many different types (see #stream=). For working with the # decoded stream contents use #stream. def raw_stream data.stream end # Returns the Fiber representing the unprocessed content of the stream. def stream_source if data.stream.kind_of?(String) HexaPDF::Filter.source_from_string(data.stream) else data.stream.fiber(config['io.chunk_size']) end end # Returns the decoder Fiber for the stream data. # # See the Filter module for more information on how to work with the fiber. def stream_decoder source = stream_source if data.stream.kind_of?(StreamData) data.stream.filter.zip(data.stream.decode_parms) do |filter, decode_parms| source = filter_for_name(filter).decoder(source, decode_parms) end end source end # :call-seq: # stream.stream_encoder # # Returns the encoder Fiber for the stream data. # # See the Filter module for more information on how to work with the fiber. def stream_encoder(source = stream_source) encoder_data = [document.unwrap(self[:Filter])].flatten. zip([document.unwrap(self[:DecodeParms])].flatten). delete_if {|f, _| f.nil? } if data.stream.kind_of?(StreamData) decoder_data = data.stream.filter.zip(data.stream.decode_parms) while !decoder_data.empty? && !encoder_data.empty? && decoder_data.last == encoder_data.last decoder_data.pop encoder_data.pop end decoder_data.each do |filter, decode_parms| source = filter_for_name(filter).decoder(source, decode_parms) end end encoder_data.reverse!.each do |filter, decode_parms| source = filter_for_name(filter).encoder(source, decode_parms) end source end # Sets the filters that should be used for encoding the stream. # # The arguments +filter+ as well as +decode_parms+ can either be a single items or arrays. # # The filters have to be specified in the *decoding order*! For example, if the filters would # be [:A85, :Fl], the stream would first be encoded with the Flate and then with the ASCII85 # filter. def set_filter(filter, decode_parms = nil) if filter.nil? || (filter.kind_of?(Array) && filter.empty?) delete(:Filter) else self[:Filter] = filter end if decode_parms.nil? || (decode_parms.kind_of?(Array) && decode_parms.empty?) || !key?(:Filter) delete(:DecodeParms) else self[:DecodeParms] = decode_parms end end private # Makes sure that the stream data is either a String or a HexaPDF::StreamData object. def after_data_change super data.stream ||= ''.b unless data.stream.kind_of?(StreamData) || data.stream.kind_of?(String) raise ArgumentError, "Object of class #{data.stream.class} cannot be used as stream value" end end # Returns the filter object that corresponds to the given filter name. # # See: HexaPDF::Filter def filter_for_name(filter_name) config.constantize('filter.map', filter_name) do raise HexaPDF::Error, "Unknown stream filter '#{filter_name}' encountered" end end # :nodoc: # A mapping from short name to long name for filters. FILTER_MAP = {AHx: :ASCIIHexDecode, A85: :ASCII85Decode, LZW: :LZWDecode, Fl: :FlateDecode, RL: :RunLengthDecode, CCF: :CCITTFaxDecode, DCT: :DCTDecode}.freeze # Validates the /Filter entry so that it contains only long-name filter names. def perform_validation super if value[:Filter].kind_of?(Symbol) && FILTER_MAP.key?(value[:Filter]) yield("A stream's /Filter entry may only use long-form filter names", true) value[:Filter] = FILTER_MAP[value[:Filter]] elsif value[:Filter].kind_of?(Array) value[:Filter].map! do |filter| next filter unless FILTER_MAP.key?(filter) yield("A stream's /Filter entry may only use long-form filter names", true) FILTER_MAP[filter] end end end end end