From b29f6b63d086ee042080999d37a6b947e7c71073 Mon Sep 17 00:00:00 2001 From: Clemens Date: Sat, 8 Jan 2022 21:22:45 +0100 Subject: [PATCH] Initial GeoTIFF support --- .../{WorldFileImage.cs => GeoTaggedImage.cs} | 69 ++++++++------- MapImages/UWP/MapImages.UWP.csproj | 7 +- MapImages/WPF/GeoTaggedImage.WPF.cs | 85 +++++++++++++++++++ MapImages/WinUI/GeoTaggedImage.WinUI.cs | 17 ++++ 4 files changed, 146 insertions(+), 32 deletions(-) rename MapImages/Shared/{WorldFileImage.cs => GeoTaggedImage.cs} (78%) create mode 100644 MapImages/WPF/GeoTaggedImage.WPF.cs create mode 100644 MapImages/WinUI/GeoTaggedImage.WinUI.cs diff --git a/MapImages/Shared/WorldFileImage.cs b/MapImages/Shared/GeoTaggedImage.cs similarity index 78% rename from MapImages/Shared/WorldFileImage.cs rename to MapImages/Shared/GeoTaggedImage.cs index aea7040a..c0f39e92 100644 --- a/MapImages/Shared/WorldFileImage.cs +++ b/MapImages/Shared/GeoTaggedImage.cs @@ -29,11 +29,10 @@ using System.Windows.Media.Imaging; namespace MapControl.Images { - public class WorldFileImage + public partial class GeoTaggedImage { public static readonly DependencyProperty PathProperty = DependencyProperty.RegisterAttached( - "Path", typeof(string), typeof(WorldFileImage), - new PropertyMetadata(null, async (o, e) => (await ReadWorldFileImage((string)e.NewValue)).SetImage((Image)o))); + "Path", typeof(string), typeof(GeoTaggedImage), new PropertyMetadata(null, PathPropertyChanged)); public BitmapSource Bitmap { get; } public Matrix Transform { get; } @@ -41,7 +40,7 @@ namespace MapControl.Images public BoundingBox BoundingBox { get; } public double Rotation { get; } - public WorldFileImage(BitmapSource bitmap, Matrix transform, MapProjection projection) + public GeoTaggedImage(BitmapSource bitmap, Matrix transform, MapProjection projection) { Bitmap = bitmap; Transform = transform; @@ -83,40 +82,41 @@ namespace MapControl.Images image.SetValue(PathProperty, path); } - public static async Task ReadWorldFileImage(string imagePath, string worldFilePath, string projFilePath = null) + public static Task ReadImage(string imageFilePath) { - var bitmap = (BitmapSource)await ImageLoader.LoadImageAsync(imagePath); - var transform = ReadWorldFile(worldFilePath); - var projection = (projFilePath != null && File.Exists(projFilePath)) - ? new GeoApiProjection { WKT = File.ReadAllText(projFilePath) } - : null; - - return new WorldFileImage(bitmap, transform, projection); - } - - public static Task ReadWorldFileImage(string imagePath) - { - var ext = Path.GetExtension(imagePath); + var ext = Path.GetExtension(imageFilePath); if (ext.Length < 4) { throw new ArgumentException("Invalid image file path extension, must have at least three characters."); } - var dir = Path.GetDirectoryName(imagePath); - var file = Path.GetFileNameWithoutExtension(imagePath); + var dir = Path.GetDirectoryName(imageFilePath); + var file = Path.GetFileNameWithoutExtension(imageFilePath); var worldFilePath = Path.Combine(dir, file + ext.Remove(2, 1) + "w"); - var projFilePath = Path.Combine(dir, file + ".prj"); - return ReadWorldFileImage(imagePath, worldFilePath, projFilePath); + if (File.Exists(worldFilePath)) + { + return ReadImage(imageFilePath, worldFilePath, Path.Combine(dir, file + ".prj")); + } + + return ReadGeoTiff(imageFilePath); + } + + public static async Task ReadImage(string imageFilePath, string worldFilePath, string projFilePath = null) + { + var transform = ReadWorldFile(worldFilePath); + + var projection = (projFilePath != null && File.Exists(projFilePath)) + ? ReadProjectionFile(projFilePath) + : null; + + var bitmap = (BitmapSource)await ImageLoader.LoadImageAsync(imageFilePath); + + return new GeoTaggedImage(bitmap, transform, projection); } public static Matrix ReadWorldFile(string path) { - if (!File.Exists(path)) - { - throw new ArgumentException("World file \"" + path + "\"not found."); - } - var parameters = File.ReadLines(path) .Take(6) .Select((line, i) => @@ -143,12 +143,12 @@ namespace MapControl.Images parameters[5]); // line 6: F or OffsetY } - public static MapProjection ReadProjFile(string path) + public static MapProjection ReadProjectionFile(string path) { return new GeoApiProjection { WKT = File.ReadAllText(path) }; } - public void SetImage(Image image) + public void ApplyToImage(Image image) { if (Rotation != 0d) { @@ -178,12 +178,21 @@ namespace MapControl.Images } MapPanel.SetBoundingBox(image, BoundingBox); + return image; } - public static async Task CreateImage(string imagePath) + public static async Task CreateImage(string imageFilePath) { - return (await ReadWorldFileImage(imagePath)).CreateImage(); + return (await ReadImage(imageFilePath)).CreateImage(); + } + + private static async void PathPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) + { + if (o is Image image && e.NewValue is string imageFilePath) + { + (await ReadImage(imageFilePath)).ApplyToImage(image); + } } } } diff --git a/MapImages/UWP/MapImages.UWP.csproj b/MapImages/UWP/MapImages.UWP.csproj index 38db8cd8..ff0d28ff 100644 --- a/MapImages/UWP/MapImages.UWP.csproj +++ b/MapImages/UWP/MapImages.UWP.csproj @@ -40,11 +40,14 @@ PackageReference + + GeoTaggedImage.cs + GroundOverlayPanel.cs - - WorldFileImage.cs + + GeoTaggedImage.WinUI.cs diff --git a/MapImages/WPF/GeoTaggedImage.WPF.cs b/MapImages/WPF/GeoTaggedImage.WPF.cs new file mode 100644 index 00000000..53f04899 --- /dev/null +++ b/MapImages/WPF/GeoTaggedImage.WPF.cs @@ -0,0 +1,85 @@ +// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control +// © 2022 Clemens Fischer +// Licensed under the Microsoft Public License (Ms-PL) + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace MapControl.Images +{ + public partial class GeoTaggedImage + { + private const string PixelScaleQuery = "/ifd/{ushort=33550}"; + private const string TiePointQuery = "/ifd/{ushort=33922}"; + private const string TransformationQuery = "/ifd/{ushort=34264}"; + private const string NoDataQuery = "/ifd/{ushort=42113}"; + + public static Task ReadGeoTiff(string imageFilePath) + { + return Task.Run(() => + { + BitmapSource bitmap; + Matrix transform; + + using (var stream = File.OpenRead(imageFilePath)) + { + bitmap = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + } + + var mdata = bitmap.Metadata as BitmapMetadata; + + if (mdata.GetQuery(PixelScaleQuery) is double[] pixelScale && + mdata.GetQuery(TiePointQuery) is double[] tiePoint && + pixelScale.Length == 3 && tiePoint.Length >= 6) + { + transform = new Matrix(pixelScale[0], 0d, 0d, -pixelScale[1], tiePoint[3], tiePoint[4]); + } + else if (mdata.GetQuery(TransformationQuery) is double[] transformation && + transformation.Length == 16) + { + transform = new Matrix(transformation[0], transformation[1], transformation[4], + transformation[5], transformation[3], transformation[7]); + } + else + { + throw new ArgumentException("No coordinate transformation found in \"" + imageFilePath + "\"."); + } + + if (mdata.GetQuery(NoDataQuery) is string noData && int.TryParse(noData, out int noDataValue)) + { + bitmap = ConvertTransparentPixel(bitmap, noDataValue); + } + + return new GeoTaggedImage(bitmap, transform, null); + }); + } + + public static BitmapSource ConvertTransparentPixel(BitmapSource source, int transparentPixel) + { + var target = source; + + if (source.Format == PixelFormats.Gray8 && transparentPixel < 256) + { + var colors = Enumerable.Range(0, 256) + .Select(i => Color.FromArgb(i == transparentPixel ? (byte)0 : (byte)255, (byte)i, (byte)i, (byte)i)) + .ToList(); + + var buffer = new byte[source.PixelWidth * source.PixelHeight]; + + source.CopyPixels(buffer, source.PixelWidth, 0); + + target = BitmapSource.Create( + source.PixelWidth, source.PixelHeight, source.DpiX, source.DpiY, + PixelFormats.Indexed8, new BitmapPalette(colors), buffer, source.PixelWidth); + + target.Freeze(); + } + + return target; + } + } +} diff --git a/MapImages/WinUI/GeoTaggedImage.WinUI.cs b/MapImages/WinUI/GeoTaggedImage.WinUI.cs new file mode 100644 index 00000000..2fbcc2f2 --- /dev/null +++ b/MapImages/WinUI/GeoTaggedImage.WinUI.cs @@ -0,0 +1,17 @@ +// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control +// © 2022 Clemens Fischer +// Licensed under the Microsoft Public License (Ms-PL) + +using System; +using System.Threading.Tasks; + +namespace MapControl.Images +{ + public partial class GeoTaggedImage + { + public static Task ReadGeoTiff(string imageFilePath) + { + throw new NotImplementedException(); + } + } +}