diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs index 0c6d6f395b..633bda9f46 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata.Profiles.Icc; @@ -33,64 +34,103 @@ internal static class ColorProfileConverterExtensionsIcc MemoryAllocator = converter.Options.MemoryAllocator, // TODO: Double check this but I think these are normalized values. - SourceWhitePoint = CieXyz.FromScaledVector4(new(converter.Options.SourceIccProfile.Header.PcsIlluminant, 1F)), - TargetWhitePoint = CieXyz.FromScaledVector4(new(converter.Options.TargetIccProfile.Header.PcsIlluminant, 1F)), + SourceWhitePoint = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant), + TargetWhitePoint = new CieXyz(converter.Options.TargetIccProfile.Header.PcsIlluminant), }); IccDataToPcsConverter sourceConverter = new(converter.Options.SourceIccProfile); IccPcsToDataConverter targetConverter = new(converter.Options.TargetIccProfile); - IccColorSpaceType sourcePcsType = converter.Options.SourceIccProfile.Header.ProfileConnectionSpace; - IccColorSpaceType targetPcsType = converter.Options.TargetIccProfile.Header.ProfileConnectionSpace; - IccVersion sourceVersion = converter.Options.SourceIccProfile.Header.Version; - IccVersion targetVersion = converter.Options.TargetIccProfile.Header.Version; - - Vector4 pcs = sourceConverter.Calculate(source.ToScaledVector4()); - - // Profile connecting spaces can only be Lab, XYZ. - if (sourcePcsType is IccColorSpaceType.CieLab && targetPcsType is IccColorSpaceType.CieXyz) - { - // Convert from Lab to XYZ. - CieLab lab = CieLab.FromScaledVector4(pcs); - CieXyz xyz = pcsConverter.Convert(in lab); - pcs = xyz.ToScaledVector4(); - } - else if (sourcePcsType is IccColorSpaceType.CieXyz && targetPcsType is IccColorSpaceType.CieLab) + IccProfileHeader sourceHeader = converter.Options.SourceIccProfile.Header; + IccProfileHeader targetHeader = converter.Options.TargetIccProfile.Header; + IccColorSpaceType sourcePcsType = sourceHeader.ProfileConnectionSpace; + IccColorSpaceType targetPcsType = targetHeader.ProfileConnectionSpace; + IccRenderingIntent sourceIntent = sourceHeader.RenderingIntent; + IccRenderingIntent targetIntent = targetHeader.RenderingIntent; + IccVersion sourceVersion = sourceHeader.Version; + IccVersion targetVersion = targetHeader.Version; + + // all conversions are funnelled through XYZ in case PCS adjustments need to be made + CieXyz xyz; + + Vector4 sourcePcs = sourceConverter.Calculate(source.ToScaledVector4()); + switch (sourcePcsType) { - // Convert from XYZ to Lab. - CieXyz xyz = CieXyz.FromScaledVector4(pcs); - CieLab lab = pcsConverter.Convert(in xyz); - pcs = lab.ToScaledVector4(); + case IccColorSpaceType.CieLab: + if (sourceConverter.Is16BitLutEntry) + { + sourcePcs = LabV2ToLab(sourcePcs); + } + + CieLab lab = CieLab.FromScaledVector4(sourcePcs); + xyz = pcsConverter.Convert(in lab); + break; + case IccColorSpaceType.CieXyz: + xyz = new CieXyz(sourcePcs[0], sourcePcs[1], sourcePcs[2]); + xyz = pcsConverter.Convert(in xyz); + break; + default: + throw new ArgumentOutOfRangeException($"Source PCS {sourcePcsType} not supported"); } - else if (sourcePcsType is IccColorSpaceType.CieXyz && targetPcsType is IccColorSpaceType.CieXyz) - { - // Convert from XYZ to XYZ. - CieXyz xyz = CieXyz.FromScaledVector4(pcs); - CieXyz targetXyz = pcsConverter.Convert(in xyz); - pcs = targetXyz.ToScaledVector4(); - } - else if (sourcePcsType is IccColorSpaceType.CieLab && targetPcsType is IccColorSpaceType.CieLab) + + // TODO: handle PCS adjustment for absolute intent? + // TODO: or throw unsupported error, since most profiles headers contain perceptual (i've encountered a couple of relative, but so far no saturation or absolute) + bool adjustSourcePcsForPerceptual = sourceIntent == IccRenderingIntent.Perceptual && sourceVersion.Major == 2; + bool adjustTargetPcsForPerceptual = targetIntent == IccRenderingIntent.Perceptual && targetVersion.Major == 2; + + // if both profiles need PCS adjustment, they both share the same unadjusted PCS space + // effectively cancelling out the need to make the adjustment + bool adjustPcsForPerceptual = adjustSourcePcsForPerceptual ^ adjustTargetPcsForPerceptual; + if (adjustPcsForPerceptual) { - // Convert from Lab to Lab. - if (sourceVersion.Major == 4 && targetVersion.Major == 2) + // as per DemoIccMAX icPerceptual values in IccCmm.h + CieXyz refBlack = new(0.00336F, 0.0034731F, 0.00287F); + CieXyz refWhite = new(0.9642F, 1.0000F, 0.8249F); + + if (adjustSourcePcsForPerceptual) { - // Convert from Lab v4 to Lab v2. - pcs = LabToLabV2(pcs); + Vector3 iccXyz = xyz.ToScaledVector4().AsVector128().AsVector3(); + Vector3 scale = Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3()); + Vector3 offset = refBlack.ToScaledVector4().AsVector128().AsVector3(); + Vector3 adjustedXyz = (iccXyz * scale) + offset; + xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F)); } - else if (sourceVersion.Major == 2 && targetVersion.Major == 4) + + if (adjustTargetPcsForPerceptual) { - // Convert from Lab v2 to Lab v4. - pcs = LabV2ToLab(pcs); + Vector3 iccXyz = xyz.ToScaledVector4().AsVector128().AsVector3(); + Vector3 scale = Vector3.Divide(Vector3.One, Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3())); + Vector3 offset = -refBlack.ToScaledVector4().AsVector128().AsVector3() * scale; + Vector3 adjustedXyz = (iccXyz * scale) + offset; + xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F)); } + } - CieLab lab = CieLab.FromScaledVector4(pcs); - CieLab targetLab = pcsConverter.Convert(in lab); - pcs = targetLab.ToScaledVector4(); + Vector4 targetPcs; + switch (targetPcsType) + { + case IccColorSpaceType.CieLab: + CieLab lab = pcsConverter.Convert(in xyz); + targetPcs = lab.ToScaledVector4(); + if (adjustTargetPcsForPerceptual) + { + targetPcs = LabToLabV2(targetPcs); + } + + break; + case IccColorSpaceType.CieXyz: + CieXyz targetXyz = pcsConverter.Convert(in xyz); + targetPcs = targetXyz.ToScaledVector4(); + break; + default: + throw new ArgumentOutOfRangeException($"Target PCS {targetPcsType} not supported"); } // Convert to the target space. - return TTo.FromScaledVector4(targetConverter.Calculate(pcs)); + Vector4 targetValue = targetConverter.Calculate(targetPcs); + return TTo.FromScaledVector4(targetValue); } + // TODO: update to match workflow of the function above internal static void ConvertUsingIccProfile(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile @@ -203,6 +243,8 @@ internal static class ColorProfileConverterExtensionsIcc TTo.FromScaledVector4(pcsNormalized, destination); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector4 LabToLabV2(Vector4 input) => input * 65280F / 65535F; diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs index ea3d50ce79..c97578ee3f 100644 --- a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs +++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs @@ -20,14 +20,18 @@ internal class LutEntryCalculator : IVector4Calculator { Guard.NotNull(lut, nameof(lut)); this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix); + this.Is16Bit = false; } public LutEntryCalculator(IccLut16TagDataEntry lut) { Guard.NotNull(lut, nameof(lut)); this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix); + this.Is16Bit = true; } + internal bool Is16Bit { get; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector4 Calculate(Vector4 value) { diff --git a/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs b/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs index b67eccd03e..530459f93c 100644 --- a/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs +++ b/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs @@ -13,6 +13,8 @@ internal abstract partial class IccConverterBase { private IVector4Calculator calculator; + public bool Is16BitLutEntry => this.calculator is LutEntryCalculator { Is16Bit: true }; + /// /// Checks the profile for available conversion methods and gathers all the information's necessary for it. /// diff --git a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs b/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs index b3aa29f185..1fd76f40e0 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs @@ -1,52 +1,74 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.ColorProfiles; using Wacton.Unicolour; using Wacton.Unicolour.Icc; +using Rgb = SixLabors.ImageSharp.ColorProfiles.Rgb; namespace SixLabors.ImageSharp.Tests.ColorProfiles.Icc; public class ColorProfileConverterTests { [Theory] - [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Fogra39)] - [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Swop2006)] - [InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Fogra39)] - [InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Swop2006)] - public void CanConvertCmykIccProfiles(string sourceProfileName, string targetProfileName) + [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Fogra39, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) + [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Swop2006, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) + [InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Fogra39, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) + [InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Swop2006, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) + [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.JapanColor2011, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 16-bit vs 8-bit) + [InlineData(TestIccProfiles.JapanColor2011, TestIccProfiles.Fogra39, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 8-bit vs 16-bit) + [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Cgats21, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (different LUT versions, v2 vs v4) + [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV4, IccConversion.CmykToRgb)] // CMYK -> LAB -> RGB (different LUT versions, v2 vs v4) + [InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.Fogra39, IccConversion.RgbToCmyk)] // RGB -> LAB -> CMYK (different LUT versions, v4 vs v2) + [InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.RommRgb, IccConversion.RgbToRgb)] // RGB -> LAB -> XYZ -> RGB (different LUT elements, B-Matrix-M-CLUT-A vs B-Matrix-M) + // TODO: enable once supported by Unicolour - in the meantime, manually test known values + // [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV2, IccConversion.CmykToRgb)] // CMYK -> XYZ -> LAB -> RGB (different LUT tags, A2B vs TRC) + // [InlineData(TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39, IccConversion.RgbToCmyk)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B) + public void CanConvertCmykIccProfiles(string sourceProfile, string targetProfile, IccConversion iccConversion) { - Cmyk input = new(GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue()); + // TODO: delete after testing + float[] input = [0.734798908f, 0.887050927f, 0.476583719f, 0.547810674f]; + // float[] input = [GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue()]; + double[] expectedTargetValues = GetExpectedTargetValues(sourceProfile, targetProfile, input); ColorProfileConverter converter = new(new ColorConversionOptions { - SourceIccProfile = TestIccProfiles.GetProfile(sourceProfileName), - TargetIccProfile = TestIccProfiles.GetProfile(targetProfileName), + SourceIccProfile = TestIccProfiles.GetProfile(sourceProfile), + TargetIccProfile = TestIccProfiles.GetProfile(targetProfile) }); - Cmyk expectedTargetValues = GetExpectedTargetCmyk(sourceProfileName, targetProfileName, input); - Cmyk actualTargetValues = converter.Convert(input); + Vector4 actualTargetValues = iccConversion switch + { + IccConversion.CmykToCmyk => converter.Convert(new Cmyk(new Vector4(input))).ToScaledVector4(), + IccConversion.CmykToRgb => converter.Convert(new Cmyk(new Vector4(input))).ToScaledVector4(), + IccConversion.RgbToCmyk => converter.Convert(new Rgb(new Vector3(input))).ToScaledVector4(), + IccConversion.RgbToRgb => converter.Convert(new Rgb(new Vector3(input))).ToScaledVector4(), + _ => throw new ArgumentOutOfRangeException(nameof(iccConversion), iccConversion, null) + }; - const double tolerance = 0.0000005; - Assert.Equal(expectedTargetValues.C, actualTargetValues.C, tolerance); - Assert.Equal(expectedTargetValues.M, actualTargetValues.M, tolerance); - Assert.Equal(expectedTargetValues.Y, actualTargetValues.Y, tolerance); - Assert.Equal(expectedTargetValues.K, actualTargetValues.K, tolerance); + const double tolerance = 0.000005; + for (int i = 0; i < expectedTargetValues.Length; i++) + { + Assert.Equal(expectedTargetValues[i], actualTargetValues[i], tolerance); + } } - private static Cmyk GetExpectedTargetCmyk(string sourceProfileName, string targetProfileName, Cmyk sourceCmyk) + private static double[] GetExpectedTargetValues(string sourceProfile, string targetProfile, float[] input) { - Wacton.Unicolour.Configuration sourceConfig = TestIccProfiles.GetUnicolourConfiguration(sourceProfileName); - Wacton.Unicolour.Configuration targetConfig = TestIccProfiles.GetUnicolourConfiguration(targetProfileName); + Wacton.Unicolour.Configuration sourceConfig = TestIccProfiles.GetUnicolourConfiguration(sourceProfile); + Wacton.Unicolour.Configuration targetConfig = TestIccProfiles.GetUnicolourConfiguration(targetProfile); + + if (sourceConfig.Icc.Error != null || targetConfig.Icc.Error != null) + { + Assert.Fail("Unicolour does not support the ICC profile - test values manually in the meantime"); + } - Channels channels = new(sourceCmyk.C, sourceCmyk.M, sourceCmyk.Y, sourceCmyk.K); + Channels channels = new(input.Select(value => (double)value).ToArray()); Unicolour source = new(sourceConfig, channels); - ColourSpace pcs = sourceConfig.Icc.Profile!.Header.Pcs == "Lab " ? ColourSpace.Lab : ColourSpace.Xyz; - ColourTriplet pcsTriplet = pcs == ColourSpace.Lab ? source.Lab.Triplet : source.Xyz.Triplet; - Unicolour target = new(targetConfig, pcs, pcsTriplet.Tuple); - double[] targetCmyk = target.Icc.Values; - return new Cmyk((float)targetCmyk[0], (float)targetCmyk[1], (float)targetCmyk[2], (float)targetCmyk[3]); + Unicolour target = source.ConvertToConfiguration(targetConfig); + return target.Icc.Values; } private static float GetNormalizedRandomValue() @@ -60,4 +82,12 @@ public class ColorProfileConverterTests // Clamp the result between 0 and 1 to ensure it does not exceed the bounds. return value == 0 ? 0F : Math.Clamp((float)value + 0.0000001F, 0, 1); } + + public enum IccConversion + { + CmykToCmyk, + CmykToRgb, + RgbToCmyk, + RgbToRgb + } } diff --git a/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs b/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs index a89722dd92..742e5f9a70 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs @@ -12,9 +12,13 @@ internal static class TestIccProfiles private static readonly ConcurrentDictionary ProfileCache = new(); private static readonly ConcurrentDictionary UnicolourConfigurationCache = new(); - public const string Fogra39 = "Coated_Fogra39L_VIGC_300.icc"; - - public const string Swop2006 = "SWOP2006_Coated5v2.icc"; + public const string Fogra39 = "Coated_Fogra39L_VIGC_300.icc"; // v2 CMYK -> LAB, output, lut16 + public const string Swop2006 = "SWOP2006_Coated5v2.icc"; // v2 CMYK -> LAB, output, lut16 + public const string JapanColor2011 = "JapanColor2011Coated.icc"; // v2 CMYK -> LAB, output, lut8 + public const string Cgats21 = "CGATS21_CRPC7.icc"; // v4 CMYK -> LAB, output, lutAToB: B-CLUT-A + public const string RommRgb = "ISO22028-2_ROMM-RGB.icc"; // v4 RGB -> XYZ, colorspace, lutAToB: B-Matrix-M [only intent 0] + public const string StandardRgbV4 = "sRGB_v4_ICC_preference.icc"; // v4 RGB -> LAB, colorspace, lutAToB: B-Matrix-M-CLUT-A [only intent 0 & 1] + public const string StandardRgbV2 = "sRGB2014.icc"; // v2 RGB -> XYZ, display, TRCs public static IccProfile GetProfile(string file) => ProfileCache.GetOrAdd(file, f => new IccProfile(File.ReadAllBytes(GetFullPath(f)))); @@ -22,7 +26,7 @@ internal static class TestIccProfiles public static Wacton.Unicolour.Configuration GetUnicolourConfiguration(string file) => UnicolourConfigurationCache.GetOrAdd( file, - f => new Wacton.Unicolour.Configuration(iccConfiguration: new(GetFullPath(f), Intent.Unspecified))); + f => new Wacton.Unicolour.Configuration(iccConfiguration: new(GetFullPath(f), Intent.Unspecified, file))); private static string GetFullPath(string file) => Path.GetFullPath(Path.Combine(".", "TestDataIcc", "Profiles", file)); diff --git a/tests/ImageSharp.Tests/TestDataIcc/Profiles/CGATS21_CRPC7.icc b/tests/ImageSharp.Tests/TestDataIcc/Profiles/CGATS21_CRPC7.icc new file mode 100644 index 0000000000..c4b7084721 --- /dev/null +++ b/tests/ImageSharp.Tests/TestDataIcc/Profiles/CGATS21_CRPC7.icc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b186e6dadc38883138ccd30203b2870fb71db6451d3a5d24f9590cfabd26689 +size 3462496 diff --git a/tests/ImageSharp.Tests/TestDataIcc/Profiles/ISO22028-2_ROMM-RGB.icc b/tests/ImageSharp.Tests/TestDataIcc/Profiles/ISO22028-2_ROMM-RGB.icc new file mode 100644 index 0000000000..b06abf217a --- /dev/null +++ b/tests/ImageSharp.Tests/TestDataIcc/Profiles/ISO22028-2_ROMM-RGB.icc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96b2f2987f83e2a545e607799fbfdff43ef8158fb9b215b187c574db8f145aaf +size 864 diff --git a/tests/ImageSharp.Tests/TestDataIcc/Profiles/JapanColor2011Coated.icc b/tests/ImageSharp.Tests/TestDataIcc/Profiles/JapanColor2011Coated.icc new file mode 100644 index 0000000000..ee8dde35aa --- /dev/null +++ b/tests/ImageSharp.Tests/TestDataIcc/Profiles/JapanColor2011Coated.icc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73202ea802e57a63a0be05ed993fde8e4d51cc436120e6c47815ce785ff42916 +size 1979304 diff --git a/tests/ImageSharp.Tests/TestDataIcc/Profiles/sRGB2014.icc b/tests/ImageSharp.Tests/TestDataIcc/Profiles/sRGB2014.icc new file mode 100644 index 0000000000..46916dd8ce --- /dev/null +++ b/tests/ImageSharp.Tests/TestDataIcc/Profiles/sRGB2014.icc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:384b832de3412066743b52a75ee906b6fb9fb8d9e09e936fc2c43223815c6e0a +size 3024 diff --git a/tests/ImageSharp.Tests/TestDataIcc/Profiles/sRGB_v4_ICC_preference.icc b/tests/ImageSharp.Tests/TestDataIcc/Profiles/sRGB_v4_ICC_preference.icc new file mode 100644 index 0000000000..8ddb31b2b8 --- /dev/null +++ b/tests/ImageSharp.Tests/TestDataIcc/Profiles/sRGB_v4_ICC_preference.icc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83174717332326ddc198d9df188a4daec27b8979ba152cebbfc470c793d0bb11 +size 60960