# -*- 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-2023 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'
module HexaPDF
module ImageLoader
# This module is used for loading images in the JPEG format from files or IO streams.
#
# See: PDF1.7 s7.4.8, ITU T.81 Annex B, ITU T.872
module JPEG
# The magic marker that tells us if the file/IO contains an image in JPEG format.
MAGIC_FILE_MARKER = "\xFF\xD8\xFF".b
# The various start-of-frame markers that tell us which kind of JPEG it is. The marker
# segment itself contains all the needed information needed for creating the PDF image
# object.
#
# See: ITU T.81 B1.1.3
SOF_MARKERS = [
0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF
].freeze
# Adobe uses the marker 0xEE (APPE or APP14) for its purposes. We need to use it for
# determinig whether we have a CMYK or YCCK image.
APP14_MARKER = 0xEE
# Value of the 12th byte in an APP14 marker specifying that the image uses CMYK color
# encoding, with all four colors complemented.
APP14_TRANSFORM_CMYK = 0
# End-of-image marker
EOI_MARKER = 0xD9
# Start-of-scan marker
SOS_MARKER = 0xDA
# :call-seq:
# JPEG.handles?(filename) -> true or false
# JPGE.handles?(io) -> true or false
#
# Returns +true+ if the given file or IO stream can be handled, ie. if it contains an image
# in JPEG format.
def self.handles?(file_or_io)
if file_or_io.kind_of?(String)
File.read(file_or_io, 3, mode: 'rb') == MAGIC_FILE_MARKER
else
file_or_io.rewind
file_or_io.read(3) == MAGIC_FILE_MARKER
end
end
# :call-seq:
# JPEG.load(document, filename) -> image_obj
# JPEG.load(document, io) -> image_obj
#
# Creates a PDF image object from the JPEG file or IO stream.
def self.load(document, file_or_io)
dict = if file_or_io.kind_of?(String)
File.open(file_or_io, 'rb') {|io| image_data_from_io(io) }
else
image_data_from_io(file_or_io)
end
document.add(dict, stream: HexaPDF::StreamData.new(file_or_io))
end
# Returns a hash containing the extracted JPEG image data.
def self.image_data_from_io(io)
io.seek(2, IO::SEEK_SET)
while true
code0 = io.getbyte
code1 = io.getbyte
# B1.1.2 - all markers start with 0xFF
if code0 != 0xFF
raise HexaPDF::Error, "Invalid bytes found, expected marker code"
end
# B1.1.2 - markers may be preceeded by any number of 0xFF fill bytes
code1 = io.getbyte while code1 == 0xFF
break if code1 == SOS_MARKER || code1 == EOI_MARKER
# B1.1.4 - next two bytes are the length of the segment (except for RSTm or TEM markers
# but those shouldn't appear here)
length = io.read(2).unpack1('n')
# According to T.872 6.1 and 6.5.3, if this marker is present, we need to use it for
# correctly determining whether complemented CMYK or YCCK is used
if code1 == APP14_MARKER
io.seek(length - 3, IO::SEEK_CUR)
invert_colors = true if io.getbyte == APP14_TRANSFORM_CMYK
next
elsif !SOF_MARKERS.include?(code1)
io.seek(length - 2, IO::SEEK_CUR)
next
end
bits, height, width, components = io.read(6).unpack('CnnC')
io.seek(length - 2 - 6, IO::SEEK_CUR)
# short-cut loop if we have all needed information
break if components != 4 || invert_colors
end
# PDF1.7 s8.9.5.1
if bits != 8
raise HexaPDF::Error, "Unsupported number of bits per component: #{bits}"
end
color_space = case components
when 1 then :DeviceGray
when 3 then :DeviceRGB
when 4 then :DeviceCMYK
end
dict = {
Type: :XObject,
Subtype: :Image,
Width: width,
Height: height,
ColorSpace: color_space,
BitsPerComponent: bits,
Filter: :DCTDecode,
}
if invert_colors && color_space == :DeviceCMYK
dict[:Decode] = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0]
end
dict
end
private_class_method :image_data_from_io
end
end
end