/// <summary> /// Attempts to find the TIFF "PhotometricInterpretation" metadata /// </summary> /// <param name="_Meta"></param> /// <param name="_MetaPath"></param> /// <returns>True if gamma was specified</returns> protected bool FindPhotometricInterpretation( BitmapMetadata _Meta, string _MetaPath ) { bool bGammaWasSpecified = false; EnumerateMetaData( _Meta, new MetaDataProcessor( _MetaPath, ( object v ) => { int PhotometricInterpretation = -1; if ( v is string ) { if ( !int.TryParse( v as string, out PhotometricInterpretation ) ) throw new Exception( "Invalid string for TIFF Photometric Interpretation !" ); } else if ( v is ushort ) PhotometricInterpretation = (ushort) v; switch ( PhotometricInterpretation ) { case 0: // Grayscale case 1: m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = 1.0f; bGammaWasSpecified = true; break; case 2: // NTSC RGB m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = 2.2f; bGammaWasSpecified = true; break; default: // According to the spec (page 117), a value of 6 is a YCrCb image while a value of 8 is a L*a*b* image // SHould we handle this in case of ??? throw new Exception( "TODO: Handle TIFF special photometric interpretation !" ); } } ) ); return bGammaWasSpecified; }
/// <summary> /// Build from a standard profile /// </summary> /// <param name="_Profile"></param> public ColorProfile( STANDARD_PROFILE _Profile ) { switch ( _Profile ) { case STANDARD_PROFILE.LINEAR: m_Chromaticities = Chromaticities.sRGB; m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = 1.0f; break; case STANDARD_PROFILE.sRGB: m_Chromaticities = Chromaticities.sRGB; m_GammaCurve = GAMMA_CURVE.sRGB; m_Gamma = GAMMA_EXPONENT_sRGB; break; case STANDARD_PROFILE.ADOBE_RGB_D50: m_Chromaticities = Chromaticities.AdobeRGB_D50; m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = GAMMA_EXPONENT_ADOBE; break; case STANDARD_PROFILE.ADOBE_RGB_D65: m_Chromaticities = Chromaticities.AdobeRGB_D65; m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = GAMMA_EXPONENT_ADOBE; break; case STANDARD_PROFILE.PRO_PHOTO: m_Chromaticities = Chromaticities.ProPhoto; m_GammaCurve = GAMMA_CURVE.PRO_PHOTO; m_Gamma = GAMMA_EXPONENT_PRO_PHOTO; break; case STANDARD_PROFILE.RADIANCE: m_Chromaticities = Chromaticities.Radiance; m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = 1.0f; break; default: throw new Exception( "Unsupported standard profile!" ); } BuildTransformFromChroma( true ); }
protected void EnumerateMetaDataPNG( BitmapMetadata _MetaData, out bool _bProfileFound, out bool _bGammaWasSpecified ) { bool bGammaWasSpecified = false; bool bProfileFound = false; EnumerateMetaData( _MetaData, // Read chromaticities new MetaDataProcessor( "/cHRM", ( object v ) => { BitmapMetadata ChromaData = v as BitmapMetadata; Chromaticities TempChroma = Chromaticities.Empty; EnumerateMetaData( ChromaData, new MetaDataProcessor( "/RedX", ( object v2 ) => { TempChroma.R.x = 0.00001f * (uint) v2; } ), new MetaDataProcessor( "/RedY", ( object v2 ) => { TempChroma.R.y = 0.00001f * (uint) v2; } ), new MetaDataProcessor( "/GreenX", ( object v2 ) => { TempChroma.G.x = 0.00001f * (uint) v2; } ), new MetaDataProcessor( "/GreenY", ( object v2 ) => { TempChroma.G.y = 0.00001f * (uint) v2; } ), new MetaDataProcessor( "/BlueX", ( object v2 ) => { TempChroma.B.x = 0.00001f * (uint) v2; } ), new MetaDataProcessor( "/BlueY", ( object v2 ) => { TempChroma.B.y = 0.00001f * (uint) v2; } ), new MetaDataProcessor( "/WhitePointX", ( object v2 ) => { TempChroma.W.x = 0.00001f * (uint) v2; } ), new MetaDataProcessor( "/WhitePointY", ( object v2 ) => { TempChroma.W.y = 0.00001f * (uint) v2; } ) ); if ( TempChroma.RecognizedChromaticity != STANDARD_PROFILE.INVALID ) { // Assign new chroma values m_Chromaticities = TempChroma; bProfileFound = true; } } ), // Read gamma new MetaDataProcessor( "/gAMA/ImageGamma", ( object v ) => { m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = 1.0f / (0.00001f * (uint) v); bGammaWasSpecified = true; } ), // Read explicit sRGB new MetaDataProcessor( "/sRGB/RenderingIntent", ( object v ) => { m_Chromaticities = Chromaticities.sRGB; bProfileFound = true; bGammaWasSpecified = false; } ), // Read string profile from iTXT new MetaDataProcessor( "/iTXt/TextEntry", ( object v ) => { if ( bProfileFound ) return; // No need... // Hack content ! string XMLContent = v as string; string ICCProfile = FindAttribute( XMLContent, "photoshop:ICCProfile" ); if ( ICCProfile != null && (bProfileFound = HandleICCProfileString( ICCProfile )) ) return; string ColorSpace = FindAttribute( XMLContent, "exif:ColorSpace" ); if ( ColorSpace != null ) bProfileFound = HandleEXIFColorSpace( ColorSpace ); } ) ); _bGammaWasSpecified = bGammaWasSpecified; _bProfileFound = bProfileFound; }
/// <summary> /// Ensures the current gamma curve type and value are the ones we want /// </summary> /// <param name="_Curve"></param> /// <param name="_Gamma"></param> /// <returns></returns> protected bool EnsureGamma( GAMMA_CURVE _Curve, float _Gamma ) { return m_GammaCurve == _Curve && Math.Abs( _Gamma - m_Gamma ) < 1e-3f; }
/// <summary> /// Builds the RGB<->XYZ transforms from chromaticities /// (refer to http://wiki.nuaj.net/index.php/Color_Transforms#XYZ_Matrices for explanations) /// </summary> protected void BuildTransformFromChroma( bool _bCheckGammaCurveOverride ) { float3 xyz_R = new float3( m_Chromaticities.R.x, m_Chromaticities.R.y, 1.0f - m_Chromaticities.R.x - m_Chromaticities.R.y ); float3 xyz_G = new float3( m_Chromaticities.G.x, m_Chromaticities.G.y, 1.0f - m_Chromaticities.G.x - m_Chromaticities.G.y ); float3 xyz_B = new float3( m_Chromaticities.B.x, m_Chromaticities.B.y, 1.0f - m_Chromaticities.B.x - m_Chromaticities.B.y ); float3 XYZ_W = xyY2XYZ( new float3( m_Chromaticities.W.x, m_Chromaticities.W.y, 1.0f ) ); float4x4 M_xyz = new float4x4() { row0 = new float4( xyz_R, 0.0f ), row1 = new float4( xyz_G, 0.0f ), row2 = new float4( xyz_B, 0.0f ), row3 = new float4( 0.0f, 0.0f, 0.0f, 1.0f ) }; M_xyz.Invert(); float4 Sum_RGB = new float4( XYZ_W, 1.0f ) * M_xyz; // Finally, we can retrieve the RGB->XYZ transform m_RGB2XYZ.row0 = new float4( Sum_RGB.x * xyz_R, 0.0f ); m_RGB2XYZ.row1 = new float4( Sum_RGB.y * xyz_G, 0.0f ); m_RGB2XYZ.row2 = new float4( Sum_RGB.z * xyz_B, 0.0f ); // And the XYZ->RGB transform m_XYZ2RGB = m_RGB2XYZ; m_XYZ2RGB.Invert(); // ============= Attempt to recognize a standard profile ============= STANDARD_PROFILE RecognizedChromaticity = m_Chromaticities.RecognizedChromaticity; if ( _bCheckGammaCurveOverride ) { // Also ensure the gamma ramp is correct before assigning a standard profile bool bIsGammaCorrect = true; switch ( RecognizedChromaticity ) { case STANDARD_PROFILE.sRGB: bIsGammaCorrect = EnsureGamma( GAMMA_CURVE.sRGB, GAMMA_EXPONENT_sRGB ); break; case STANDARD_PROFILE.ADOBE_RGB_D50: bIsGammaCorrect = EnsureGamma( GAMMA_CURVE.STANDARD, GAMMA_EXPONENT_ADOBE ); break; case STANDARD_PROFILE.ADOBE_RGB_D65: bIsGammaCorrect = EnsureGamma( GAMMA_CURVE.STANDARD, GAMMA_EXPONENT_ADOBE ); break; case STANDARD_PROFILE.PRO_PHOTO: bIsGammaCorrect = EnsureGamma( GAMMA_CURVE.PRO_PHOTO, GAMMA_EXPONENT_PRO_PHOTO ); break; case STANDARD_PROFILE.RADIANCE: bIsGammaCorrect = EnsureGamma( GAMMA_CURVE.STANDARD, 1.0f ); break; } if ( !bIsGammaCorrect ) RecognizedChromaticity = STANDARD_PROFILE.CUSTOM; // A non-standard gamma curves fails our pre-defined design... } // ============= Assign the internal converter depending on the profile ============= switch ( RecognizedChromaticity ) { case STANDARD_PROFILE.sRGB: m_GammaCurve = GAMMA_CURVE.sRGB; m_Gamma = GAMMA_EXPONENT_sRGB; m_InternalConverter = new InternalColorConverter_sRGB(); break; case STANDARD_PROFILE.ADOBE_RGB_D50: m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = GAMMA_EXPONENT_ADOBE; m_InternalConverter = new InternalColorConverter_AdobeRGB_D50(); break; case STANDARD_PROFILE.ADOBE_RGB_D65: m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = GAMMA_EXPONENT_ADOBE; m_InternalConverter = new InternalColorConverter_AdobeRGB_D65(); break; case STANDARD_PROFILE.PRO_PHOTO: m_GammaCurve = GAMMA_CURVE.PRO_PHOTO; m_Gamma = GAMMA_EXPONENT_PRO_PHOTO; m_InternalConverter = new InternalColorConverter_ProPhoto(); break; case STANDARD_PROFILE.RADIANCE: m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = 1.0f; m_InternalConverter = new InternalColorConverter_Radiance(); break; default: // Switch to one of our generic converters switch ( m_GammaCurve ) { case GAMMA_CURVE.sRGB: m_InternalConverter = new InternalColorConverter_Generic_sRGBGamma( m_RGB2XYZ, m_XYZ2RGB ); break; case GAMMA_CURVE.PRO_PHOTO: m_InternalConverter = new InternalColorConverter_Generic_ProPhoto( m_RGB2XYZ, m_XYZ2RGB ); break; case GAMMA_CURVE.STANDARD: if ( Math.Abs( m_Gamma - 1.0f ) < 1e-3f ) m_InternalConverter = new InternalColorConverter_Generic_NoGamma( m_RGB2XYZ, m_XYZ2RGB ); else m_InternalConverter = new InternalColorConverter_Generic_StandardGamma( m_RGB2XYZ, m_XYZ2RGB, m_Gamma ); break; } break; } }
/// <summary> /// Creates a color profile from chromaticities /// </summary> /// <param name="_Chromaticities">The chromaticities for this profile</param> /// <param name="_GammaCurve">The type of gamma curve to use</param> /// <param name="_Gamma">The gamma power</param> public ColorProfile( Chromaticities _Chromaticities, GAMMA_CURVE _GammaCurve, float _Gamma ) { m_Chromaticities = _Chromaticities; m_GammaCurve = _GammaCurve; m_Gamma = _Gamma; BuildTransformFromChroma( true ); }
/// <summary> /// Creates the color profile from metadata embedded in the image file /// </summary> /// <param name="_MetaData"></param> /// <param name="_FileType"></param> public ColorProfile( BitmapMetadata _MetaData, Bitmap.FILE_TYPE _FileType ) { string MetaDump = _MetaData != null ? DumpMetaData( _MetaData ) : null; bool bGammaFoundInFile = false; switch ( _FileType ) { case Bitmap.FILE_TYPE.JPEG: m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = 2.2f; // JPG uses a 2.2 gamma by default m_Chromaticities = Chromaticities.sRGB; // Default for JPEGs is sRGB EnumerateMetaDataJPG( _MetaData, out m_bProfileFoundInFile, out bGammaFoundInFile ); if ( !m_bProfileFoundInFile && !bGammaFoundInFile ) bGammaFoundInFile = true; // Unless specified otherwise, we override the gamma no matter what since JPEGs use a 2.2 gamma by default anyway break; case Bitmap.FILE_TYPE.PNG: m_GammaCurve = GAMMA_CURVE.sRGB; m_Gamma = GAMMA_EXPONENT_sRGB; m_Chromaticities = Chromaticities.sRGB; // Default for PNGs is standard sRGB EnumerateMetaDataPNG( _MetaData, out m_bProfileFoundInFile, out bGammaFoundInFile ); break; case Bitmap.FILE_TYPE.TIFF: m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = 1.0f; // Linear gamma by default m_Chromaticities = Chromaticities.sRGB; // Default for TIFFs is sRGB EnumerateMetaDataTIFF( _MetaData, out m_bProfileFoundInFile, out bGammaFoundInFile ); break; case Bitmap.FILE_TYPE.GIF: m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = 1.0f; m_Chromaticities = Chromaticities.sRGB; // Default for GIFs is standard sRGB with no gamma break; case Bitmap.FILE_TYPE.BMP: // BMP Don't have metadata! m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = 1.0f; m_Chromaticities = Chromaticities.sRGB; // Default for BMPs is standard sRGB with no gamma break; case Bitmap.FILE_TYPE.CRW: // Raw files have no correction case Bitmap.FILE_TYPE.CR2: case Bitmap.FILE_TYPE.DNG: m_GammaCurve = GAMMA_CURVE.STANDARD; m_Gamma = 1.0f; m_Chromaticities = Chromaticities.sRGB; // Default for BMPs is standard sRGB with no gamma break; } BuildTransformFromChroma( bGammaFoundInFile ); }