/// <summary>
        /// Reads atom data from <paramref name="stream"/>, invoking <paramref name="handler"/> for each atom encountered.
        /// </summary>
        /// <param name="stream">The stream to read atoms from.</param>
        /// <param name="handler">A callback function to handle each atom.</param>
        /// <param name="stopByBytes">The maximum number of bytes to process before discontinuing.</param>
        public static void ProcessAtoms([NotNull] Stream stream, [NotNull] Action <AtomCallbackArgs> handler, long stopByBytes = -1)
        {
            var reader = new SequentialStreamReader(stream);

            var seriesStartPos = stream.Position;

            while (stopByBytes == -1 || stream.Position < seriesStartPos + stopByBytes)
            {
                var atomStartPos = stream.Position;

                // Length of the atom's data, in bytes, including size bytes
                long atomSize;
                try
                {
                    atomSize = reader.GetUInt32();
                }
                catch (IOException)
                {
                    // TODO don't use exception to trap end of stream
                    return;
                }

                // Typically four ASCII characters, but may be non-printable.
                // By convention, lowercase 4CCs are reserved by Apple.
                var atomType = reader.GetUInt32();

                if (atomSize == 1)
                {
                    // Size doesn't fit in 32 bits so read the 64 bit size here
                    atomSize = checked ((long)reader.GetUInt64());
                }
                else
                {
                    Debug.Assert(atomSize >= 8, "Atom should be at least 8 bytes long");
                }

                var args = new AtomCallbackArgs(atomType, atomSize, stream, atomStartPos, reader);

                handler(args);

                if (args.Cancel)
                {
                    return;
                }

                if (atomSize == 0)
                {
                    return;
                }

                var toSkip = atomStartPos + atomSize - stream.Position;

                if (toSkip < 0)
                {
                    throw new Exception("Handler moved stream beyond end of atom");
                }

                reader.TrySkip(toSkip);
            }
        }
 public AtomCallbackArgs(uint type, long size, Stream stream, long startPosition, SequentialStreamReader reader)
 {
     Type          = type;
     Size          = size;
     Stream        = stream;
     StartPosition = startPosition;
     Reader        = reader;
 }
        protected override TgaFooter Extract(Stream stream, int offset)
        {
            var reader = new SequentialStreamReader(stream, isMotorolaByteOrder: false);

            return(new TgaFooter(
                       extOffset: reader.GetInt32(),
                       devOffset: reader.GetInt32(),
                       signature: reader.GetBytes(_footerSignature.Length)));
        }
        protected override TgaTagInfo[] Extract(Stream stream, int _)
        {
            var reader = new SequentialStreamReader(stream, isMotorolaByteOrder: false);
            var count  = reader.GetUInt16();
            var tags   = new TgaTagInfo[count];

            for (int i = 0; i < count; i++)
            {
                tags[i] = GetTag(reader);
            }
            return(tags);
        /// <summary>
        /// Reads atom data from <paramref name="stream"/>, invoking <paramref name="handler"/> for each atom encountered.
        /// </summary>
        /// <param name="stream">The stream to read atoms from.</param>
        /// <param name="handler">A callback function to handle each atom.</param>
        /// <param name="stopByBytes">The maximum number of bytes to process before discontinuing.</param>
        public static void ProcessAtoms([NotNull] Stream stream, [NotNull] Action <AtomCallbackArgs> handler, long stopByBytes = -1)
        {
            var reader = new SequentialStreamReader(stream);

            var seriesStartPos = stream.Position;

            while (stopByBytes == -1 || stream.Position < seriesStartPos + stopByBytes)
            {
                var atomStartPos = stream.Position;

                try
                {
                    // Check if the end of the stream is closer then 8 bytes to current position (Length of the atom's data + atom type)
                    if (reader.IsCloserToEnd(8))
                    {
                        return;
                    }

                    // Length of the atom's data, in bytes, including size bytes
                    long atomSize = reader.GetUInt32();

                    // Typically four ASCII characters, but may be non-printable.
                    // By convention, lowercase 4CCs are reserved by Apple.
                    var atomType = reader.GetUInt32();

                    if (atomSize == 1)
                    {
                        // Check if the end of the stream is closer then 8 bytes
                        if (reader.IsCloserToEnd(8))
                        {
                            return;
                        }

                        // Size doesn't fit in 32 bits so read the 64 bit size here
                        atomSize = checked ((long)reader.GetUInt64());
                    }
                    else if (atomSize < 8)
                    {
                        // Atom should be at least 8 bytes long
                        return;
                    }

                    var args = new AtomCallbackArgs(atomType, atomSize, stream, atomStartPos, reader);

                    handler(args);

                    if (args.Cancel)
                    {
                        return;
                    }

                    if (atomSize == 0)
                    {
                        return;
                    }

                    var toSkip = atomStartPos + atomSize - stream.Position;

                    if (toSkip < 0)
                    {
                        throw new Exception("Handler moved stream beyond end of atom");
                    }

                    // To avoid exception handling we can check if needed number of bytes are available
                    if (!reader.IsCloserToEnd(toSkip))
                    {
                        reader.TrySkip(toSkip);
                    }
                }
                catch (IOException)
                {
                    // Exception trapping is used when stream doesn't support stream length method only
                    return;
                }
            }
        }
        protected override void Populate(Stream stream, int offset, TgaExtensionDirectory directory)
        {
            var reader = new SequentialStreamReader(stream, isMotorolaByteOrder: false);

            var size = reader.GetUInt16();

            if (size < ExtensionSize)
            {
                throw new ImageProcessingException("Invalid TGA extension size");
            }
            var authorName = GetString(41);

            if (authorName.Length > 0)
            {
                directory.Set(TgaExtensionDirectory.TagAuthorName, authorName);
            }
            var comments = GetString(324);

            if (comments.Length > 0)
            {
                directory.Set(TgaExtensionDirectory.TagComments, comments);
            }
            if (TryGetDateTime(out var dateTime))
            {
                directory.Set(TgaExtensionDirectory.TagDateTime, dateTime);
            }
            var jobName = GetString(41);

            if (jobName.Length > 0)
            {
                directory.Set(TgaExtensionDirectory.TagJobName, jobName);
            }
            if (TryGetTimeSpan(out var jobTime))
            {
                directory.Set(TgaExtensionDirectory.TagJobTime, jobTime);
            }
            var softwareName = GetString(41);

            if (softwareName.Length > 0)
            {
                directory.Set(TgaExtensionDirectory.TagSoftwareName, softwareName);
            }
            var softwareVersion = GetSoftwareVersion(softwareName);

            if (softwareVersion.Length > 0)
            {
                directory.Set(TgaExtensionDirectory.TagSoftwareVersion, softwareVersion);
            }
            var keyColor = reader.GetUInt32();

            if (keyColor != 0)
            {
                directory.Set(TgaExtensionDirectory.TagKeyColor, keyColor);
            }
            if (TryGetRational(out var aspectRatio))
            {
                directory.Set(TgaExtensionDirectory.TagAspectRatio, aspectRatio);
            }
            if (TryGetRational(out var gamma))
            {
                directory.Set(TgaExtensionDirectory.TagGamma, gamma);
            }
            var colorCorrectionOffset = reader.GetInt32();

            if (colorCorrectionOffset != 0)
            {
                directory.Set(TgaExtensionDirectory.TagColorCorrectionOffset, colorCorrectionOffset);
            }
            var thumbnailOffset = reader.GetInt32();

            if (thumbnailOffset != 0)
            {
                directory.Set(TgaExtensionDirectory.TagThumbnailOffset, thumbnailOffset);
            }
            var scanLineOffset = reader.GetInt32();

            if (scanLineOffset != 0)
            {
                directory.Set(TgaExtensionDirectory.TagScanLineOffset, scanLineOffset);
            }
            var attributesType = reader.GetByte();

            directory.Set(TgaExtensionDirectory.TagAttributesType, attributesType);

            string GetString(int length)
            {
                var buffer = new byte[length];

                reader.GetBytes(buffer, 0, length);
                int i = 0;

                while (i < buffer.Length && buffer[i] != '\0')
                {
                    ++i;
                }
                return(Encoding.ASCII.GetString(buffer, 0, i).TrimEnd());
            }

            bool TryGetDateTime(out DateTime dateTime)
            {
                var month  = reader.GetInt16();
                var day    = reader.GetInt16();
                var year   = reader.GetInt16();
                var hour   = reader.GetInt16();
                var minute = reader.GetInt16();
                var second = reader.GetInt16();

                if (month == 0 && day == 0 && year == 0)
                {
                    dateTime = DateTime.MinValue;
                    return(false);
                }
                dateTime = new DateTime(year, month, day, hour, minute, second);
                return(true);
            }

            bool TryGetTimeSpan(out TimeSpan timeSpan)
            {
                var hours   = reader.GetInt16();
                var minutes = reader.GetInt16();
                var seconds = reader.GetInt16();

                if (hours == 0 && minutes == 0 && seconds == 0)
                {
                    timeSpan = TimeSpan.Zero;
                    return(false);
                }
                timeSpan = new TimeSpan(hours, minutes, seconds);
                return(true);
            }

            string GetSoftwareVersion(string softwareName)
            {
                var number = reader.GetUInt16();
                var letter = reader.GetByte();

                if (number == 0)
                {
                    return(string.Empty);
                }
                var sb    = new StringBuilder();
                var denom = softwareName != "Paint Shop Pro" ? 100 : 0x100;

                sb.Append(number / denom);
                sb.Append('.');
                sb.Append(number % denom);
                if (letter != 0 && letter != 0x20)
                {
                    sb.Append((char)letter);
                }
                return(sb.ToString());
            }

            bool TryGetRational(out Rational value)
            {
                var num   = reader.GetUInt16();
                var denom = reader.GetUInt16();

                if (denom == 0)
                {
                    value = default;
                    return(false);
                }
                value = new Rational(num, denom);
                return(true);
            }
        }
        private const int ExifTag = 0x45786966; // Exif

        public static DirectoryList ReadMetadata(Stream stream)
        {
            var directories = new List <Directory>();

            //
            // Read all boxes from the file
            //

            var reader = new SequentialStreamReader(stream);

            var boxes = BoxReader.ReadBoxes(reader);

            //
            // Map those boxes to directories
            //

            ParseQuickTimeTest();

            uint primaryItem = boxes.Descendant <PrimaryItemBox>()?.PrimaryItem ?? uint.MaxValue;
            var  itemRefs    = (boxes.Descendant <ItemReferenceBox>()?.Boxes ?? new SingleItemTypeReferenceBox[0])
                               .Where(i => i.Type == BoxTypes.ThmbTag || i.Type == BoxTypes.CdscTag).ToList();

            ParseImageProperties();

            if (stream.CanSeek)
            {
                ParseItemSegments();
            }

            return(directories);

            void ParseQuickTimeTest()
            {
                if (boxes.Descendant <FileTypeBox>() is { } ftype)
                {
                    var dir = new QuickTimeFileTypeDirectory();
                    if (ftype.MajorBrand > 0)
                    {
                        dir.Set(QuickTimeFileTypeDirectory.TagMajorBrand, ftype.MajorBrandString);
                    }

                    if (ftype.MinorBrand > 0)
                    {
                        dir.Set(QuickTimeFileTypeDirectory.TagMinorVersion, ftype.MinorBrandString);
                    }

                    if (ftype.CompatibleBrands.Count > 0)
                    {
                        dir.Set(
                            QuickTimeFileTypeDirectory.TagCompatibleBrands,
                            string.Join(", ", ftype.CompatibleBrandStrings.ToArray()));
                    }

                    directories.Add(dir);
                }
            }

            void ParseImageProperties()
            {
                uint[] allPrimaryTiles = (boxes.Descendant <ItemReferenceBox>()?.Boxes ?? new SingleItemTypeReferenceBox[0])
                                         .SelectMany(i => i.FromItemId == primaryItem && i.Type == BoxTypes.DimgTag ? i.ToItemIds : new uint[0])
                                         .ToArray();
                var itemPropertyBox = boxes.Descendant <ItemPropertyBox>();

                if (itemPropertyBox == null)
                {
                    return;
                }
                var props        = itemPropertyBox.Boxes.Descendant <ItemPropertyContainerBox>().Boxes;
                var associations = itemPropertyBox.Boxes.Descendant <ItemPropertyAssociationBox>();

                ParsePropertyBoxes(
                    "HEIC Primary Item Properties",
                    ImageProperties(primaryItem, allPrimaryTiles, associations, props));

                foreach (var itemRef in itemRefs)
                {
                    ParsePropertyBoxes(
                        "HEIC Thumbnail Properties",
                        ImageProperties(itemRef.FromItemId, new uint[0], associations, props));
                }

                return;