/// <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);
            }
        }
        /// <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);
            }
        }