mirror of
https://github.com/rosenbjerg/FFMpegCore.git
synced 2024-11-10 00:24:14 +01:00
Moved SkiaSharp implemented to its own extension
This commit is contained in:
parent
8afe1e0c9e
commit
cc22d15061
9 changed files with 228 additions and 34 deletions
28
FFMpegCore.Extensions.SkiaSharp/BitmapExtensions.cs
Normal file
28
FFMpegCore.Extensions.SkiaSharp/BitmapExtensions.cs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Extensions.SkiaSharp
|
||||||
|
{
|
||||||
|
public static class BitmapExtensions
|
||||||
|
{
|
||||||
|
public static bool AddAudio(this SKBitmap poster, string audio, string output)
|
||||||
|
{
|
||||||
|
var destination = $"{Environment.TickCount}.png";
|
||||||
|
using (var fileStream = File.OpenWrite(destination))
|
||||||
|
{
|
||||||
|
poster.Encode(fileStream, SKEncodedImageFormat.Png, default); // PNG does not respect the quality parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return FFMpeg.PosterWithAudio(destination, audio, output);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(destination))
|
||||||
|
{
|
||||||
|
File.Delete(destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs
Normal file
58
FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
using FFMpegCore.Pipes;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Extensions.SkiaSharp
|
||||||
|
{
|
||||||
|
public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable
|
||||||
|
{
|
||||||
|
public int Width => Source.Width;
|
||||||
|
|
||||||
|
public int Height => Source.Height;
|
||||||
|
|
||||||
|
public string Format { get; private set; }
|
||||||
|
|
||||||
|
public SKBitmap Source { get; private set; }
|
||||||
|
|
||||||
|
public BitmapVideoFrameWrapper(SKBitmap bitmap)
|
||||||
|
{
|
||||||
|
Source = bitmap ?? throw new ArgumentNullException(nameof(bitmap));
|
||||||
|
Format = ConvertStreamFormat(bitmap.ColorType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Serialize(Stream stream)
|
||||||
|
{
|
||||||
|
var data = Source.Bytes;
|
||||||
|
stream.Write(data, 0, data.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SerializeAsync(Stream stream, CancellationToken token)
|
||||||
|
{
|
||||||
|
var data = Source.Bytes;
|
||||||
|
await stream.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Source.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ConvertStreamFormat(SKColorType fmt)
|
||||||
|
{
|
||||||
|
switch (fmt)
|
||||||
|
{
|
||||||
|
case SKColorType.Gray8:
|
||||||
|
return "gray8";
|
||||||
|
case SKColorType.Bgra8888:
|
||||||
|
return "bgra";
|
||||||
|
case SKColorType.Rgb888x:
|
||||||
|
return "rgb";
|
||||||
|
case SKColorType.Rgba8888:
|
||||||
|
return "rgba";
|
||||||
|
case SKColorType.Rgb565:
|
||||||
|
return "rgb565";
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"Not supported pixel format {fmt}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<IsPackable>true</IsPackable>
|
||||||
|
<Description>Image extension for FFMpegCore using System.Common.Drawing</Description>
|
||||||
|
<PackageVersion>5.0.0</PackageVersion>
|
||||||
|
<PackageReleaseNotes>
|
||||||
|
</PackageReleaseNotes>
|
||||||
|
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing</PackageTags>
|
||||||
|
<Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev</Authors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SkiaSharp" Version="2.88.3" />
|
||||||
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
57
FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs
Normal file
57
FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
using System.Drawing;
|
||||||
|
using FFMpegCore.Pipes;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Extensions.SkiaSharp
|
||||||
|
{
|
||||||
|
public static class FFMpegImage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Saves a 'png' thumbnail to an in-memory bitmap
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">Source video file.</param>
|
||||||
|
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
|
||||||
|
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
|
||||||
|
/// <param name="streamIndex">Selected video stream index.</param>
|
||||||
|
/// <param name="inputFileIndex">Input file index</param>
|
||||||
|
/// <returns>Bitmap with the requested snapshot.</returns>
|
||||||
|
public static SKBitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
||||||
|
{
|
||||||
|
var source = FFProbe.Analyse(input);
|
||||||
|
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
|
||||||
|
arguments
|
||||||
|
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
|
||||||
|
.ForceFormat("rawvideo")))
|
||||||
|
.ProcessSynchronously();
|
||||||
|
|
||||||
|
ms.Position = 0;
|
||||||
|
using var bitmap = SKBitmap.Decode(ms);
|
||||||
|
return bitmap.Copy();
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Saves a 'png' thumbnail to an in-memory bitmap
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">Source video file.</param>
|
||||||
|
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
|
||||||
|
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
|
||||||
|
/// <param name="streamIndex">Selected video stream index.</param>
|
||||||
|
/// <param name="inputFileIndex">Input file index</param>
|
||||||
|
/// <returns>Bitmap with the requested snapshot.</returns>
|
||||||
|
public static async Task<SKBitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
||||||
|
{
|
||||||
|
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
|
||||||
|
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
|
||||||
|
await arguments
|
||||||
|
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
|
||||||
|
.ForceFormat("rawvideo")))
|
||||||
|
.ProcessAsynchronously();
|
||||||
|
|
||||||
|
ms.Position = 0;
|
||||||
|
return SKBitmap.Decode(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,13 @@
|
||||||
using SkiaSharp;
|
using System.Drawing;
|
||||||
|
|
||||||
namespace FFMpegCore.Extensions.System.Drawing.Common
|
namespace FFMpegCore.Extensions.System.Drawing.Common
|
||||||
{
|
{
|
||||||
public static class BitmapExtensions
|
public static class BitmapExtensions
|
||||||
{
|
{
|
||||||
public static bool AddAudio(this SKBitmap poster, string audio, string output)
|
public static bool AddAudio(this Image poster, string audio, string output)
|
||||||
{
|
{
|
||||||
var destination = $"{Environment.TickCount}.png";
|
var destination = $"{Environment.TickCount}.png";
|
||||||
using (var fileStream = File.OpenWrite(destination))
|
poster.Save(destination);
|
||||||
{
|
|
||||||
poster.Encode(fileStream, SKEncodedImageFormat.Png, default); // PNG does not respect the quality parameter
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return FFMpeg.PosterWithAudio(destination, audio, output);
|
return FFMpeg.PosterWithAudio(destination, audio, output);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
using FFMpegCore.Pipes;
|
using System.Drawing;
|
||||||
using SkiaSharp;
|
using System.Drawing.Imaging;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using FFMpegCore.Pipes;
|
||||||
|
|
||||||
namespace FFMpegCore.Extensions.System.Drawing.Common
|
namespace FFMpegCore.Extensions.System.Drawing.Common
|
||||||
{
|
{
|
||||||
|
@ -11,24 +13,44 @@ public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable
|
||||||
|
|
||||||
public string Format { get; private set; }
|
public string Format { get; private set; }
|
||||||
|
|
||||||
public SKBitmap Source { get; private set; }
|
public Bitmap Source { get; private set; }
|
||||||
|
|
||||||
public BitmapVideoFrameWrapper(SKBitmap bitmap)
|
public BitmapVideoFrameWrapper(Bitmap bitmap)
|
||||||
{
|
{
|
||||||
Source = bitmap ?? throw new ArgumentNullException(nameof(bitmap));
|
Source = bitmap ?? throw new ArgumentNullException(nameof(bitmap));
|
||||||
Format = ConvertStreamFormat(bitmap.ColorType);
|
Format = ConvertStreamFormat(bitmap.PixelFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Serialize(Stream stream)
|
public void Serialize(Stream stream)
|
||||||
{
|
{
|
||||||
var data = Source.Bytes;
|
var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat);
|
||||||
stream.Write(data, 0, data.Length);
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var buffer = new byte[data.Stride * data.Height];
|
||||||
|
Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);
|
||||||
|
stream.Write(buffer, 0, buffer.Length);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Source.UnlockBits(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SerializeAsync(Stream stream, CancellationToken token)
|
public async Task SerializeAsync(Stream stream, CancellationToken token)
|
||||||
{
|
{
|
||||||
var data = Source.Bytes;
|
var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat);
|
||||||
await stream.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false);
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var buffer = new byte[data.Stride * data.Height];
|
||||||
|
Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);
|
||||||
|
await stream.WriteAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Source.UnlockBits(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
@ -36,20 +58,27 @@ public void Dispose()
|
||||||
Source.Dispose();
|
Source.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ConvertStreamFormat(SKColorType fmt)
|
private static string ConvertStreamFormat(PixelFormat fmt)
|
||||||
{
|
{
|
||||||
switch (fmt)
|
switch (fmt)
|
||||||
{
|
{
|
||||||
case SKColorType.Gray8:
|
case PixelFormat.Format16bppGrayScale:
|
||||||
return "gray8";
|
return "gray16le";
|
||||||
case SKColorType.Bgra8888:
|
case PixelFormat.Format16bppRgb555:
|
||||||
|
return "bgr555le";
|
||||||
|
case PixelFormat.Format16bppRgb565:
|
||||||
|
return "bgr565le";
|
||||||
|
case PixelFormat.Format24bppRgb:
|
||||||
|
return "bgr24";
|
||||||
|
case PixelFormat.Format32bppArgb:
|
||||||
return "bgra";
|
return "bgra";
|
||||||
case SKColorType.Rgb888x:
|
case PixelFormat.Format32bppPArgb:
|
||||||
return "rgb";
|
//This is not really same as argb32
|
||||||
case SKColorType.Rgba8888:
|
return "argb";
|
||||||
|
case PixelFormat.Format32bppRgb:
|
||||||
return "rgba";
|
return "rgba";
|
||||||
case SKColorType.Rgb565:
|
case PixelFormat.Format48bppRgb:
|
||||||
return "rgb565";
|
return "rgb48le";
|
||||||
default:
|
default:
|
||||||
throw new NotSupportedException($"Not supported pixel format {fmt}");
|
throw new NotSupportedException($"Not supported pixel format {fmt}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="SkiaSharp" Version="2.88.3" />
|
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.3" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using FFMpegCore.Pipes;
|
using FFMpegCore.Pipes;
|
||||||
using SkiaSharp;
|
|
||||||
|
|
||||||
namespace FFMpegCore.Extensions.System.Drawing.Common
|
namespace FFMpegCore.Extensions.System.Drawing.Common
|
||||||
{
|
{
|
||||||
|
@ -15,7 +14,7 @@ public static class FFMpegImage
|
||||||
/// <param name="streamIndex">Selected video stream index.</param>
|
/// <param name="streamIndex">Selected video stream index.</param>
|
||||||
/// <param name="inputFileIndex">Input file index</param>
|
/// <param name="inputFileIndex">Input file index</param>
|
||||||
/// <returns>Bitmap with the requested snapshot.</returns>
|
/// <returns>Bitmap with the requested snapshot.</returns>
|
||||||
public static SKBitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
||||||
{
|
{
|
||||||
var source = FFProbe.Analyse(input);
|
var source = FFProbe.Analyse(input);
|
||||||
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||||
|
@ -27,8 +26,8 @@ public static SKBitmap Snapshot(string input, Size? size = null, TimeSpan? captu
|
||||||
.ProcessSynchronously();
|
.ProcessSynchronously();
|
||||||
|
|
||||||
ms.Position = 0;
|
ms.Position = 0;
|
||||||
using var bitmap = SKBitmap.Decode(ms);
|
using var bitmap = new Bitmap(ms);
|
||||||
return bitmap.Copy();
|
return bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), bitmap.PixelFormat);
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves a 'png' thumbnail to an in-memory bitmap
|
/// Saves a 'png' thumbnail to an in-memory bitmap
|
||||||
|
@ -39,7 +38,7 @@ public static SKBitmap Snapshot(string input, Size? size = null, TimeSpan? captu
|
||||||
/// <param name="streamIndex">Selected video stream index.</param>
|
/// <param name="streamIndex">Selected video stream index.</param>
|
||||||
/// <param name="inputFileIndex">Input file index</param>
|
/// <param name="inputFileIndex">Input file index</param>
|
||||||
/// <returns>Bitmap with the requested snapshot.</returns>
|
/// <returns>Bitmap with the requested snapshot.</returns>
|
||||||
public static async Task<SKBitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
public static async Task<Bitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
||||||
{
|
{
|
||||||
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
|
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
|
||||||
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||||
|
@ -51,7 +50,7 @@ await arguments
|
||||||
.ProcessAsynchronously();
|
.ProcessAsynchronously();
|
||||||
|
|
||||||
ms.Position = 0;
|
ms.Position = 0;
|
||||||
return SKBitmap.Decode(ms);
|
return new Bitmap(ms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Test", "FFMpegCo
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Examples", "FFMpegCore.Examples\FFMpegCore.Examples.csproj", "{3125CF91-FFBD-4E4E-8930-247116AFE772}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Examples", "FFMpegCore.Examples\FFMpegCore.Examples.csproj", "{3125CF91-FFBD-4E4E-8930-247116AFE772}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMpegCore.Extensions.System.Drawing.Common", "FFMpegCore.Extensions.System.Drawing.Common\FFMpegCore.Extensions.System.Drawing.Common.csproj", "{9C1A4930-9369-4A18-AD98-929A2A510D80}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Extensions.System.Drawing.Common", "FFMpegCore.Extensions.System.Drawing.Common\FFMpegCore.Extensions.System.Drawing.Common.csproj", "{9C1A4930-9369-4A18-AD98-929A2A510D80}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Extensions.SkiaSharp", "FFMpegCore.Extensions.SkiaSharp\FFMpegCore.Extensions.SkiaSharp.csproj", "{5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
@ -33,6 +35,10 @@ Global
|
||||||
{9C1A4930-9369-4A18-AD98-929A2A510D80}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9C1A4930-9369-4A18-AD98-929A2A510D80}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9C1A4930-9369-4A18-AD98-929A2A510D80}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9C1A4930-9369-4A18-AD98-929A2A510D80}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9C1A4930-9369-4A18-AD98-929A2A510D80}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9C1A4930-9369-4A18-AD98-929A2A510D80}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
Loading…
Reference in a new issue