Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,11 @@ public void ParseStream(Stream stream, bool metadataOnly = false)
}

this.InitDerivedMetaDataProperties();

if (this.MetaData.IccProfile?.CheckIsValid() == false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this to InitDerivedMetaDataProperties

{
this.MetaData.IccProfile = null;
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ public void ParseStream(Stream stream, bool metadataOnly = false)
{
// It's highly unlikely that APPn related data will be found after the SOS marker
// We should have gathered everything we need by now.
return;
break;
}

case JpegConstants.Markers.DHT:
Expand Down Expand Up @@ -345,6 +345,11 @@ public void ParseStream(Stream stream, bool metadataOnly = false)
// Read on.
fileMarker = FindNextFileMarker(this.markerBuffer, this.InputStream);
}

if (this.MetaData.IccProfile?.CheckIsValid() == false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this to AssignResolution and rename that to InitDerivedMetaDataProperties, The return above prevents us reading more than we need to when we are identifying images.

Copy link
Contributor Author

@ABildstein ABildstein May 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially wanted to put it there, but it's only called from Decode and Identify and not in ParseStream. In the go port it's called at the end of the ParseStream method (and by extension in Decode and Identify).
Should we move AssignResolution there as well? I changed the return you mentioned to a break so things after the while loop will be executed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both Decode and Identify call ParseStream in both decoders. We want to return at that point in PdfJs decoder to avoid reading the entire SOS segment when we don’t need to. So using break slows us down.

I can move the method if you like? Save you the bother.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes they do, but ParseStream is public so it can be called directly, that's the only reason I wanted to do it there.
Sorry about the return thing, I thought break behaves differently at that point. We could use a
goto to jump out of the while (it's evil, I know :P)

and no worries, it's a small change, I can do it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 It's only public as we use it for tests. The class is internal so not available to consumers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok great, then there's no reason to change anything in that regard. Will push changes in a couple minutes.

{
this.MetaData.IccProfile = null;
}
}

/// <inheritdoc/>
Expand Down
10 changes: 8 additions & 2 deletions src/ImageSharp/MetaData/Profiles/ICC/DataReader/IccDataReader.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Text;

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

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

/// <summary>
/// Gets the length in bytes of the raw data
/// </summary>
public int DataLength
{
get { return this.data.Length; }
}

/// <summary>
/// Sets the reading position to the given value
/// </summary>
Expand Down
80 changes: 57 additions & 23 deletions src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,12 @@ public IccProfile(byte[] data)
/// by making a copy from another ICC profile.
/// </summary>
/// <param name="other">The other ICC profile, where the clone should be made from.</param>
/// <exception cref="System.ArgumentNullException"><paramref name="other"/> is null.</exception>>
/// <exception cref="ArgumentNullException"><paramref name="other"/> is null.</exception>>
public IccProfile(IccProfile other)
{
Guard.NotNull(other, nameof(other));

// TODO: Do we need to copy anything else?
if (other.data != null)
{
this.data = new byte[other.data.Length];
Buffer.BlockCopy(other.data, 0, this.data, 0, other.data.Length);
}
this.data = other.ToByteArray();
}

/// <summary>
Expand Down Expand Up @@ -108,7 +103,7 @@ public List<IccTagDataEntry> Entries
#if !NETSTANDARD1_1

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

byte[] header = new byte[128];
Buffer.BlockCopy(data, 0, header, 0, 128);
const int profileFlagPos = 44;
const int renderingIntentPos = 64;
const int profileIdPos = 84;

// need to copy some values because they need to be zero for the hashing
byte[] temp = new byte[24];
Buffer.BlockCopy(data, profileFlagPos, temp, 0, 4);
Buffer.BlockCopy(data, renderingIntentPos, temp, 4, 4);
Buffer.BlockCopy(data, profileIdPos, temp, 8, 16);

using (var md5 = MD5.Create())
{
// Zero out some values
Array.Clear(header, 44, 4); // Profile flags
Array.Clear(header, 64, 4); // Rendering Intent
Array.Clear(header, 84, 16); // Profile ID

// Calculate hash
byte[] hash = md5.ComputeHash(data);

// Read values from hash
var reader = new IccDataReader(hash);
return reader.ReadProfileId();
try
{
// Zero out some values
Array.Clear(data, profileFlagPos, 4);
Array.Clear(data, renderingIntentPos, 4);
Array.Clear(data, profileIdPos, 16);

// Calculate hash
byte[] hash = md5.ComputeHash(data);

// Read values from hash
var reader = new IccDataReader(hash);
return reader.ReadProfileId();
}
finally
{
Buffer.BlockCopy(temp, 0, data, profileFlagPos, 4);
Buffer.BlockCopy(temp, 4, data, renderingIntentPos, 4);
Buffer.BlockCopy(temp, 8, data, profileIdPos, 16);
}
}
}

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

/// <summary>
/// Checks for signs of a corrupt profile.
/// </summary>
/// <remarks>This is not an absolute proof of validity but should weed out most corrupt data.</remarks>
/// <returns>True if the profile is valid; False otherwise</returns>
public bool CheckIsValid()
{
return Enum.IsDefined(typeof(IccColorSpaceType), this.Header.DataColorSpace) &&
Enum.IsDefined(typeof(IccColorSpaceType), this.Header.ProfileConnectionSpace) &&
Enum.IsDefined(typeof(IccRenderingIntent), this.Header.RenderingIntent) &&
this.Header.Size >= 128 &&
this.Header.Size < 50_000_000; // it's unlikely there is a profile bigger than 50MB
}

/// <summary>
/// Converts this instance to a byte array.
/// </summary>
/// <returns>The <see cref="T:byte[]"/></returns>
public byte[] ToByteArray()
{
var writer = new IccWriter();
return writer.Write(this);
if (this.data != null)
{
byte[] copy = new byte[this.data.Length];
Buffer.BlockCopy(this.data, 0, copy, 0, copy.Length);
return copy;
}
else
{
var writer = new IccWriter();
return writer.Write(this);
}
}

private void InitializeHeader()
Expand Down
47 changes: 34 additions & 13 deletions src/ImageSharp/MetaData/Profiles/ICC/IccReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,45 +84,66 @@ private IccProfileHeader ReadHeader(IccDataReader reader)
private IccTagDataEntry[] ReadTagData(IccDataReader reader)
{
IccTagTableEntry[] tagTable = this.ReadTagTable(reader);
var entries = new IccTagDataEntry[tagTable.Length];
var entries = new List<IccTagDataEntry>(tagTable.Length);
var store = new Dictionary<uint, IccTagDataEntry>();
for (int i = 0; i < tagTable.Length; i++)

foreach (IccTagTableEntry tag in tagTable)
{
IccTagDataEntry entry;
uint offset = tagTable[i].Offset;
if (store.ContainsKey(offset))
if (store.ContainsKey(tag.Offset))
{
entry = store[offset];
entry = store[tag.Offset];
}
else
{
entry = reader.ReadTagDataEntry(tagTable[i]);
store.Add(offset, entry);
try
{
entry = reader.ReadTagDataEntry(tag);
}
catch
{
// Ignore tags that could not be read
continue;
}

store.Add(tag.Offset, entry);
}

entry.TagSignature = tagTable[i].Signature;
entries[i] = entry;
entry.TagSignature = tag.Signature;
entries.Add(entry);
}

return entries;
return entries.ToArray();
}

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

uint tagCount = reader.ReadUInt32();
var table = new IccTagTableEntry[tagCount];

// Prevent creating huge arrays because of corrupt profiles.
// A normal profile usually has 5-15 entries
if (tagCount > 100)
{
return new IccTagTableEntry[0];
}

var table = new List<IccTagTableEntry>((int)tagCount);
for (int i = 0; i < tagCount; i++)
{
uint tagSignature = reader.ReadUInt32();
uint tagOffset = reader.ReadUInt32();
uint tagSize = reader.ReadUInt32();
table[i] = new IccTagTableEntry((IccProfileTag)tagSignature, tagOffset, tagSize);

// Exclude entries that have nonsense values and could cause exceptions further on
if (tagOffset < reader.DataLength && tagSize < reader.DataLength - 128)
{
table.Add(new IccTagTableEntry((IccProfileTag)tagSignature, tagOffset, tagSize));
}
}

return table;
return table.ToArray();
}
}
}
49 changes: 49 additions & 0 deletions tests/ImageSharp.Tests/MetaData/Profiles/ICC/IccProfileTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

using System;
using SixLabors.ImageSharp.MetaData.Profiles.Icc;
using Xunit;

namespace SixLabors.ImageSharp.Tests.Icc
{
public class IccProfileTests
{

#if !NETSTANDARD1_1

[Theory]
[MemberData(nameof(IccTestDataProfiles.ProfileIdTestData), MemberType = typeof(IccTestDataProfiles))]
public void CalculateHash_WithByteArray_CalculatesProfileHash(byte[] data, IccProfileId expected)
{
IccProfileId result = IccProfile.CalculateHash(data);

Assert.Equal(expected, result);
}

[Fact]
public void CalculateHash_WithByteArray_DoesNotModifyData()
{
byte[] data = IccTestDataProfiles.Profile_Random_Array;
byte[] copy = new byte[data.Length];
Buffer.BlockCopy(data, 0, copy, 0, data.Length);

IccProfileId result = IccProfile.CalculateHash(data);

Assert.Equal(data, copy);
}

#endif

[Theory]
[MemberData(nameof(IccTestDataProfiles.ProfileValidityTestData), MemberType = typeof(IccTestDataProfiles))]
public void CheckIsValid_WithProfiles_ReturnsValidity(byte[] data, bool expected)
{
var profile = new IccProfile(data);

bool result = profile.CheckIsValid();

Assert.Equal(expected, result);
}
}
}
Loading