diff --git a/SteUp.Maui/Core/Services/ImageOrientation.cs b/SteUp.Maui/Core/Services/ImageOrientation.cs new file mode 100644 index 0000000..5f816ab --- /dev/null +++ b/SteUp.Maui/Core/Services/ImageOrientation.cs @@ -0,0 +1,106 @@ +using SkiaSharp; + +namespace SteUp.Maui.Core.Services; + +/// +/// Raddrizza le immagini applicando l'orientamento EXIF ai pixel. +/// La fotocamera di sistema non ruota lo scatto: salva i pixel come li ha letti il +/// sensore e annota la rotazione in EXIF. SkiaSharp () +/// ignora quel tag, quindi senza questo passaggio le foto scattate ruotate finiscono +/// storte nelle miniature e sul server. +/// +public static class ImageOrientation +{ + /// + /// Restituisce i byte dell'immagine con i pixel già nel verso giusto e senza tag + /// di orientamento residui. In caso di formato non gestito o errore restituisce + /// l'originale: meglio una foto storta che perdere lo scatto del rilevatore. + /// + public static byte[] Normalize(byte[] bytes, int quality = 92) + { + try + { + using var codec = SKCodec.Create(new SKMemoryStream(bytes)); + if (codec is null) return bytes; + + var origin = codec.EncodedOrigin; + + // Già dritta: non ricodifichiamo, così non si perde qualità inutilmente. + if (origin == SKEncodedOrigin.TopLeft) return bytes; + + using var decoded = SKBitmap.Decode(codec); + if (decoded is null) return bytes; + + using var upright = ApplyOrigin(decoded, origin); + using var image = SKImage.FromBitmap(upright); + + // Manteniamo il formato di partenza: il nome file conserva la sua estensione. + var format = codec.EncodedFormat == SKEncodedImageFormat.Png + ? SKEncodedImageFormat.Png + : SKEncodedImageFormat.Jpeg; + + using var encoded = image.Encode(format, quality); + return encoded?.ToArray() ?? bytes; + } + catch + { + return bytes; + } + } + + /// + /// Versione dritta di una bitmap già decodificata. Restituisce sempre una nuova + /// bitmap, anche quando non serve ruotare, così chi chiama ne possiede una sola. + /// + public static SKBitmap Upright(SKBitmap src, SKEncodedOrigin origin) => + origin == SKEncodedOrigin.TopLeft ? src.Copy() : ApplyOrigin(src, origin); + + private static SKBitmap ApplyOrigin(SKBitmap src, SKEncodedOrigin origin) + { + // Gli orientamenti "di lato" scambiano larghezza e altezza. + var sideways = origin is SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightTop + or SKEncodedOrigin.RightBottom or SKEncodedOrigin.LeftBottom; + + var w = sideways ? src.Height : src.Width; + var h = sideways ? src.Width : src.Height; + + var dst = new SKBitmap(w, h, src.ColorType, src.AlphaType); + using var canvas = new SKCanvas(dst); + + switch (origin) + { + case SKEncodedOrigin.TopRight: // specchiata in orizzontale + canvas.Translate(w, 0); + canvas.Scale(-1, 1); + break; + case SKEncodedOrigin.BottomRight: // ruotata di 180° + canvas.Translate(w, h); + canvas.RotateDegrees(180); + break; + case SKEncodedOrigin.BottomLeft: // specchiata in verticale + canvas.Translate(0, h); + canvas.Scale(1, -1); + break; + case SKEncodedOrigin.LeftTop: // trasposta + canvas.RotateDegrees(90); + canvas.Scale(1, -1); + break; + case SKEncodedOrigin.RightTop: // ruotata di 90° in senso orario + canvas.Translate(w, 0); + canvas.RotateDegrees(90); + break; + case SKEncodedOrigin.RightBottom: // trasversa + canvas.Translate(w, h); + canvas.RotateDegrees(270); + canvas.Scale(1, -1); + break; + case SKEncodedOrigin.LeftBottom: // ruotata di 90° in senso antiorario + canvas.Translate(0, h); + canvas.RotateDegrees(-90); + break; + } + + canvas.DrawBitmap(src, 0, 0); + return dst; + } +} diff --git a/SteUp.Maui/Core/Services/ImageThumb.cs b/SteUp.Maui/Core/Services/ImageThumb.cs index 2ba4ff0..bc552e9 100644 --- a/SteUp.Maui/Core/Services/ImageThumb.cs +++ b/SteUp.Maui/Core/Services/ImageThumb.cs @@ -19,11 +19,15 @@ public static class ImageThumb throw new InvalidOperationException("Formato immagine non supportato o file corrotto."); // Decodifica - var info = codec.Info; - using var bitmap = SKBitmap.Decode(codec); - if (bitmap is null) + using var decoded = SKBitmap.Decode(codec); + if (decoded is null) throw new InvalidOperationException("Impossibile decodificare l'immagine."); + // SKBitmap.Decode non applica l'orientamento EXIF: senza questo passaggio le + // miniature delle foto scattate ruotate escono storte (vale anche per gli + // scatti già sul dispositivo, che non passano da UtilityFile.ConvertToDto). + using var bitmap = ImageOrientation.Upright(decoded, codec.EncodedOrigin); + // Calcola resize mantenendo aspect ratio var w = bitmap.Width; var h = bitmap.Height; diff --git a/SteUp.Maui/Core/Utility/UtilityFile.cs b/SteUp.Maui/Core/Utility/UtilityFile.cs index 83665e7..c4b65d8 100644 --- a/SteUp.Maui/Core/Utility/UtilityFile.cs +++ b/SteUp.Maui/Core/Utility/UtilityFile.cs @@ -1,3 +1,4 @@ +using SteUp.Maui.Core.Services; using SteUp.Shared.Core.Dto; namespace SteUp.Maui.Core.Utility; @@ -10,13 +11,20 @@ public static class UtilityFile using var ms = new MemoryStream(); await stream.CopyToAsync(ms); + var bytes = ms.ToArray(); + + // Unico punto di passaggio di fotocamera e galleria: raddrizziamo qui, cosi' + // miniature, salvataggio locale e upload lavorano tutti su pixel gia' dritti. + if (type == AttachedDto.TypeAttached.Image) + bytes = ImageOrientation.Normalize(bytes); + return new AttachedDto { Name = file.FileName, Path = file.FullPath, MimeType = file.ContentType, - DimensionBytes = ms.Length, - FileBytes = ms.ToArray(), + DimensionBytes = bytes.Length, + FileBytes = bytes, Type = type }; }