diff --git a/Aaru.Core/Aaru.Core.csproj b/Aaru.Core/Aaru.Core.csproj
index bb4fbd602..e77a7a5b6 100644
--- a/Aaru.Core/Aaru.Core.csproj
+++ b/Aaru.Core/Aaru.Core.csproj
@@ -79,6 +79,7 @@
+
@@ -164,6 +165,7 @@
+
diff --git a/Aaru.Core/Graphics/Spiral.cs b/Aaru.Core/Graphics/Spiral.cs
new file mode 100644
index 000000000..6921e6dc1
--- /dev/null
+++ b/Aaru.Core/Graphics/Spiral.cs
@@ -0,0 +1,490 @@
+// /***************************************************************************
+// Aaru Data Preservation Suite
+// ----------------------------------------------------------------------------
+//
+// Filename : DataFile.cs
+// Author(s) : Natalia Portillo
+//
+// Component : Core algorithms.
+//
+// --[ Description ] ----------------------------------------------------------
+//
+// Abstracts writing to files.
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General public License for more details.
+//
+// You should have received a copy of the GNU General public License
+// along with this program. If not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2011-2023 Natalia Portillo
+// ****************************************************************************/
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Aaru.CommonTypes;
+using SkiaSharp;
+
+namespace Aaru.Core.Graphics;
+
+public sealed class Spiral
+{
+ static readonly DiscParameters _cdParameters = new(120, 15, 33, 46, 50, 116, 0, 0, 360000, SKColors.Silver);
+ static readonly DiscParameters _cdRecordableParameters =
+ new(120, 15, 33, 46, 50, 116, 45, 46, 360000, new SKColor(0xBD, 0xA0, 0x00));
+ static readonly DiscParameters _cdRewritableParameters =
+ new(120, 15, 33, 46, 50, 116, 45, 46, 360000, new SKColor(0x50, 0x50, 0x50));
+ static readonly DiscParameters _dvdPlusRParameters =
+ new(120, 15, 33, 46.8f, 48, 116, 46.586f, 46.8f, 2295104, new SKColor(0x6f, 0x0A, 0xCA));
+ static readonly DiscParameters _dvdPlusRParameters80 =
+ new(80, 15, 33, 46.8f, 48, 76, 46.586f, 46.8f, 714544, new SKColor(0x6f, 0x0A, 0xCA));
+ static readonly DiscParameters _dvdPlusRwParameters =
+ new(120, 15, 33, 44, 48, 116, 47.792f, 48, 2295104, new SKColor(0x38, 0x38, 0x38));
+ static readonly DiscParameters _dvdPlusRwParameters80 =
+ new(80, 15, 33, 44, 48, 76, 47.792f, 48, 714544, new SKColor(0x38, 0x38, 0x38));
+ static readonly DiscParameters _ps1CdParameters = new(120, 15, 33, 46, 50, 116, 0, 0, 360000, SKColors.Black);
+ static readonly DiscParameters _ps2CdParameters =
+ new(120, 15, 33, 46, 50, 116, 0, 0, 360000, new SKColor(0x0c, 0x08, 0xc3));
+ static readonly DiscParameters _dvdParameters =
+ new(120, 15, 33, 44, 48, 116, 0, 0, 2294922, new SKColor(0x6f, 0x0A, 0xCA));
+ static readonly DiscParameters _dvdParameters80 =
+ new(120, 15, 33, 44, 48, 76, 0, 0, 714544, new SKColor(0x6f, 0x0A, 0xCA));
+ static readonly DiscParameters _dvdRParameters =
+ new(120, 15, 33, 46, 48, 116, 44, 46, 2294922, new SKColor(0x6f, 0x0A, 0xCA));
+ static readonly DiscParameters _dvdRParameters80 =
+ new(80, 15, 33, 46, 48, 76, 44, 46, 712891, new SKColor(0x6f, 0x0A, 0xCA));
+ static readonly DiscParameters _dvdRwParameters =
+ new(120, 15, 33, 46, 48, 116, 44, 46, 2294922, new SKColor(0x38, 0x38, 0x38));
+ static readonly DiscParameters _dvdRwParameters80 =
+ new(80, 15, 33, 46, 48, 76, 44, 46, 712891, new SKColor(0x38, 0x38, 0x38));
+ readonly SKCanvas _canvas;
+ readonly List _leadInPoints;
+ readonly long _maxSector;
+ readonly List _points;
+ readonly List _recordableInformationPoints;
+
+ /// Initializes a spiral
+ /// Width in pixels for the underlying bitmap
+ /// Height in pixels for the underlying bitmap
+ /// Disc parameters
+ /// Last sector that will be drawn into the spiral
+ public Spiral(int width, int height, DiscParameters parameters, ulong lastSector)
+ {
+ Bitmap = new SKBitmap(width, height);
+ _canvas = new SKCanvas(Bitmap);
+
+ var center = new SKPoint(width / 2f, height / 2f);
+
+ int smallerDimension = Math.Min(width, height) - 8;
+
+ // Get other diameters
+ float centerHoleDiameter = smallerDimension * parameters.CenterHole / parameters.DiscDiameter;
+ float clampingDiameter = smallerDimension * parameters.ClampingMinimum / parameters.DiscDiameter;
+
+ float informationAreaStartDiameter =
+ smallerDimension * parameters.InformationAreaStart / parameters.DiscDiameter;
+
+ float leadInEndDiameter = smallerDimension * parameters.LeadInEnd / parameters.DiscDiameter;
+ float informationAreaEndDiameter = smallerDimension * parameters.InformationAreaEnd / parameters.DiscDiameter;
+
+ float recordableAreaStartDiameter =
+ smallerDimension * parameters.RecordableInformationStart / parameters.DiscDiameter;
+
+ float recordableAreaEndDiameter =
+ smallerDimension * parameters.RecordableInformationEnd / parameters.DiscDiameter;
+
+ _maxSector = parameters.NominalMaxSectors;
+ long lastSector1 = (long)lastSector;
+
+ // If the dumped media is overburnt
+ if(lastSector1 > _maxSector)
+ _maxSector = lastSector1;
+
+ // Ensure the disc hole is not painted over
+ var clipPath = new SKPath();
+ clipPath.AddCircle(center.X, center.Y, centerHoleDiameter / 2);
+ _canvas.ClipPath(clipPath, SKClipOperation.Difference);
+
+ // Paint CD
+ _canvas.DrawCircle(center, smallerDimension / 2f, new SKPaint
+ {
+ Style = SKPaintStyle.StrokeAndFill,
+ Color = parameters.DiscColor
+ });
+
+ // Draw out border of disc
+ _canvas.DrawCircle(center, smallerDimension / 2f, new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ StrokeWidth = 4
+ });
+
+ // Draw disc hole border
+ _canvas.DrawCircle(center, centerHoleDiameter / 2f, new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ StrokeWidth = 4
+ });
+
+ // Draw clamping area
+ _canvas.DrawCircle(center, clampingDiameter / 2f, new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Gray,
+ StrokeWidth = 4
+ });
+
+ // Some trigonometry thing I do not understand fully
+ const float a = 1f;
+
+ // Draw the Lead-In
+ _leadInPoints = GetSpiralPoints(center, informationAreaStartDiameter / 2, leadInEndDiameter / 2, a);
+
+ var path = new SKPath();
+
+ path.MoveTo(_leadInPoints[0]);
+
+ foreach(SKPoint point in _leadInPoints)
+ path.LineTo(point);
+
+ _canvas.DrawPath(path, new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.LightGray,
+ StrokeWidth = 2
+ });
+
+ // If there's a recordable information area, get its points
+ if(recordableAreaEndDiameter > 0 &&
+ recordableAreaStartDiameter > 0)
+ _recordableInformationPoints =
+ GetSpiralPoints(center, recordableAreaStartDiameter / 2, recordableAreaEndDiameter / 2, a);
+
+ _points = GetSpiralPoints(center, leadInEndDiameter / 2, informationAreaEndDiameter / 2, a);
+
+ path = new SKPath();
+
+ path.MoveTo(_points[0]);
+
+ long pointsPerSector = _points.Count / _maxSector;
+ long sectorsPerPoint = _maxSector / _points.Count;
+
+ if(_maxSector % _points.Count > 0)
+ sectorsPerPoint++;
+
+ long lastPoint;
+
+ if(pointsPerSector > 0)
+ lastPoint = lastSector1 * pointsPerSector;
+ else
+ lastPoint = lastSector1 / sectorsPerPoint;
+
+ for(int index = 0; index < lastPoint; index++)
+ {
+ SKPoint point = _points[index];
+ path.LineTo(point);
+ }
+
+ _canvas.DrawPath(path, new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Gray,
+ StrokeWidth = 2
+ });
+ }
+
+ public SKBitmap Bitmap { get; }
+
+ public static DiscParameters DiscParametersFromMediaType(MediaType mediaType, bool smallDisc = false) =>
+
+ // TODO: Blu-ray
+ // TODO: DDCD
+ // TODO: HD DVD
+ // TODO: UMD
+ // TODO: GD-ROM
+ mediaType switch
+ {
+ MediaType.CD => _cdParameters,
+ MediaType.CDDA => _cdParameters,
+ MediaType.CDG => _cdParameters,
+ MediaType.CDEG => _cdParameters,
+ MediaType.CDI => _cdParameters,
+ MediaType.CDIREADY => _cdParameters,
+ MediaType.CDROM => _cdParameters,
+ MediaType.CDROMXA => _cdParameters,
+ MediaType.CDPLUS => _cdParameters,
+ MediaType.CDMO => _cdParameters,
+ MediaType.VCD => _cdParameters,
+ MediaType.SVCD => _cdParameters,
+ MediaType.PCD => _cdParameters,
+ MediaType.DTSCD => _cdParameters,
+ MediaType.CDMIDI => _cdParameters,
+ MediaType.CDV => _cdParameters,
+ MediaType.CDR => _cdRecordableParameters,
+ MediaType.CDRW => _cdRewritableParameters,
+ MediaType.CDMRW => _cdRewritableParameters,
+ MediaType.SACD => _dvdParameters,
+ MediaType.DVDROM => smallDisc ? _dvdParameters : _dvdParameters80,
+ MediaType.DVDR => smallDisc ? _dvdRParameters : _dvdRParameters80,
+ MediaType.DVDRW => smallDisc ? _dvdRwParameters : _dvdRwParameters80,
+ MediaType.DVDPR => smallDisc ? _dvdPlusRParameters : _dvdPlusRParameters80,
+ MediaType.DVDPRW => smallDisc ? _dvdPlusRwParameters : _dvdPlusRwParameters80,
+ MediaType.DVDPRWDL => smallDisc ? _dvdPlusRwParameters : _dvdPlusRwParameters80,
+ MediaType.DVDRDL => smallDisc ? _dvdRParameters : _dvdRParameters80,
+ MediaType.DVDPRDL => smallDisc ? _dvdPlusRParameters : _dvdPlusRParameters80,
+ MediaType.DVDRWDL => smallDisc ? _dvdRwParameters : _dvdRwParameters80,
+ MediaType.PS1CD => _ps1CdParameters,
+ MediaType.PS2CD => _ps2CdParameters,
+ MediaType.PS2DVD => _dvdParameters,
+ MediaType.PS3DVD => _dvdParameters,
+ MediaType.XGD => _dvdParameters,
+ MediaType.XGD2 => _dvdParameters,
+ MediaType.XGD3 => _dvdParameters,
+ MediaType.XGD4 => _dvdParameters,
+ MediaType.MEGACD => _cdParameters,
+ MediaType.SATURNCD => _cdParameters,
+ MediaType.MilCD => _cdParameters,
+ MediaType.SuperCDROM2 => _cdParameters,
+ MediaType.JaguarCD => _cdParameters,
+ MediaType.ThreeDO => _cdParameters,
+ MediaType.PCFX => _cdParameters,
+ MediaType.NeoGeoCD => _cdParameters,
+ MediaType.CDTV => _cdParameters,
+ MediaType.CD32 => _cdParameters,
+ MediaType.Nuon => _dvdParameters,
+ MediaType.GOD => _dvdParameters80,
+ MediaType.WOD => _dvdParameters,
+ MediaType.Pippin => _cdParameters,
+ _ => null
+ };
+
+ /// Paints the segment of the spiral that corresponds to the specified sector in green
+ /// Sector
+ public void PaintSectorGood(ulong sector) => PaintSector(sector, SKColors.Green);
+
+ /// Paints the segment of the spiral that corresponds to the specified sector in red
+ /// Sector
+ public void PaintSectorBad(ulong sector) => PaintSector(sector, SKColors.Red);
+
+ /// Paints the segment of the spiral that corresponds to the specified sector in yellow
+ /// Sector
+ public void PaintSectorUnknown(ulong sector) => PaintSector(sector, SKColors.Yellow);
+
+ /// Paints the segment of the spiral that corresponds to the specified sector in gray
+ /// Sector
+ public void PaintSectorUndumped(ulong sector) => PaintSector(sector, SKColors.Gray);
+
+ /// Paints the segment of the spiral that corresponds to the information specific to recordable discs in green
+ public void PaintRecordableInformationGood()
+ {
+ if(_recordableInformationPoints is null)
+ return;
+
+ var path = new SKPath();
+
+ path.MoveTo(_recordableInformationPoints[0]);
+
+ foreach(SKPoint point in _recordableInformationPoints)
+ path.LineTo(point);
+
+ _canvas.DrawPath(path, new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Green,
+ StrokeWidth = 2
+ });
+ }
+
+ /// Paints the segment of the spiral that corresponds to the specified sector in the specified color
+ /// Sector
+ /// Color to paint the segment
+ public void PaintSector(ulong sector, SKColor color)
+ {
+ long pointsPerSector = _points.Count / _maxSector;
+ long sectorsPerPoint = _maxSector / _points.Count;
+
+ if(_maxSector % _points.Count > 0)
+ sectorsPerPoint++;
+
+ var paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = color,
+ StrokeWidth = 2
+ };
+
+ var path = new SKPath();
+
+ if(pointsPerSector > 0)
+ {
+ long firstPoint = (long)sector * pointsPerSector;
+
+ path.MoveTo(_points[(int)firstPoint]);
+
+ for(int i = (int)firstPoint; i < firstPoint + pointsPerSector; i++)
+ path.LineTo(_points[i]);
+
+ _canvas.DrawPath(path, paint);
+
+ return;
+ }
+
+ long point = (long)sector / sectorsPerPoint;
+
+ if(point == 0)
+ {
+ path.MoveTo(_points[0]);
+ path.LineTo(_points[1]);
+ }
+ else if(point >= _points.Count - 1)
+ {
+ path.MoveTo(_points[^2]);
+ path.LineTo(_points[^1]);
+ }
+ else
+ {
+ path.MoveTo(_points[(int)point]);
+ path.LineTo(_points[(int)point + 1]);
+ }
+
+ _canvas.DrawPath(path, paint);
+ }
+
+ ///
+ /// Paints the segment of the spiral that corresponds to the specified sector of the Lead-In in the specified
+ /// color
+ ///
+ /// Sector
+ /// Color to paint the segment in
+ /// Total size of the lead-in in sectors
+ public void PaintLeadInSector(ulong sector, SKColor color, int leadInSize)
+ {
+ long pointsPerSector = _leadInPoints.Count / leadInSize;
+ long sectorsPerPoint = leadInSize / _leadInPoints.Count;
+
+ if(leadInSize % _leadInPoints.Count > 0)
+ sectorsPerPoint++;
+
+ var paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = color,
+ StrokeWidth = 2
+ };
+
+ var path = new SKPath();
+
+ if(pointsPerSector > 0)
+ {
+ long firstPoint = (long)sector * pointsPerSector;
+
+ path.MoveTo(_leadInPoints[(int)firstPoint]);
+
+ for(int i = (int)firstPoint; i < firstPoint + pointsPerSector; i++)
+ path.LineTo(_leadInPoints[i]);
+
+ _canvas.DrawPath(path, paint);
+
+ return;
+ }
+
+ long point = (long)sector / sectorsPerPoint;
+
+ if(point == 0)
+ {
+ path.MoveTo(_leadInPoints[0]);
+ path.LineTo(_leadInPoints[1]);
+ }
+ else if(point >= _leadInPoints.Count - 1)
+ {
+ path.MoveTo(_leadInPoints[^2]);
+ path.LineTo(_leadInPoints[^1]);
+ }
+ else
+ {
+ path.MoveTo(_leadInPoints[(int)point]);
+ path.LineTo(_leadInPoints[(int)point + 1]);
+ }
+
+ _canvas.DrawPath(path, paint);
+ }
+
+ /// Writes the spiral bitmap as a PNG into the specified stream
+ /// Stream that will receive the spiral bitmap
+ public void WriteToStream(Stream stream)
+ {
+ var image = SKImage.FromBitmap(Bitmap);
+ SKData data = image.Encode();
+ data.SaveTo(stream);
+ }
+
+ /// Gets all the points that are needed to draw a spiral with the specified parameters
+ /// Center of the spiral start
+ /// Minimum radius before which the spiral must have no points
+ /// Radius at which the spiral will end
+ /// TODO: Something trigonometry something something...
+ /// List of points to draw the specified spiral
+ static List GetSpiralPoints(SKPoint center, float minRadius, float maxRadius, float A)
+ {
+ // Get the points.
+ List points = new();
+ const float dtheta = (float)(0.5 * Math.PI / 180); // Five degrees.
+
+ for(float theta = 0;; theta += dtheta)
+ {
+ // Calculate r.
+ float r = A * theta;
+
+ if(r < minRadius)
+ continue;
+
+ // Convert to Cartesian coordinates.
+ float x = (float)(r * Math.Cos(theta));
+ float y = (float)(r * Math.Sin(theta));
+
+ // Center.
+ x += center.X;
+ y += center.Y;
+
+ // Create the point.
+ points.Add(new SKPoint(x, y));
+
+ // If we have gone far enough, stop.
+ if(r > maxRadius)
+ break;
+ }
+
+ return points;
+ }
+
+ // GD-ROM LD area ends at 29mm, HD area starts at 30mm radius
+
+ /// Defines the physical disc parameters
+ /// Diameter of the whole disc
+ /// Diameter of the hole at the center
+ /// Diameter of the clamping area
+ /// Diameter at which the information area starts
+ /// Diameter at which the Lead-In starts
+ /// Diameter at which the information area ends
+ /// Diameter at which the information specific to recordable media starts
+ /// Diameter at which the information specific to recordable media starts
+ /// Number of maximum sectors, for discs following the specifications
+ /// Typical disc color
+ public sealed record DiscParameters(float DiscDiameter, float CenterHole, float ClampingMinimum,
+ float InformationAreaStart, float LeadInEnd, float InformationAreaEnd,
+ float RecordableInformationStart, float RecordableInformationEnd,
+ int NominalMaxSectors, SKColor DiscColor);
+}
\ No newline at end of file