Raddrizza le foto applicando l'orientamento EXIF
La fotocamera di sistema non ruota lo scatto: salva i pixel come li legge il sensore e annota la rotazione in EXIF. SkiaSharp (SKBitmap.Decode) ignora quel tag, quindi le foto scattate ruotate finivano storte nelle miniature e venivano inviate cosi' al server. - ImageOrientation: applica l'orientamento ai pixel e ricodifica senza tag residui, mantenendo il formato di partenza. Immagine gia' dritta: nessuna ricodifica, per non perdere qualita' inutilmente. In caso di errore restituisce i byte originali, meglio una foto storta che nessuna foto. - UtilityFile.ConvertToDto: normalizza li', unico punto di passaggio di fotocamera e galleria, cosi' miniature, salvataggio locale e upload lavorano su pixel gia' dritti. Corretto anche DimensionBytes. - ImageThumb: applica l'orientamento anche in fase di miniatura, per raddrizzare le foto gia' presenti sul dispositivo. Nessun ritratto forzato: costringere una foto orizzontale in verticale vorrebbe dire ruotare il contenuto o ritagliarlo, e queste foto sono prove di ispezione. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
|||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace SteUp.Maui.Core.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 (<see cref="SKBitmap.Decode(SKCodec)"/>)
|
||||||
|
/// ignora quel tag, quindi senza questo passaggio le foto scattate ruotate finiscono
|
||||||
|
/// storte nelle miniature e sul server.
|
||||||
|
/// </summary>
|
||||||
|
public static class ImageOrientation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Versione dritta di una bitmap già decodificata. Restituisce sempre una nuova
|
||||||
|
/// bitmap, anche quando non serve ruotare, così chi chiama ne possiede una sola.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,11 +19,15 @@ public static class ImageThumb
|
|||||||
throw new InvalidOperationException("Formato immagine non supportato o file corrotto.");
|
throw new InvalidOperationException("Formato immagine non supportato o file corrotto.");
|
||||||
|
|
||||||
// Decodifica
|
// Decodifica
|
||||||
var info = codec.Info;
|
using var decoded = SKBitmap.Decode(codec);
|
||||||
using var bitmap = SKBitmap.Decode(codec);
|
if (decoded is null)
|
||||||
if (bitmap is null)
|
|
||||||
throw new InvalidOperationException("Impossibile decodificare l'immagine.");
|
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
|
// Calcola resize mantenendo aspect ratio
|
||||||
var w = bitmap.Width;
|
var w = bitmap.Width;
|
||||||
var h = bitmap.Height;
|
var h = bitmap.Height;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using SteUp.Maui.Core.Services;
|
||||||
using SteUp.Shared.Core.Dto;
|
using SteUp.Shared.Core.Dto;
|
||||||
|
|
||||||
namespace SteUp.Maui.Core.Utility;
|
namespace SteUp.Maui.Core.Utility;
|
||||||
@@ -10,13 +11,20 @@ public static class UtilityFile
|
|||||||
using var ms = new MemoryStream();
|
using var ms = new MemoryStream();
|
||||||
await stream.CopyToAsync(ms);
|
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
|
return new AttachedDto
|
||||||
{
|
{
|
||||||
Name = file.FileName,
|
Name = file.FileName,
|
||||||
Path = file.FullPath,
|
Path = file.FullPath,
|
||||||
MimeType = file.ContentType,
|
MimeType = file.ContentType,
|
||||||
DimensionBytes = ms.Length,
|
DimensionBytes = bytes.Length,
|
||||||
FileBytes = ms.ToArray(),
|
FileBytes = bytes,
|
||||||
Type = type
|
Type = type
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user