void Handler(AtomCallbackArgs a) { switch (a.TypeString) { case "moov": { QuickTimeReader.ProcessAtoms(stream, MoovHandler, a.BytesLeft); break; } case "ftyp": { var directory = new QuickTimeFileTypeDirectory(); directory.Set(QuickTimeFileTypeDirectory.TagMajorBrand, a.Reader.Get4ccString()); directory.Set(QuickTimeFileTypeDirectory.TagMinorVersion, a.Reader.GetUInt32()); var compatibleBrands = new List <string>(); while (a.BytesLeft >= 4) { compatibleBrands.Add(a.Reader.Get4ccString()); } directory.Set(QuickTimeFileTypeDirectory.TagCompatibleBrands, String.Join(", ", compatibleBrands)); directories.Add(directory); break; } } }
/// <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); } }
void UserDataHandler(AtomCallbackArgs a) { switch (a.TypeString) { case "?xyz": var stringSize = a.Reader.GetUInt16(); a.Reader.Skip(2); // uint16 language code var stringBytes = a.Reader.GetBytes(stringSize); // TODO parse ISO 6709 string into GeoLocation? GeoLocation does not (currently) support altitude, where ISO 6709 does GetMetaHeaderDirectory().Set( QuickTimeMetadataHeaderDirectory.TagGpsLocation, new StringValue(stringBytes, Encoding.UTF8)); break; } }
void Handler(AtomCallbackArgs a) { switch (a.TypeString) { case "moov": { QuickTimeReader.ProcessAtoms(stream, MoovHandler, a.BytesLeft); break; } case "uuid": { var XMP = new byte[] { 0xbe, 0x7a, 0xcf, 0xcb, 0x97, 0xa9, 0x42, 0xe8, 0x9c, 0x71, 0x99, 0x94, 0x91, 0xe3, 0xaf, 0xac }; if (a.BytesLeft >= XMP.Length) { var uuid = a.Reader.GetBytes(XMP.Length); if (XMP.RegionEquals(0, XMP.Length, uuid)) { var xmpBytes = a.Reader.GetNullTerminatedBytes((int)a.BytesLeft); var xmpDirectory = new XmpReader().Extract(xmpBytes); directories.Add(xmpDirectory); } } break; } case "ftyp": { var directory = new QuickTimeFileTypeDirectory(); directory.Set(QuickTimeFileTypeDirectory.TagMajorBrand, a.Reader.Get4ccString()); directory.Set(QuickTimeFileTypeDirectory.TagMinorVersion, a.Reader.GetUInt32()); var compatibleBrands = new List <string>(); while (a.BytesLeft >= 4) { compatibleBrands.Add(a.Reader.Get4ccString()); } #if NET35 directory.Set(QuickTimeFileTypeDirectory.TagCompatibleBrands, string.Join(", ", compatibleBrands.ToArray())); #else directory.Set(QuickTimeFileTypeDirectory.TagCompatibleBrands, string.Join(", ", compatibleBrands)); #endif directories.Add(directory); break; } } }
void UuidHandler(AtomCallbackArgs a) { switch (a.TypeString) { case "CMT1": { var handler = new QuickTimeTiffHandler <ExifIfd0Directory>(directories); var reader = new IndexedSeekingReader(a.Stream, (int)a.Reader.Position); TiffReader.ProcessTiff(reader, handler); break; } case "CMT2": { var handler = new QuickTimeTiffHandler <ExifSubIfdDirectory>(directories); var reader = new IndexedSeekingReader(a.Stream, (int)a.Reader.Position); TiffReader.ProcessTiff(reader, handler); break; } case "CMT3": { var handler = new QuickTimeTiffHandler <CanonMakernoteDirectory>(directories); var reader = new IndexedSeekingReader(a.Stream, (int)a.Reader.Position); TiffReader.ProcessTiff(reader, handler); break; } case "CMT4": { var handler = new QuickTimeTiffHandler <GpsDirectory>(directories); var reader = new IndexedSeekingReader(a.Stream, (int)a.Reader.Position); TiffReader.ProcessTiff(reader, handler); break; } } }
/// <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; } } }
void MoovHandler(AtomCallbackArgs a) { switch (a.TypeString) { case "mvhd": { var directory = new QuickTimeMovieHeaderDirectory(); directory.Set(QuickTimeMovieHeaderDirectory.TagVersion, a.Reader.GetByte()); directory.Set(QuickTimeMovieHeaderDirectory.TagFlags, a.Reader.GetBytes(3)); directory.Set(QuickTimeMovieHeaderDirectory.TagCreated, _epoch.AddTicks(TimeSpan.TicksPerSecond * a.Reader.GetUInt32())); directory.Set(QuickTimeMovieHeaderDirectory.TagModified, _epoch.AddTicks(TimeSpan.TicksPerSecond * a.Reader.GetUInt32())); var timeScale = a.Reader.GetUInt32(); directory.Set(QuickTimeMovieHeaderDirectory.TagTimeScale, timeScale); directory.Set(QuickTimeMovieHeaderDirectory.TagDuration, TimeSpan.FromSeconds(a.Reader.GetUInt32() / (double)timeScale)); directory.Set(QuickTimeMovieHeaderDirectory.TagPreferredRate, a.Reader.Get32BitFixedPoint()); directory.Set(QuickTimeMovieHeaderDirectory.TagPreferredVolume, a.Reader.Get16BitFixedPoint()); a.Reader.Skip(10); directory.Set(QuickTimeMovieHeaderDirectory.TagMatrix, a.Reader.GetBytes(36)); directory.Set(QuickTimeMovieHeaderDirectory.TagPreviewTime, a.Reader.GetUInt32()); directory.Set(QuickTimeMovieHeaderDirectory.TagPreviewDuration, a.Reader.GetUInt32()); directory.Set(QuickTimeMovieHeaderDirectory.TagPosterTime, a.Reader.GetUInt32()); directory.Set(QuickTimeMovieHeaderDirectory.TagSelectionTime, a.Reader.GetUInt32()); directory.Set(QuickTimeMovieHeaderDirectory.TagSelectionDuration, a.Reader.GetUInt32()); directory.Set(QuickTimeMovieHeaderDirectory.TagCurrentTime, a.Reader.GetUInt32()); directory.Set(QuickTimeMovieHeaderDirectory.TagNextTrackId, a.Reader.GetUInt32()); directories.Add(directory); break; } case "uuid": { var CR3 = new byte[] { 0x85, 0xc0, 0xb6, 0x87, 0x82, 0x0f, 0x11, 0xe0, 0x81, 0x11, 0xf4, 0xce, 0x46, 0x2b, 0x6a, 0x48 }; var uuid = a.Reader.GetBytes(CR3.Length); if (CR3.RegionEquals(0, CR3.Length, uuid)) { QuickTimeReader.ProcessAtoms(stream, UuidHandler, a.BytesLeft); } break; } case "trak": { QuickTimeReader.ProcessAtoms(stream, TrakHandler, a.BytesLeft); break; } // case "clip": // { // QuickTimeReader.ProcessAtoms(stream, clipHandler, a.BytesLeft); // break; // } // case "prfl": // { // a.Reader.Skip(4L); // var partId = a.Reader.GetUInt32(); // var featureCode = a.Reader.GetUInt32(); // var featureValue = string.Join(" ", a.Reader.GetBytes(4).Select(v => v.ToString("X2")).ToArray()); // Debug.WriteLine($"PartId={partId} FeatureCode={featureCode} FeatureValue={featureValue}"); // break; // } } }
void MetaDataHandler(AtomCallbackArgs a) { // see https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html switch (a.TypeString) { case "keys": { var version = a.Reader.GetByte(); var flags = a.Reader.GetBytes(3); var entryCount = a.Reader.GetUInt32(); for (int i = 1; i <= entryCount; i++) { var keySize = a.Reader.GetUInt32(); var keyValueSize = (int)keySize - 8; var keyNamespace = a.Reader.GetUInt32(); var keyValue = a.Reader.GetBytes(keyValueSize); metaDataKeys.Add(Encoding.UTF8.GetString(keyValue)); } break; } case "ilst": { var directory = new QuickTimeMetadataHeaderDirectory(); // Iterate over the list of Metadata Item Atoms. for (int i = 0; i < metaDataKeys.Count; i++) { long atomSize = a.Reader.GetUInt32(); if (atomSize < 24) { directory.AddError("Invalid ilist atom type"); a.Reader.Skip(atomSize - 4); continue; } var atomType = a.Reader.GetUInt32(); // Indexes into the metadata item keys atom are 1-based (1…entry_count). // atom type for each metadata item atom is the index of the key if (atomType < 1 || atomType > metaDataKeys.Count) { directory.AddError("Invalid ilist atom type"); a.Reader.Skip(atomSize - 8); continue; } var key = metaDataKeys[(int)atomType - 1]; // Value Atom var typeIndicator = a.Reader.GetUInt32(); var localeIndicator = a.Reader.GetUInt32(); // Data Atom var dataTypeIndicator = a.Reader.GetUInt32(); if (!_supportedAtomValueTypes.Contains((int)dataTypeIndicator)) { directory.AddError($"Unsupported type indicator \"{dataTypeIndicator}\" for key \"{key}\""); a.Reader.Skip(atomSize - 20); continue; } // Currently only the Default Country/Locale is supported var dataLocaleIndicator = a.Reader.GetUInt32(); if (dataLocaleIndicator != 0) { directory.AddError($"Unsupported locale indicator \"{dataLocaleIndicator}\" for key \"{key}\""); a.Reader.Skip(atomSize - 24); continue; } var data = a.Reader.GetBytes((int)atomSize - 24); if (directory.TryGetTag(key, out int tag)) { DecodeData(data, (int)dataTypeIndicator, tag, directory); } else { directory.AddError($"Unsupported ilist key \"{key}\""); } } directories.Add(directory); break; } } }
/// <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 // TODO GetUInt64 (i.e. unsigned) atomSize = reader.GetInt64(); } 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); } }
void MetaDataHandler(AtomCallbackArgs a) { // see https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html switch (a.TypeString) { case "keys": { a.Reader.Skip(4); // 1 byte version, 3 bytes flags var entryCount = a.Reader.GetUInt32(); for (int i = 1; i <= entryCount; i++) { var keySize = a.Reader.GetUInt32(); var keyValueSize = (int)keySize - 8; a.Reader.Skip(4); // uint32: key namespace var keyValue = a.Reader.GetBytes(keyValueSize); metaDataKeys.Add(Encoding.UTF8.GetString(keyValue)); } break; } case "ilst": { // Iterate over the list of Metadata Item Atoms. for (int i = 0; i < metaDataKeys.Count; i++) { long atomSize = a.Reader.GetUInt32(); if (atomSize < 24) { GetMetaHeaderDirectory().AddError("Invalid ilist atom type"); a.Reader.Skip(atomSize - 4); continue; } var atomType = a.Reader.GetUInt32(); // Indexes into the metadata item keys atom are 1-based (1…entry_count). // atom type for each metadata item atom is the index of the key if (atomType < 1 || atomType > metaDataKeys.Count) { GetMetaHeaderDirectory().AddError("Invalid ilist atom type"); a.Reader.Skip(atomSize - 8); continue; } var key = metaDataKeys[(int)atomType - 1]; // Value Atom a.Reader.Skip(8); // uint32 type indicator, uint32 locale indicator // Data Atom var dataTypeIndicator = a.Reader.GetUInt32(); if (!_supportedAtomValueTypes.Contains((int)dataTypeIndicator)) { GetMetaHeaderDirectory().AddError($"Unsupported type indicator \"{dataTypeIndicator}\" for key \"{key}\""); a.Reader.Skip(atomSize - 20); continue; } // locale not supported yet. a.Reader.Skip(4); var data = a.Reader.GetBytes((int)atomSize - 24); if (QuickTimeMetadataHeaderDirectory.TryGetTag(key, out int tag)) { object value = dataTypeIndicator switch { // UTF-8 1 => new StringValue(data, Encoding.UTF8), // BE Float32 (used for User Rating) 23 => BitConverter.ToSingle(BitConverter.IsLittleEndian ? data.Reverse().ToArray() : data, 0), // 13 JPEG // 14 PNG // 27 BMP _ => data }; GetMetaHeaderDirectory().Set(tag, value); } else { GetMetaHeaderDirectory().AddError($"Unsupported ilist key \"{key}\""); } } break; } } }