// /*************************************************************************** // 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); }