You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and dots ('.'), can be up to 35 characters long. Letters must be lowercase.
477 lines
21 KiB
477 lines
21 KiB
/* |
|
Copyright 2005-2007 Adobe Systems Incorporated |
|
|
|
Use, modification and distribution are subject to the Boost Software License, |
|
Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at |
|
http://www.boost.org/LICENSE_1_0.txt). |
|
|
|
See http://opensource.adobe.com/gil for most recent version including documentation. |
|
*/ |
|
/*************************************************************************************************/ |
|
|
|
#ifndef GIL_CHANNEL_ALGORITHM_HPP |
|
#define GIL_CHANNEL_ALGORITHM_HPP |
|
|
|
//////////////////////////////////////////////////////////////////////////////////////// |
|
/// \file |
|
/// \brief Channel algorithms |
|
/// \author Lubomir Bourdev and Hailin Jin \n |
|
/// Adobe Systems Incorporated |
|
/// \date 2005-2007 \n Last updated on May 6, 2007 |
|
/// |
|
/// Definitions of standard GIL 8-bit, 16-bit, 32-bit channels |
|
/// |
|
//////////////////////////////////////////////////////////////////////////////////////// |
|
|
|
#include "gil_config.hpp" |
|
#include "channel.hpp" |
|
#include <boost/mpl/less.hpp> |
|
#include <boost/mpl/integral_c.hpp> |
|
#include <boost/mpl/greater.hpp> |
|
#include <boost/type_traits.hpp> |
|
|
|
namespace boost { namespace gil { |
|
|
|
//#ifdef _MSC_VER |
|
//#pragma warning(push) |
|
//#pragma warning(disable: 4309) // disable truncation of constant value warning (using -1 to get the max value of an integral) |
|
//#endif |
|
|
|
namespace detail { |
|
|
|
// some forward declarations |
|
template <typename SrcChannelV, typename DstChannelV, bool SrcIsIntegral, bool DstIsIntegral> struct channel_converter_unsigned_impl; |
|
template <typename SrcChannelV, typename DstChannelV, bool SrcIsGreater> struct channel_converter_unsigned_integral; |
|
template <typename SrcChannelV, typename DstChannelV, bool SrcLessThanDst, bool SrcDivisible> struct channel_converter_unsigned_integral_impl; |
|
template <typename SrcChannelV, typename DstChannelV, bool SrcLessThanDst, bool CannotFitInInteger> struct channel_converter_unsigned_integral_nondivisible; |
|
|
|
////////////////////////////////////// |
|
//// unsigned_integral_max_value - given an unsigned integral channel type, returns its maximum value as an MPL integral constant |
|
////////////////////////////////////// |
|
|
|
|
|
template <typename UnsignedIntegralChannel> |
|
struct unsigned_integral_max_value : public mpl::integral_c<UnsignedIntegralChannel,-1> {}; |
|
|
|
template <> |
|
struct unsigned_integral_max_value<uint8_t> : public mpl::integral_c<uint32_t,0xFF> {}; |
|
template <> |
|
struct unsigned_integral_max_value<uint16_t> : public mpl::integral_c<uint32_t,0xFFFF> {}; |
|
template <> |
|
struct unsigned_integral_max_value<uint32_t> : public mpl::integral_c<uintmax_t,0xFFFFFFFF> {}; |
|
|
|
|
|
template <int K> |
|
struct unsigned_integral_max_value<packed_channel_value<K> > |
|
: public mpl::integral_c<typename packed_channel_value<K>::integer_t, (1<<K)-1> {}; |
|
|
|
////////////////////////////////////// |
|
//// unsigned_integral_num_bits - given an unsigned integral channel type, returns the minimum number of bits needed to represent it |
|
////////////////////////////////////// |
|
|
|
template <typename UnsignedIntegralChannel> |
|
struct unsigned_integral_num_bits : public mpl::int_<sizeof(UnsignedIntegralChannel)*8> {}; |
|
|
|
template <int K> |
|
struct unsigned_integral_num_bits<packed_channel_value<K> > |
|
: public mpl::int_<K> {}; |
|
|
|
} // namespace detail |
|
|
|
/** |
|
\defgroup ChannelConvertAlgorithm channel_convert |
|
\brief Converting from one channel type to another |
|
\ingroup ChannelAlgorithm |
|
|
|
Conversion is done as a simple linear mapping of one channel range to the other, |
|
such that the minimum/maximum value of the source maps to the minimum/maximum value of the destination. |
|
One implication of this is that the value 0 of signed channels may not be preserved! |
|
|
|
When creating new channel models, it is often a good idea to provide specializations for the channel conversion algorithms, for |
|
example, for performance optimizations. If the new model is an integral type that can be signed, it is easier to define the conversion |
|
only for the unsigned type (\p channel_converter_unsigned) and provide specializations of \p detail::channel_convert_to_unsigned |
|
and \p detail::channel_convert_from_unsigned to convert between the signed and unsigned type. |
|
|
|
Example: |
|
\code |
|
// bits32f is a floating point channel with range [0.0f ... 1.0f] |
|
bits32f src_channel = channel_traits<bits32f>::max_value(); |
|
assert(src_channel == 1); |
|
|
|
// bits8 is 8-bit unsigned integral channel (typedef-ed from unsigned char) |
|
bits8 dst_channel = channel_convert<bits8>(src_channel); |
|
assert(dst_channel == 255); // max value goes to max value |
|
\endcode |
|
*/ |
|
|
|
/** |
|
\defgroup ChannelConvertUnsignedAlgorithm channel_converter_unsigned |
|
\ingroup ChannelConvertAlgorithm |
|
\brief Convert one unsigned/floating point channel to another. Converts both the channel type and range |
|
@{ |
|
*/ |
|
|
|
////////////////////////////////////// |
|
//// channel_converter_unsigned |
|
////////////////////////////////////// |
|
|
|
template <typename SrcChannelV, typename DstChannelV> // Model ChannelValueConcept |
|
struct channel_converter_unsigned |
|
: public detail::channel_converter_unsigned_impl<SrcChannelV,DstChannelV,is_integral<SrcChannelV>::value,is_integral<DstChannelV>::value> {}; |
|
|
|
|
|
/// \brief Converting a channel to itself - identity operation |
|
template <typename T> struct channel_converter_unsigned<T,T> : public detail::identity<T> {}; |
|
|
|
|
|
namespace detail { |
|
|
|
////////////////////////////////////// |
|
//// channel_converter_unsigned_impl |
|
////////////////////////////////////// |
|
|
|
/// \brief This is the default implementation. Performance specializatons are provided |
|
template <typename SrcChannelV, typename DstChannelV, bool SrcIsIntegral, bool DstIsIntegral> |
|
struct channel_converter_unsigned_impl : public std::unary_function<DstChannelV,SrcChannelV> { |
|
DstChannelV operator()(SrcChannelV src) const { |
|
return DstChannelV(channel_traits<DstChannelV>::min_value() + |
|
(src - channel_traits<SrcChannelV>::min_value()) / channel_range<SrcChannelV>() * channel_range<DstChannelV>()); |
|
} |
|
private: |
|
template <typename C> |
|
static double channel_range() { |
|
return double(channel_traits<C>::max_value()) - double(channel_traits<C>::min_value()); |
|
} |
|
}; |
|
|
|
// When both the source and the destination are integral channels, perform a faster conversion |
|
template <typename SrcChannelV, typename DstChannelV> |
|
struct channel_converter_unsigned_impl<SrcChannelV,DstChannelV,true,true> |
|
: public channel_converter_unsigned_integral<SrcChannelV,DstChannelV, |
|
mpl::less<unsigned_integral_max_value<SrcChannelV>,unsigned_integral_max_value<DstChannelV> >::value > {}; |
|
|
|
|
|
////////////////////////////////////// |
|
//// channel_converter_unsigned_integral |
|
////////////////////////////////////// |
|
|
|
template <typename SrcChannelV, typename DstChannelV> |
|
struct channel_converter_unsigned_integral<SrcChannelV,DstChannelV,true> |
|
: public channel_converter_unsigned_integral_impl<SrcChannelV,DstChannelV,true, |
|
!(unsigned_integral_max_value<DstChannelV>::value % unsigned_integral_max_value<SrcChannelV>::value) > {}; |
|
|
|
template <typename SrcChannelV, typename DstChannelV> |
|
struct channel_converter_unsigned_integral<SrcChannelV,DstChannelV,false> |
|
: public channel_converter_unsigned_integral_impl<SrcChannelV,DstChannelV,false, |
|
!(unsigned_integral_max_value<SrcChannelV>::value % unsigned_integral_max_value<DstChannelV>::value) > {}; |
|
|
|
|
|
////////////////////////////////////// |
|
//// channel_converter_unsigned_integral_impl |
|
////////////////////////////////////// |
|
|
|
// Both source and destination are unsigned integral channels, |
|
// the src max value is less than the dst max value, |
|
// and the dst max value is divisible by the src max value |
|
template <typename SrcChannelV, typename DstChannelV> |
|
struct channel_converter_unsigned_integral_impl<SrcChannelV,DstChannelV,true,true> { |
|
DstChannelV operator()(SrcChannelV src) const { |
|
typedef typename unsigned_integral_max_value<DstChannelV>::value_type integer_t; |
|
static const integer_t mul = unsigned_integral_max_value<DstChannelV>::value / unsigned_integral_max_value<SrcChannelV>::value; |
|
return DstChannelV(src * mul); |
|
} |
|
}; |
|
|
|
// Both source and destination are unsigned integral channels, |
|
// the dst max value is less than (or equal to) the src max value, |
|
// and the src max value is divisible by the dst max value |
|
template <typename SrcChannelV, typename DstChannelV> |
|
struct channel_converter_unsigned_integral_impl<SrcChannelV,DstChannelV,false,true> { |
|
DstChannelV operator()(SrcChannelV src) const { |
|
typedef typename unsigned_integral_max_value<SrcChannelV>::value_type integer_t; |
|
static const integer_t div = unsigned_integral_max_value<SrcChannelV>::value / unsigned_integral_max_value<DstChannelV>::value; |
|
static const integer_t div2 = div/2; |
|
return DstChannelV((src + div2) / div); |
|
} |
|
}; |
|
|
|
// Prevent overflow for the largest integral type |
|
template <typename DstChannelV> |
|
struct channel_converter_unsigned_integral_impl<uintmax_t,DstChannelV,false,true> { |
|
DstChannelV operator()(uintmax_t src) const { |
|
static const uintmax_t div = unsigned_integral_max_value<bits32>::value / unsigned_integral_max_value<DstChannelV>::value; |
|
static const uintmax_t div2 = div/2; |
|
if (src > unsigned_integral_max_value<uintmax_t>::value - div2) |
|
return unsigned_integral_max_value<DstChannelV>::value; |
|
return DstChannelV((src + div2) / div); |
|
} |
|
}; |
|
|
|
// Both source and destination are unsigned integral channels, |
|
// and the dst max value is not divisible by the src max value |
|
// See if you can represent the expression (src * dst_max) / src_max in integral form |
|
template <typename SrcChannelV, typename DstChannelV, bool SrcLessThanDst> |
|
struct channel_converter_unsigned_integral_impl<SrcChannelV,DstChannelV,SrcLessThanDst,false> |
|
: public channel_converter_unsigned_integral_nondivisible<SrcChannelV,DstChannelV,SrcLessThanDst, |
|
mpl::greater< |
|
mpl::plus<unsigned_integral_num_bits<SrcChannelV>,unsigned_integral_num_bits<DstChannelV> >, |
|
unsigned_integral_num_bits<uintmax_t> |
|
>::value> {}; |
|
|
|
|
|
// Both source and destination are unsigned integral channels, |
|
// the src max value is less than the dst max value, |
|
// and the dst max value is not divisible by the src max value |
|
// The expression (src * dst_max) / src_max fits in an integer |
|
template <typename SrcChannelV, typename DstChannelV> |
|
struct channel_converter_unsigned_integral_nondivisible<SrcChannelV,DstChannelV,true,false> { |
|
DstChannelV operator()(SrcChannelV src) const { |
|
typedef typename detail::min_fast_uint<unsigned_integral_num_bits<SrcChannelV>::value+unsigned_integral_num_bits<DstChannelV>::value>::type integer_t; |
|
return DstChannelV(integer_t(src * unsigned_integral_max_value<DstChannelV>::value) / unsigned_integral_max_value<SrcChannelV>::value); |
|
} |
|
}; |
|
|
|
// Both source and destination are unsigned integral channels, |
|
// the src max value is less than the dst max value, |
|
// and the dst max value is not divisible by the src max value |
|
// The expression (src * dst_max) / src_max cannot fit in an integer (overflows). Use a double |
|
template <typename SrcChannelV, typename DstChannelV> |
|
struct channel_converter_unsigned_integral_nondivisible<SrcChannelV,DstChannelV,true,true> { |
|
DstChannelV operator()(SrcChannelV src) const { |
|
static const double mul = unsigned_integral_max_value<DstChannelV>::value / double(unsigned_integral_max_value<SrcChannelV>::value); |
|
return DstChannelV(src * mul); |
|
} |
|
}; |
|
|
|
// Both source and destination are unsigned integral channels, |
|
// the dst max value is less than (or equal to) the src max value, |
|
// and the src max value is not divisible by the dst max value |
|
template <typename SrcChannelV, typename DstChannelV, bool CannotFit> |
|
struct channel_converter_unsigned_integral_nondivisible<SrcChannelV,DstChannelV,false,CannotFit> { |
|
DstChannelV operator()(SrcChannelV src) const { |
|
|
|
typedef typename detail::unsigned_integral_max_value< SrcChannelV >::value_type src_integer_t; |
|
typedef typename detail::unsigned_integral_max_value< DstChannelV >::value_type dst_integer_t; |
|
|
|
static const double div = unsigned_integral_max_value<SrcChannelV>::value |
|
/ static_cast< double >( unsigned_integral_max_value<DstChannelV>::value ); |
|
|
|
static const src_integer_t div2 = static_cast< src_integer_t >( div / 2.0 ); |
|
|
|
return DstChannelV( static_cast< dst_integer_t >(( static_cast< double >( src + div2 ) / div ))); |
|
} |
|
}; |
|
|
|
} // namespace detail |
|
|
|
///////////////////////////////////////////////////// |
|
/// bits32f conversion |
|
///////////////////////////////////////////////////// |
|
|
|
template <typename DstChannelV> struct channel_converter_unsigned<bits32f,DstChannelV> : public std::unary_function<bits32f,DstChannelV> { |
|
DstChannelV operator()(bits32f x) const |
|
{ |
|
typedef typename detail::unsigned_integral_max_value< DstChannelV >::value_type dst_integer_t; |
|
return DstChannelV( static_cast< dst_integer_t >(x*channel_traits<DstChannelV>::max_value()+0.5f )); |
|
} |
|
}; |
|
|
|
template <typename SrcChannelV> struct channel_converter_unsigned<SrcChannelV,bits32f> : public std::unary_function<SrcChannelV,bits32f> { |
|
bits32f operator()(SrcChannelV x) const { return bits32f(x/float(channel_traits<SrcChannelV>::max_value())); } |
|
}; |
|
|
|
template <> struct channel_converter_unsigned<bits32f,bits32f> : public std::unary_function<bits32f,bits32f> { |
|
bits32f operator()(bits32f x) const { return x; } |
|
}; |
|
|
|
|
|
/// \brief 32 bit <-> float channel conversion |
|
template <> struct channel_converter_unsigned<bits32,bits32f> : public std::unary_function<bits32,bits32f> { |
|
bits32f operator()(bits32 x) const { |
|
// unfortunately without an explicit check it is possible to get a round-off error. We must ensure that max_value of bits32 matches max_value of bits32f |
|
if (x>=channel_traits<bits32>::max_value()) return channel_traits<bits32f>::max_value(); |
|
return float(x) / float(channel_traits<bits32>::max_value()); |
|
} |
|
}; |
|
/// \brief 32 bit <-> float channel conversion |
|
template <> struct channel_converter_unsigned<bits32f,bits32> : public std::unary_function<bits32f,bits32> { |
|
bits32 operator()(bits32f x) const { |
|
// unfortunately without an explicit check it is possible to get a round-off error. We must ensure that max_value of bits32 matches max_value of bits32f |
|
if (x>=channel_traits<bits32f>::max_value()) return channel_traits<bits32>::max_value(); |
|
return bits32(x * channel_traits<bits32>::max_value() + 0.5f); |
|
} |
|
}; |
|
|
|
/// @} |
|
|
|
namespace detail { |
|
// Converting from signed to unsigned integral channel. |
|
// It is both a unary function, and a metafunction (thus requires the 'type' nested typedef, which equals result_type) |
|
template <typename ChannelValue> // Model ChannelValueConcept |
|
struct channel_convert_to_unsigned : public detail::identity<ChannelValue> { |
|
typedef ChannelValue type; |
|
}; |
|
|
|
template <> struct channel_convert_to_unsigned<bits8s> : public std::unary_function<bits8s,bits8> { |
|
typedef bits8 type; |
|
type operator()(bits8s val) const { return val+128; } |
|
}; |
|
|
|
template <> struct channel_convert_to_unsigned<bits16s> : public std::unary_function<bits16s,bits16> { |
|
typedef bits16 type; |
|
type operator()(bits16s val) const { return val+32768; } |
|
}; |
|
|
|
template <> struct channel_convert_to_unsigned<bits32s> : public std::unary_function<bits32s,bits32> { |
|
typedef bits32 type; |
|
type operator()(bits32s x) const { return static_cast<bits32>(x+(1<<31)); } |
|
}; |
|
|
|
|
|
// Converting from unsigned to signed integral channel |
|
// It is both a unary function, and a metafunction (thus requires the 'type' nested typedef, which equals result_type) |
|
template <typename ChannelValue> // Model ChannelValueConcept |
|
struct channel_convert_from_unsigned : public detail::identity<ChannelValue> { |
|
typedef ChannelValue type; |
|
}; |
|
|
|
template <> struct channel_convert_from_unsigned<bits8s> : public std::unary_function<bits8,bits8s> { |
|
typedef bits8s type; |
|
type operator()(bits8 val) const { return val-128; } |
|
}; |
|
|
|
template <> struct channel_convert_from_unsigned<bits16s> : public std::unary_function<bits16,bits16s> { |
|
typedef bits16s type; |
|
type operator()(bits16 val) const { return val-32768; } |
|
}; |
|
|
|
template <> struct channel_convert_from_unsigned<bits32s> : public std::unary_function<bits32,bits32s> { |
|
typedef bits32s type; |
|
type operator()(bits32 x) const { return static_cast<bits32s>(x-(1<<31)); } |
|
}; |
|
|
|
} // namespace detail |
|
|
|
/// \ingroup ChannelConvertAlgorithm |
|
/// \brief A unary function object converting between channel types |
|
template <typename SrcChannelV, typename DstChannelV> // Model ChannelValueConcept |
|
struct channel_converter : public std::unary_function<SrcChannelV,DstChannelV> { |
|
DstChannelV operator()(const SrcChannelV& src) const { |
|
typedef detail::channel_convert_to_unsigned<SrcChannelV> to_unsigned; |
|
typedef detail::channel_convert_from_unsigned<DstChannelV> from_unsigned; |
|
typedef channel_converter_unsigned<typename to_unsigned::result_type, typename from_unsigned::argument_type> converter_unsigned; |
|
return from_unsigned()(converter_unsigned()(to_unsigned()(src))); |
|
} |
|
}; |
|
|
|
/// \ingroup ChannelConvertAlgorithm |
|
/// \brief Converting from one channel type to another. |
|
template <typename DstChannel, typename SrcChannel> // Model ChannelConcept (could be channel references) |
|
inline typename channel_traits<DstChannel>::value_type channel_convert(const SrcChannel& src) { |
|
return channel_converter<typename channel_traits<SrcChannel>::value_type, |
|
typename channel_traits<DstChannel>::value_type>()(src); |
|
} |
|
|
|
/// \ingroup ChannelConvertAlgorithm |
|
/// \brief Same as channel_converter, except it takes the destination channel by reference, which allows |
|
/// us to move the templates from the class level to the method level. This is important when invoking it |
|
/// on heterogeneous pixels. |
|
struct default_channel_converter { |
|
template <typename Ch1, typename Ch2> |
|
void operator()(const Ch1& src, Ch2& dst) const { |
|
dst=channel_convert<Ch2>(src); |
|
} |
|
}; |
|
|
|
namespace detail { |
|
// fast integer division by 255 |
|
inline uint32_t div255(uint32_t in) { uint32_t tmp=in+128; return (tmp + (tmp>>8))>>8; } |
|
|
|
// fast integer divison by 32768 |
|
inline uint32_t div32768(uint32_t in) { return (in+16384)>>15; } |
|
} |
|
|
|
/** |
|
\defgroup ChannelMultiplyAlgorithm channel_multiply |
|
\ingroup ChannelAlgorithm |
|
\brief Multiplying unsigned channel values of the same type. Performs scaled multiplication result = a * b / max_value |
|
|
|
Example: |
|
\code |
|
bits8 x=128; |
|
bits8 y=128; |
|
bits8 mul = channel_multiply(x,y); |
|
assert(mul == 64); // 64 = 128 * 128 / 255 |
|
\endcode |
|
*/ |
|
/// @{ |
|
|
|
/// \brief This is the default implementation. Performance specializatons are provided |
|
template <typename ChannelValue> |
|
struct channel_multiplier_unsigned : public std::binary_function<ChannelValue,ChannelValue,ChannelValue> { |
|
ChannelValue operator()(ChannelValue a, ChannelValue b) const { |
|
return ChannelValue(a / double(channel_traits<ChannelValue>::max_value()) * b); |
|
} |
|
}; |
|
|
|
/// \brief Specialization of channel_multiply for 8-bit unsigned channels |
|
template<> struct channel_multiplier_unsigned<bits8> : public std::binary_function<bits8,bits8,bits8> { |
|
bits8 operator()(bits8 a, bits8 b) const { return bits8(detail::div255(uint32_t(a) * uint32_t(b))); } |
|
}; |
|
|
|
/// \brief Specialization of channel_multiply for 16-bit unsigned channels |
|
template<> struct channel_multiplier_unsigned<bits16> : public std::binary_function<bits16,bits16,bits16> { |
|
bits16 operator()(bits16 a, bits16 b) const { return bits16((uint32_t(a) * uint32_t(b))/65535); } |
|
}; |
|
|
|
/// \brief Specialization of channel_multiply for float 0..1 channels |
|
template<> struct channel_multiplier_unsigned<bits32f> : public std::binary_function<bits32f,bits32f,bits32f> { |
|
bits32f operator()(bits32f a, bits32f b) const { return a*b; } |
|
}; |
|
|
|
/// \brief A function object to multiply two channels. result = a * b / max_value |
|
template <typename ChannelValue> |
|
struct channel_multiplier : public std::binary_function<ChannelValue, ChannelValue, ChannelValue> { |
|
ChannelValue operator()(ChannelValue a, ChannelValue b) const { |
|
typedef detail::channel_convert_to_unsigned<ChannelValue> to_unsigned; |
|
typedef detail::channel_convert_from_unsigned<ChannelValue> from_unsigned; |
|
typedef channel_multiplier_unsigned<typename to_unsigned::result_type> multiplier_unsigned; |
|
return from_unsigned()(multiplier_unsigned()(to_unsigned()(a), to_unsigned()(b))); |
|
} |
|
}; |
|
|
|
/// \brief A function multiplying two channels. result = a * b / max_value |
|
template <typename Channel> // Models ChannelConcept (could be a channel reference) |
|
inline typename channel_traits<Channel>::value_type channel_multiply(Channel a, Channel b) { |
|
return channel_multiplier<typename channel_traits<Channel>::value_type>()(a,b); |
|
} |
|
/// @} |
|
|
|
/** |
|
\defgroup ChannelInvertAlgorithm channel_invert |
|
\ingroup ChannelAlgorithm |
|
\brief Returns the inverse of a channel. result = max_value - x + min_value |
|
|
|
Example: |
|
\code |
|
// bits8 == uint8_t == unsigned char |
|
bits8 x=255; |
|
bits8 inv = channel_invert(x); |
|
assert(inv == 0); |
|
\endcode |
|
*/ |
|
|
|
/// \brief Default implementation. Provide overloads for performance |
|
/// \ingroup ChannelInvertAlgorithm channel_invert |
|
template <typename Channel> // Models ChannelConcept (could be a channel reference) |
|
inline typename channel_traits<Channel>::value_type channel_invert(Channel x) { |
|
return channel_traits<Channel>::max_value()-x + channel_traits<Channel>::min_value(); |
|
} |
|
|
|
//#ifdef _MSC_VER |
|
//#pragma warning(pop) |
|
//#endif |
|
|
|
} } // namespace boost::gil |
|
|
|
#endif
|
|
|