Skip to content

Commit 41e23b8

Browse files
Merge pull request #588 from JBildstein/icc-improvements
ICC Improvements
2 parents 90272b4 + df7fa40 commit 41e23b8

File tree

7 files changed

+226
-65
lines changed

7 files changed

+226
-65
lines changed

src/ImageSharp/Formats/Jpeg/GolangPort/GolangJpegDecoderCore.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,11 @@ private void InitDerivedMetaDataProperties()
450450
this.MetaData.VerticalResolution = verticalValue;
451451
}
452452
}
453+
454+
if (this.MetaData.IccProfile?.CheckIsValid() == false)
455+
{
456+
this.MetaData.IccProfile = null;
457+
}
453458
}
454459

455460
/// <summary>

src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ public Image<TPixel> Decode<TPixel>(Stream stream)
196196
where TPixel : struct, IPixel<TPixel>
197197
{
198198
this.ParseStream(stream);
199-
this.AssignResolution();
199+
this.InitDerivedMetaDataProperties();
200200
return this.PostProcessIntoImage<TPixel>();
201201
}
202202

@@ -207,7 +207,7 @@ public Image<TPixel> Decode<TPixel>(Stream stream)
207207
public IImageInfo Identify(Stream stream)
208208
{
209209
this.ParseStream(stream, true);
210-
this.AssignResolution();
210+
this.InitDerivedMetaDataProperties();
211211
return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.MetaData);
212212
}
213213

@@ -395,9 +395,9 @@ private JpegColorSpace DeduceJpegColorSpace()
395395
}
396396

397397
/// <summary>
398-
/// Assigns the horizontal and vertical resolution to the image if it has a JFIF header or EXIF metadata.
398+
/// Assigns derived metadata properties to <see cref="MetaData"/>, eg. horizontal and vertical resolution if it has a JFIF header.
399399
/// </summary>
400-
private void AssignResolution()
400+
private void InitDerivedMetaDataProperties()
401401
{
402402
if (this.jFif.XDensity > 0 && this.jFif.YDensity > 0)
403403
{
@@ -420,6 +420,11 @@ private void AssignResolution()
420420
this.MetaData.VerticalResolution = verticalValue;
421421
}
422422
}
423+
424+
if (this.MetaData.IccProfile?.CheckIsValid() == false)
425+
{
426+
this.MetaData.IccProfile = null;
427+
}
423428
}
424429

425430
/// <summary>

src/ImageSharp/MetaData/Profiles/ICC/DataReader/IccDataReader.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Six Labors and contributors.
22
// Licensed under the Apache License, Version 2.0.
33

4-
using System;
54
using System.Text;
65

76
namespace SixLabors.ImageSharp.MetaData.Profiles.Icc
@@ -11,7 +10,6 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Icc
1110
/// </summary>
1211
internal sealed partial class IccDataReader
1312
{
14-
private static readonly bool IsLittleEndian = BitConverter.IsLittleEndian;
1513
private static readonly Encoding AsciiEncoding = Encoding.GetEncoding("ASCII");
1614

1715
/// <summary>
@@ -34,6 +32,14 @@ public IccDataReader(byte[] data)
3432
this.data = data;
3533
}
3634

35+
/// <summary>
36+
/// Gets the length in bytes of the raw data
37+
/// </summary>
38+
public int DataLength
39+
{
40+
get { return this.data.Length; }
41+
}
42+
3743
/// <summary>
3844
/// Sets the reading position to the given value
3945
/// </summary>

src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,12 @@ public IccProfile(byte[] data)
5252
/// by making a copy from another ICC profile.
5353
/// </summary>
5454
/// <param name="other">The other ICC profile, where the clone should be made from.</param>
55-
/// <exception cref="System.ArgumentNullException"><paramref name="other"/> is null.</exception>>
55+
/// <exception cref="ArgumentNullException"><paramref name="other"/> is null.</exception>>
5656
public IccProfile(IccProfile other)
5757
{
5858
Guard.NotNull(other, nameof(other));
5959

60-
// TODO: Do we need to copy anything else?
61-
if (other.data != null)
62-
{
63-
this.data = new byte[other.data.Length];
64-
Buffer.BlockCopy(other.data, 0, this.data, 0, other.data.Length);
65-
}
60+
this.data = other.ToByteArray();
6661
}
6762

6863
/// <summary>
@@ -108,7 +103,7 @@ public List<IccTagDataEntry> Entries
108103
#if !NETSTANDARD1_1
109104

110105
/// <summary>
111-
/// Calculates the MD5 hash value of an ICC profile header
106+
/// Calculates the MD5 hash value of an ICC profile
112107
/// </summary>
113108
/// <param name="data">The data of which to calculate the hash value</param>
114109
/// <returns>The calculated hash</returns>
@@ -117,22 +112,38 @@ public static IccProfileId CalculateHash(byte[] data)
117112
Guard.NotNull(data, nameof(data));
118113
Guard.IsTrue(data.Length >= 128, nameof(data), "Data length must be at least 128 to be a valid profile header");
119114

120-
byte[] header = new byte[128];
121-
Buffer.BlockCopy(data, 0, header, 0, 128);
115+
const int profileFlagPos = 44;
116+
const int renderingIntentPos = 64;
117+
const int profileIdPos = 84;
118+
119+
// need to copy some values because they need to be zero for the hashing
120+
byte[] temp = new byte[24];
121+
Buffer.BlockCopy(data, profileFlagPos, temp, 0, 4);
122+
Buffer.BlockCopy(data, renderingIntentPos, temp, 4, 4);
123+
Buffer.BlockCopy(data, profileIdPos, temp, 8, 16);
122124

123125
using (var md5 = MD5.Create())
124126
{
125-
// Zero out some values
126-
Array.Clear(header, 44, 4); // Profile flags
127-
Array.Clear(header, 64, 4); // Rendering Intent
128-
Array.Clear(header, 84, 16); // Profile ID
129-
130-
// Calculate hash
131-
byte[] hash = md5.ComputeHash(data);
132-
133-
// Read values from hash
134-
var reader = new IccDataReader(hash);
135-
return reader.ReadProfileId();
127+
try
128+
{
129+
// Zero out some values
130+
Array.Clear(data, profileFlagPos, 4);
131+
Array.Clear(data, renderingIntentPos, 4);
132+
Array.Clear(data, profileIdPos, 16);
133+
134+
// Calculate hash
135+
byte[] hash = md5.ComputeHash(data);
136+
137+
// Read values from hash
138+
var reader = new IccDataReader(hash);
139+
return reader.ReadProfileId();
140+
}
141+
finally
142+
{
143+
Buffer.BlockCopy(temp, 0, data, profileFlagPos, 4);
144+
Buffer.BlockCopy(temp, 4, data, renderingIntentPos, 4);
145+
Buffer.BlockCopy(temp, 8, data, profileIdPos, 16);
146+
}
136147
}
137148
}
138149

@@ -149,14 +160,37 @@ public void Extend(byte[] bytes)
149160
Buffer.BlockCopy(bytes, 0, this.data, currentLength, bytes.Length);
150161
}
151162

163+
/// <summary>
164+
/// Checks for signs of a corrupt profile.
165+
/// </summary>
166+
/// <remarks>This is not an absolute proof of validity but should weed out most corrupt data.</remarks>
167+
/// <returns>True if the profile is valid; False otherwise</returns>
168+
public bool CheckIsValid()
169+
{
170+
return Enum.IsDefined(typeof(IccColorSpaceType), this.Header.DataColorSpace) &&
171+
Enum.IsDefined(typeof(IccColorSpaceType), this.Header.ProfileConnectionSpace) &&
172+
Enum.IsDefined(typeof(IccRenderingIntent), this.Header.RenderingIntent) &&
173+
this.Header.Size >= 128 &&
174+
this.Header.Size < 50_000_000; // it's unlikely there is a profile bigger than 50MB
175+
}
176+
152177
/// <summary>
153178
/// Converts this instance to a byte array.
154179
/// </summary>
155180
/// <returns>The <see cref="T:byte[]"/></returns>
156181
public byte[] ToByteArray()
157182
{
158-
var writer = new IccWriter();
159-
return writer.Write(this);
183+
if (this.data != null)
184+
{
185+
byte[] copy = new byte[this.data.Length];
186+
Buffer.BlockCopy(this.data, 0, copy, 0, copy.Length);
187+
return copy;
188+
}
189+
else
190+
{
191+
var writer = new IccWriter();
192+
return writer.Write(this);
193+
}
160194
}
161195

162196
private void InitializeHeader()

src/ImageSharp/MetaData/Profiles/ICC/IccReader.cs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,45 +84,66 @@ private IccProfileHeader ReadHeader(IccDataReader reader)
8484
private IccTagDataEntry[] ReadTagData(IccDataReader reader)
8585
{
8686
IccTagTableEntry[] tagTable = this.ReadTagTable(reader);
87-
var entries = new IccTagDataEntry[tagTable.Length];
87+
var entries = new List<IccTagDataEntry>(tagTable.Length);
8888
var store = new Dictionary<uint, IccTagDataEntry>();
89-
for (int i = 0; i < tagTable.Length; i++)
89+
90+
foreach (IccTagTableEntry tag in tagTable)
9091
{
9192
IccTagDataEntry entry;
92-
uint offset = tagTable[i].Offset;
93-
if (store.ContainsKey(offset))
93+
if (store.ContainsKey(tag.Offset))
9494
{
95-
entry = store[offset];
95+
entry = store[tag.Offset];
9696
}
9797
else
9898
{
99-
entry = reader.ReadTagDataEntry(tagTable[i]);
100-
store.Add(offset, entry);
99+
try
100+
{
101+
entry = reader.ReadTagDataEntry(tag);
102+
}
103+
catch
104+
{
105+
// Ignore tags that could not be read
106+
continue;
107+
}
108+
109+
store.Add(tag.Offset, entry);
101110
}
102111

103-
entry.TagSignature = tagTable[i].Signature;
104-
entries[i] = entry;
112+
entry.TagSignature = tag.Signature;
113+
entries.Add(entry);
105114
}
106115

107-
return entries;
116+
return entries.ToArray();
108117
}
109118

110119
private IccTagTableEntry[] ReadTagTable(IccDataReader reader)
111120
{
112121
reader.SetIndex(128); // An ICC header is 128 bytes long
113122

114123
uint tagCount = reader.ReadUInt32();
115-
var table = new IccTagTableEntry[tagCount];
116124

125+
// Prevent creating huge arrays because of corrupt profiles.
126+
// A normal profile usually has 5-15 entries
127+
if (tagCount > 100)
128+
{
129+
return new IccTagTableEntry[0];
130+
}
131+
132+
var table = new List<IccTagTableEntry>((int)tagCount);
117133
for (int i = 0; i < tagCount; i++)
118134
{
119135
uint tagSignature = reader.ReadUInt32();
120136
uint tagOffset = reader.ReadUInt32();
121137
uint tagSize = reader.ReadUInt32();
122-
table[i] = new IccTagTableEntry((IccProfileTag)tagSignature, tagOffset, tagSize);
138+
139+
// Exclude entries that have nonsense values and could cause exceptions further on
140+
if (tagOffset < reader.DataLength && tagSize < reader.DataLength - 128)
141+
{
142+
table.Add(new IccTagTableEntry((IccProfileTag)tagSignature, tagOffset, tagSize));
143+
}
123144
}
124145

125-
return table;
146+
return table.ToArray();
126147
}
127148
}
128149
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) Six Labors and contributors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using SixLabors.ImageSharp.MetaData.Profiles.Icc;
6+
using Xunit;
7+
8+
namespace SixLabors.ImageSharp.Tests.Icc
9+
{
10+
public class IccProfileTests
11+
{
12+
13+
#if !NETSTANDARD1_1
14+
15+
[Theory]
16+
[MemberData(nameof(IccTestDataProfiles.ProfileIdTestData), MemberType = typeof(IccTestDataProfiles))]
17+
public void CalculateHash_WithByteArray_CalculatesProfileHash(byte[] data, IccProfileId expected)
18+
{
19+
IccProfileId result = IccProfile.CalculateHash(data);
20+
21+
Assert.Equal(expected, result);
22+
}
23+
24+
[Fact]
25+
public void CalculateHash_WithByteArray_DoesNotModifyData()
26+
{
27+
byte[] data = IccTestDataProfiles.Profile_Random_Array;
28+
byte[] copy = new byte[data.Length];
29+
Buffer.BlockCopy(data, 0, copy, 0, data.Length);
30+
31+
IccProfileId result = IccProfile.CalculateHash(data);
32+
33+
Assert.Equal(data, copy);
34+
}
35+
36+
#endif
37+
38+
[Theory]
39+
[MemberData(nameof(IccTestDataProfiles.ProfileValidityTestData), MemberType = typeof(IccTestDataProfiles))]
40+
public void CheckIsValid_WithProfiles_ReturnsValidity(byte[] data, bool expected)
41+
{
42+
var profile = new IccProfile(data);
43+
44+
bool result = profile.CheckIsValid();
45+
46+
Assert.Equal(expected, result);
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)