Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#12 Use exif reader for tiff tags reading #1494

Merged

Conversation

IldarKhayrutdinov
Copy link
Contributor

@IldarKhayrutdinov IldarKhayrutdinov commented Jan 6, 2021

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

  • Remove duplicate code, use exif reader for tiff tags reading
  • Correct TrySetValue method for ExifNumberArray
  • Minor fixes for exif values and exif reader
  • Duplicate tags problem report

I was afraid to make a lot of changes to exif classes, so it turned out a little clumsy. Don't hesitate to write comments 😉
Tests are works correctly and successful.
The logic of Exif reading has not changed.
The difficulty was in supporting multi-page Tiff files, where IDF tags and their extended data (> 4 bytes) can be scattered throughout the file in no particular order, and file can be very large.

@IldarKhayrutdinov IldarKhayrutdinov marked this pull request as ready for review January 7, 2021 11:39
@IldarKhayrutdinov IldarKhayrutdinov mentioned this pull request Jan 7, 2021
17 tasks
@brianpopow brianpopow self-assigned this Jan 8, 2021
Copy link
Collaborator

@brianpopow brianpopow left a comment

Choose a reason for hiding this comment

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

@IldarKhayrutdinov thanks once again for your help.

return false;
// sometimes duplicates appear,
// can compare val.Tag == exif.Tag
if (val == exif)
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is comparing by reference, this seems like a mistake.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, this is suspicious, but it used to work like this:

Copy link
Member

@JimBobSquarePants JimBobSquarePants left a comment

Choose a reason for hiding this comment

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

Thanks! Definitely a step in the right direction! I've picked up on a few things though to revisit.

@@ -31,7 +31,7 @@ public static T[] GetArray<T>(this TiffFrameMetadata meta, ExifTag tag, bool opt
public static bool TryGetArray<T>(this TiffFrameMetadata meta, ExifTag tag, out T[] result)
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a fan of all these extension methods. ExifProfile has strong type (Get/Set)Value methods already.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

right, now can remove or simplify these methods

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@JimBobSquarePants I got rid of some methods, but most remained, they are useful as wrappers for ExifProfile methods - allow you to cast types for properties

Copy link
Member

@JimBobSquarePants JimBobSquarePants Jan 26, 2021

Choose a reason for hiding this comment

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

allow you to cast types for properties

I don't understand. The values returned by the existing method are already strong typed to the correct type without the need for internal calls and casting.

For example. This method is called upstream by the following in TiffDecoderMetadataCreator.

byte[] buf = frame.GetArray<byte>(ExifTag.XMP, true);
if (buf != null)
{
    tiffMetadata.XmpProfile = buf;
}

However, this could easily have been.

IExifValue<byte[]> xmp = frame.ExifProfile.GetValue(ExifTag.XMP);
if (xmp != null)
{
    tiffMetadata.XmpProfile = xmp.Value;
}

which uses the existing methods removing any code duplication and is better for someone reading the code since it indicates exactly where the metadata is coming from.

I do wish that ExifProfile.GetValue was ExifProfile.TryGetValue but we can't fix that without introducing a breaking change (I could be convinced to do that though).

Copy link
Member

Choose a reason for hiding this comment

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

The same duplication is happening in the resolution getter/setter code.

We have existing methods in jpeg GetExifResolutionValue that demonstrate a much more simple approach using existing APIs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@JimBobSquarePants yes, you are right, it can be simplified even more, I did it

Copy link
Member

Choose a reason for hiding this comment

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

Ace... I'll give it another review tonight. 👍


private uint gpsOffset = 0;

public ExifReader(bool isBigEndian, Stream stream)
Copy link
Member

Choose a reason for hiding this comment

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

I think these parameters should be reversed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, it looks more logical

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

}

public ExifReader(byte[] exifData) =>
this.data = new MemoryStream(exifData ?? throw new ArgumentNullException(nameof(exifData)));
Copy link
Member

Choose a reason for hiding this comment

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

What about endianness?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

endianness will readed later:

this.isBigEndian = this.ReadUInt16() != 0x4949;

the class turned out to be a little universal, it can make 2 inheritor for tiff and exif, what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@JimBobSquarePants it seems, worth to make 2 inheritor for tiff and exif, it will look better

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@JimBobSquarePants please review again, I split the classes and they seems to be looking better now

public override IExifValue DeepClone() => new ExifNumberArray(this);

private bool SetSingle<T>(T value, Func<T, Number> converter)
Copy link
Member

Choose a reason for hiding this comment

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

TrySetSingle
TrySetArray

Copy link
Contributor Author

@IldarKhayrutdinov IldarKhayrutdinov Jan 25, 2021

Choose a reason for hiding this comment

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

These methods return the value for simplicity case construction, they will always return true:

return this.SetSingle(val, v => (Number)v);

@IldarKhayrutdinov
Copy link
Contributor Author

IldarKhayrutdinov commented Jan 12, 2021

@JimBobSquarePants @brianpopow I can't reply to your comments, maybe it's because of Assignees

@brianpopow brianpopow removed their assignment Jan 12, 2021
@brianpopow
Copy link
Collaborator

brianpopow commented Jan 12, 2021

@JimBobSquarePants @brianpopow I can't reply to your comments, maybe it's because of Assignees

i dont know why that is. I have removed myself from assignees. Can you try again @IldarKhayrutdinov ?

@IldarKhayrutdinov IldarKhayrutdinov marked this pull request as draft January 12, 2021 19:28
@IldarKhayrutdinov IldarKhayrutdinov marked this pull request as ready for review January 12, 2021 19:29
@IldarKhayrutdinov
Copy link
Contributor Author

IldarKhayrutdinov commented Jan 12, 2021

strange, it doesn't work

/// <summary>
/// Gets the RowsPerStrip exif tag.
/// </summary>
public static ExifTag<Number> RowsPerStrip { get; } = new ExifTag<Number>(ExifTagValue.RowsPerStrip);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -54,15 +54,6 @@ public static ImageMetadata Create(List<TiffFrameMetadata> frames, bool ignoreMe
}
}

if (coreMetadata.ExifProfile == null)
{
byte[] buf = frame.GetArray<byte>(ExifTag.SubIFDOffset, true);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

now SubIFD and subGps are read to common TiffFrameMetaData.FrameTags list


// used for sequential read big values (actual for multiframe big files)
// todo: different tags can link to the same data (stream offset) - investigate
private readonly SortedList<uint, Action> lazyLoaders = new SortedList<uint, Action>(new DuplicateKeyComparer<uint>());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

DuplicateKeyComparer used for possiblity add a duplicate offsets (but tags don't duplicate)

{
return false;
// sometimes duplicates appear,
Copy link
Contributor Author

@IldarKhayrutdinov IldarKhayrutdinov Jan 7, 2021

Choose a reason for hiding this comment

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

note, I added a reproduction of the problem with duplicates, these tests work successfully:

  1. Assert.Equal(2, image.Metadata.ExifProfile.Values.Count(v => (ushort)v.Tag == 59932));

  2. Assert.Equal(2, profile.Values.Count(v => (ExifTagValue)(ushort)v.Tag == ExifTagValue.DateTime));

  3. Assert.Equal(2, profile.Values.Count(v => (ushort)v.Tag == 59932));

@@ -201,10 +203,14 @@ public void SetValue(TestImageWriteFormat imageFormat)
IExifValue<Rational[]> latitude = image.Metadata.ExifProfile.GetValue(ExifTag.GPSLatitude);
Assert.Equal(expectedLatitude, latitude.Value);

// todo: duplicate tags
Assert.Equal(2, image.Metadata.ExifProfile.Values.Count(v => (ushort)v.Tag == 59932));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: this PR doesn't affected, it was earlier

@IldarKhayrutdinov
Copy link
Contributor Author

strange, it doesn't work

happened!

@JimBobSquarePants
Copy link
Member

Did you mean to close this? I thought it just needed a little work.

@IldarKhayrutdinov
Copy link
Contributor Author

Did you mean to close this? I thought it just needed a little work.

sorry, I myself did not understand how it happened

@IldarKhayrutdinov
Copy link
Contributor Author

@JimBobSquarePants @brianpopow I pushed some small improvements, please review. Are you changes requested?

{
if (this.ReadHeader())
var headerBytes = new byte[2];
Copy link
Member

Choose a reason for hiding this comment

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

Stackalloc span?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sorry, do you mean doing new Span(stackalloc byte[2]) ?

return null;
}
}
protected override void RegisterExtLoader(uint offset, Action reader) => throw new NotImplementedException();
Copy link
Member

Choose a reason for hiding this comment

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

I'm assuming this is a feature waiting to be implemented?

Copy link
Contributor Author

@IldarKhayrutdinov IldarKhayrutdinov Jan 26, 2021

Choose a reason for hiding this comment

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

The HeaderReader class use one time to read only header. I changed exception type to NotSupported.
I tried to make fewer changes within the PR.
It was possible to move the stream reading methods into separate classes (as was in tiff), this would remove HeaderReader class and slightly simplify the exif/tiff readers.

Copy link
Collaborator

@brianpopow brianpopow left a comment

Choose a reason for hiding this comment

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

I think this can be merged now, but i leave the final verdict to @JimBobSquarePants

@JimBobSquarePants
Copy link
Member

@brianpopow if you’re happy, I’m happy.

@JimBobSquarePants JimBobSquarePants merged commit 1498dfa into SixLabors:tiff-format Jan 29, 2021
@JimBobSquarePants
Copy link
Member

Thanks @IldarKhayrutdinov !!

@IldarKhayrutdinov IldarKhayrutdinov deleted the tiff-format branch January 29, 2021 20:14
@IldarKhayrutdinov
Copy link
Contributor Author

It was crazy PR 🙃

JimBobSquarePants added a commit that referenced this pull request Feb 17, 2021
#12 Use exif reader for tiff tags reading
JimBobSquarePants added a commit that referenced this pull request Feb 17, 2021
#12 Use exif reader for tiff tags reading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants