// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : Spiral.cs // Author(s) : Natalia Portillo // // Component : Core algorithms. // // --[ 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-2024 Natalia Portillo // ****************************************************************************/ using System; using System.Collections.Generic; using System.IO; using Aaru.CommonTypes; using Aaru.CommonTypes.Interfaces; using SkiaSharp; namespace Aaru.Core.Graphics; // TODO: HD DVD sectors are a guess public sealed class Spiral : IMediaGraph { 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 _ddcdParameters = new(120, 15, 33, 46, 50, 116, 0, 0, 666000, SKColors.Silver); static readonly DiscParameters _ddcdRecordableParameters = new(120, 15, 33, 46, 50, 116, 45, 46, 666000, new SKColor(0xBD, 0xA0, 0x00)); static readonly DiscParameters _ddcdRewritableParameters = new(120, 15, 33, 46, 50, 116, 45, 46, 666000, 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, SKColors.Silver); static readonly DiscParameters _dvdParameters80 = new(120, 15, 33, 44, 48, 76, 0, 0, 714544, SKColors.Silver); 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)); static readonly DiscParameters _bdParameters = new(120, 15, 33, 44, 48, 116, 0, 0, 12219392, new SKColor(0x80, 0x80, 0x80)); static readonly DiscParameters _bdRParameters = new(120, 15, 33, 46, 48, 116, 44, 46, 12219392, new SKColor(0x40, 0x40, 0x40)); static readonly DiscParameters _bdReParameters = new(120, 15, 33, 46, 48, 116, 44, 46, 11826176, new SKColor(0x20, 0x20, 0x20)); static readonly DiscParameters _bdRxlParameters = new(120, 15, 33, 46, 48, 116, 44, 46, 48878592, new SKColor(0x20, 0x20, 0x20)); static readonly DiscParameters _hddvdParameters = new(120, 15, 33, 44, 48, 116, 0, 0, 7864320, new SKColor(0x6f, 0x0A, 0xCA)); static readonly DiscParameters _hddvdRParameters = new(120, 15, 33, 46, 48, 116, 44, 46, 7864320, new SKColor(0xff, 0x91, 0x00)); static readonly DiscParameters _hddvdRwParameters = new(120, 15, 33, 46, 48, 116, 44, 46, 7864320, new SKColor(0x30, 0x30, 0x30)); static readonly DiscParameters _umdParameters = new(60, 11.025f, 16.2f, 28, 32, 56, 0, 0, 471872, new SKColor(0x6f, 0x0A, 0xCA)); static readonly DiscParameters _gdParameters = new(120, 15, 33, 46, 50, 116, 0, 0, 550000, SKColors.Silver); static readonly DiscParameters _gdRecordableParameters = new(120, 15, 33, 46, 50, 116, 45, 46, 550000, new SKColor(0xBD, 0xA0, 0x00)); readonly SKCanvas _canvas; readonly bool _gdrom; readonly List _leadInPoints; readonly long _maxSector; readonly List _points; readonly List _pointsLowDensity; 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) { if(parameters == _gdParameters || parameters == _gdRecordableParameters) _gdrom = true; // GD-ROM LD area ends at 29mm, HD area starts at 30mm radius 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; var 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 but it controls the space between the spiral turns const float a = 1f; // Draw the Lead-In _leadInPoints = GetSpiralPoints(center, informationAreaStartDiameter / 2, leadInEndDiameter / 2, _gdrom ? a : a * 1.5f); 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, _gdrom ? a : a * 1.5f); } if(_gdrom) { float lowDensityEndDiameter = smallerDimension * 29 * 2 / parameters.DiscDiameter; float highDensityStartDiameter = smallerDimension * 30 * 2 / parameters.DiscDiameter; _pointsLowDensity = GetSpiralPoints(center, leadInEndDiameter / 2, lowDensityEndDiameter / 2, a * 1.5f); _points = GetSpiralPoints(center, highDensityStartDiameter / 2, informationAreaEndDiameter / 2, a); } else _points = GetSpiralPoints(center, leadInEndDiameter / 2, informationAreaEndDiameter / 2, a); path = new SKPath(); if(_gdrom && _pointsLowDensity is not null) { path.MoveTo(_pointsLowDensity[0]); foreach(SKPoint point in _pointsLowDensity) path.LineTo(point); _canvas.DrawPath(path, new SKPaint { Style = SKPaintStyle.Stroke, Color = SKColors.Gray, StrokeWidth = 2 }); } path.MoveTo(_points[0]); long pointsPerSector; long sectorsPerPoint; if(_gdrom) { pointsPerSector = _points.Count / (_maxSector - 45000); sectorsPerPoint = (_maxSector - 45000) / _points.Count; if((_maxSector - 45000) % _points.Count > 0) sectorsPerPoint++; } else { pointsPerSector = _points.Count / _maxSector; sectorsPerPoint = _maxSector / _points.Count; if(_maxSector % _points.Count > 0) sectorsPerPoint++; } long lastPoint; if(_gdrom) lastSector1 -= 45000; if(pointsPerSector > 0) lastPoint = lastSector1 * pointsPerSector; else lastPoint = lastSector1 / sectorsPerPoint; for(var 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; } #region IMediaGraph Members /// /// 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); /// public void PaintSector(ulong sector, byte red, byte green, byte blue, byte opacity = 255) => PaintSector(sector, new SKColor(red, green, blue, opacity)); /// public void PaintSectorsUndumped(ulong startingSector, uint length) => PaintSectors(startingSector, length, SKColors.Gray); /// public void PaintSectorsGood(ulong startingSector, uint length) => PaintSectors(startingSector, length, SKColors.Green); /// public void PaintSectorsBad(ulong startingSector, uint length) => PaintSectors(startingSector, length, SKColors.Red); /// public void PaintSectorsUnknown(ulong startingSector, uint length) => PaintSectors(startingSector, length, SKColors.Yellow); /// public void PaintSectors(ulong startingSector, uint length, byte red, byte green, byte blue, byte opacity = 255) => PaintSectors(startingSector, length, new SKColor(red, green, blue, opacity)); /// public void PaintSectorsUndumped(IEnumerable sectors) => PaintSectors(sectors, SKColors.Gray); /// public void PaintSectorsGood(IEnumerable sectors) => PaintSectors(sectors, SKColors.Green); /// public void PaintSectorsBad(IEnumerable sectors) => PaintSectors(sectors, SKColors.Red); /// public void PaintSectorsUnknown(IEnumerable sectors) => PaintSectors(sectors, SKColors.Yellow); /// public void PaintSectorsUnknown(IEnumerable sectors, byte red, byte green, byte blue, byte opacity = 255) => PaintSectors(sectors, new SKColor(red, green, blue, opacity)); /// /// 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 }); } /// public void WriteTo(string path) { using var fs = new FileStream(path, FileMode.Create); WriteTo(fs); fs.Close(); } /// /// Writes the spiral bitmap as a PNG into the specified stream /// Stream that will receive the spiral bitmap public void WriteTo(Stream stream) { var image = SKImage.FromBitmap(Bitmap); SKData data = image.Encode(); data.SaveTo(stream); } #endregion public static DiscParameters DiscParametersFromMediaType(MediaType mediaType, bool smallDisc = false) => 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 => _bdParameters, 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, MediaType.DDCD => _ddcdParameters, MediaType.DDCDR => _ddcdRecordableParameters, MediaType.DDCDRW => _ddcdRewritableParameters, MediaType.BDROM => _bdParameters, MediaType.BDR => _bdRParameters, MediaType.BDRE => _bdReParameters, MediaType.BDRXL => _bdRxlParameters, MediaType.PS3BD => _bdParameters, MediaType.PS4BD => _bdParameters, MediaType.PS5BD => _bdParameters, MediaType.HDDVDROM => _hddvdParameters, MediaType.HDDVDR => _hddvdRParameters, MediaType.HDDVDRDL => _hddvdRParameters, MediaType.HDDVDRW => _hddvdRwParameters, MediaType.HDDVDRWDL => _hddvdRwParameters, MediaType.CBHD => _hddvdParameters, MediaType.FMTOWNS => _cdParameters, MediaType.DVDDownload => _dvdParameters, MediaType.CVD => _cdParameters, MediaType.Playdia => _cdParameters, MediaType.WUOD => _bdParameters, MediaType.UMD => _umdParameters, MediaType.GDROM => _gdParameters, MediaType.GDR => _gdRecordableParameters, _ => null }; void PaintSectors(ulong startingSector, uint length, SKColor color) { for(uint i = 0; i < length; i++) PaintSector(startingSector + i, color); } void PaintSectors(IEnumerable sectors, SKColor color) { foreach(ulong sector in sectors) PaintSector(sector, color); } /// Paints the segment of the spiral that corresponds to the specified sector in the specified color /// Sector /// Color to paint the segment void PaintSector(ulong sector, SKColor color) { long pointsPerSector; long sectorsPerPoint; List points = _gdrom && sector <= 45000 ? _pointsLowDensity : _points; if(_gdrom) { if(sector <= 45000) { pointsPerSector = points.Count / 45000; sectorsPerPoint = 45000 / points.Count; if(45000 % points.Count > 0) sectorsPerPoint++; } else { sector -= 45000; pointsPerSector = points.Count / (_maxSector - 45000); sectorsPerPoint = (_maxSector - 45000) / points.Count; if((_maxSector - 45000) % points.Count > 0) sectorsPerPoint++; } } else { pointsPerSector = points.Count / _maxSector; 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(var 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(var 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); } /// 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 /// A constant that decides the position of a point on the spiral /// List of points to draw the specified spiral static List GetSpiralPoints(SKPoint center, float minRadius, float maxRadius, float a) { // Initialize a list to store the points of the spiral. List points = []; const float dtheta = (float)(0.5f * Math.PI / 180); for(float theta = 0;; theta += dtheta) { // Calculate r. float r = a * theta; if(r < minRadius) continue; // Converts polar coordinates (r,theta) to Cartesian (x,y) var x = (float)(r * Math.Cos(theta)); var y = (float)(r * Math.Sin(theta)); // Adjusts x and y by center coordinates x += center.X; y += center.Y; // Adds the newly calculated point to the list points.Add(new SKPoint(x, y)); // Terminate the loop if we have reached the end of the spiral if(r > maxRadius) break; } return points; } #region Nested type: DiscParameters /// 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 ); #endregion }