-
|
Hello, please allow me to ask a question that might not be directly related to the core functionality of your project.I am using the DrawnUI library in my MAUI development. I'm wondering if there is any way to convert a page (which uses a layout from DrawnUI) into an SKBitmap, so that I can directly save it as an image? My goal is to use DrawnUI for layout/formatting and then generate that layout as a picture. |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments
-
|
Hi @sswi, this is totally possible, in fact this is covered in the PDF examples inside sandbox and demo projects. So, let's say you define your layout as a class. You can do it in xaml or code-behind, i do report in xaml, because they are not speed-critical and xaml is easier to preview with hotreload. So i can preview what i will be exporting while designing. I will put a snippet from my app here, if you have additional questions please do not hesitate. You would see that your case is covered by setting Files helper is used here to ask for permissions to save result as file. In my app i the share the file using maui share feature. Files.CheckPermissionsAsync(async () =>
{
try
{
IsBusy = true;
await Task.Delay(10);
_lockLogs = true;
string fullFilename = null;
string filename = null;
var subfolder = "Logs";
var data = SelectedResult;
async Task CreateRenderingReport(bool asImage)
{
var scale = 1f;
//new code:
var formatSize = Pdf.GetPaperSizeInInches(PaperFormat.A4);
// rotate for landscape
var inches = new SKSize(formatSize.Height * scale, formatSize.Width * scale);
var dpi = 150;
var paper = Pdf.GetPaperSizePixels(inches, dpi);
if (asImage)
{
//todo half size
}
var report = new ReportFull()
{
BindingContext = this
};
var layout = report;
var destination = new SKRect(0, 0, paper.Width, paper.Height);
var measured = layout.Measure(destination.Width, destination.Height, scale);
//prepare DrawingRect
layout.Arrange(
new SKRect(0, 0, layout.MeasuredSize.Pixels.Width, layout.MeasuredSize.Pixels.Height),
layout.MeasuredSize.Pixels.Width, layout.MeasuredSize.Pixels.Height, scale);
// PREVIEW <--- uncomment below to preview stuff while you design and use xaml hot reload
// instead of really saving. On Windows !!!
//MainThread.BeginInvokeOnMainThread(() =>
//{
// var page = new PreviewDrawnLayoutPage(report);
// OpenPageInNewWindow(page, "Report", paper);
// IsBusy = false;
//});
//return;
//====================
//STEP 2: Render pages
//====================
fullFilename = Files.GetFullFilename(filename, StorageType.Cache, subfolder);
//we need a local file to ba saved in order to share it
if (File.Exists(fullFilename))
{
File.Delete(fullFilename);
}
async Task RenderReport(SkiaDrawingContext ctx)
{
var drawingContext = new DrawingContext(ctx,
new SKRect(0, 0, ctx.Width, ctx.Height), scale);
//first rendering to launch loading images and first layout
layout.Render(drawingContext);
//in our specific case we have images inside that load async,
//so wait for them and render final result
if (layout is IContentReadyAware aware)
{
while (!aware.ContentIsLoaded)
{
await Task.Delay(50);
}
}
//second rendering required to reflect layout changes and async images are loaded
ctx.Canvas.Clear(SKColors.White); //non-transparent reserves space inside pdf
layout.Render(drawingContext);
}
if (asImage)
{
// AS JPG
var surface = layout.CreateSurface((int)paper.Width, (int)paper.Height, false);
var context = new SkiaDrawingContext()
{
Canvas = surface.Canvas,
Width = (int)paper.Width,
Height = (int)paper.Height
};
await RenderReport(context);
surface.Flush();
using (var image = surface.Snapshot())
using (var dataImg = image.Encode(SKEncodedImageFormat.Jpeg, 90))
{
var file = Files.OpenFile(fullFilename, StorageType.Cache, subfolder);
using (var fileStream = file.Handler)
using (var dataStream = dataImg.AsStream())
{
await dataStream.CopyToAsync(fileStream);
await fileStream.FlushAsync();
}
Files.CloseFile(file, true);
}
surface.Dispose();
await Task.Delay(500);
IsBusy = false;
}
else
{
// AS PDF
using (var ms = new MemoryStream())
using (var stream = new SKManagedWStream(ms))
{
using (var document = SKDocument.CreatePdf(stream, new SKDocumentPdfMetadata
{
RasterDpi = dpi,
EncodingQuality = 85,
Author = ResStrings.VendorTitle,
Producer = ResStrings.VendorTitle,
Subject = this.Title
}))
{
using (var canvas = document.BeginPage(paper.Width, paper.Height))
{
var ctx = new SkiaDrawingContext()
{
Canvas = canvas,
Width = paper.Width,
Height = paper.Height
};
await RenderReport(ctx);
}
document.EndPage();
document.Close();
}
ms.Position = 0;
var content = ms.ToArray();
var file = Files.OpenFile(fullFilename, StorageType.Cache, subfolder);
// Write the bytes to the FileStream of the FileDescriptor
await file.Handler.WriteAsync(content, 0, content.Length);
// Ensure all bytes are written to the underlying device
await file.Handler.FlushAsync();
Files.CloseFile(file, true);
await Task.Delay(500);
IsBusy = false;
}
}
}
async Task WriteFile(Action<StreamWriter> action)
{
fullFilename = Files.GetFullFilename(filename, StorageType.Cache, subfolder);
if (!File.Exists(fullFilename))
{
var file = Files.OpenFile(filename, StorageType.Cache, subfolder);
using (StreamWriter s = new StreamWriter(file.Handler, Encoding.UTF8))
{
action(s);
}
Files.CloseFile(file, true);
await Task.Delay(500);
}
}
switch (_userManager.User.Options.LogFormat)
{
case OptionsLogFormat.ReportImage:
filename = Exporter.GenerateLogFileName(data.CreatedTimeUtc.ToLocalTime(), "jpg");
await CreateRenderingReport(true);
break;
case OptionsLogFormat.ReportPdf:
filename = Exporter.GenerateLogFileName(data.CreatedTimeUtc.ToLocalTime(), "pdf");
await CreateRenderingReport(false);
break;
case OptionsLogFormat.VBO:
filename = Exporter.GenerateLogFileName(data.CreatedTimeUtc.ToLocalTime(), "vbo");
await WriteFile((s) => Exporter.WriteVbo(s, data, _userManager.User.Options.Units));
break;
case OptionsLogFormat.CSV:
default:
filename = Exporter.GenerateLogFileName(data.CreatedTimeUtc.ToLocalTime(), "csv");
await WriteFile((s) => Exporter.WriteCsv(s, data));
break;
}
//we use fullFilename that was filled by actions above.. maybe todo refactor
MainThread.BeginInvokeOnMainThread(() =>
{
try
{
App.Native.ExportLogs(new string[] { fullFilename });
}
catch (Exception e)
{
Super.Log(e);
}
});
}
catch (Exception e)
{
Console.WriteLine(e);
MainThread.BeginInvokeOnMainThread(async () =>
{
await UI.Alert(ResStrings.VendorTitle, e.ToString());
});
}
finally
{
_lockLogs = false;
}
});Helpers for previewing while you desing this on Windows: public static void OpenPageInNewWindow(ContentPage page,
string title = "Editor", SKSize size = default)
{
#if WINDOWS || MACCATALYST
var window = new Window(page) { Title = title };
//if (page is ShaderEditorPage)
{
var windowWidth = 800;
var windowHeight = 800;
if (size != default)
{
windowWidth = (int)size.Width;
windowHeight = (int)size.Height;
}
window.MinimumWidth = 200;
window.MinimumHeight = 100;
window.Width = windowWidth;
window.Height = windowHeight;
var mainWindow = Application.Current?.Windows?.FirstOrDefault();
if (mainWindow != null)
{
window.X = mainWindow.X + mainWindow.Width + 20;
window.Y = mainWindow.Y;
}
}
Application.Current.OpenWindow(window);
#endif
}
public class PreviewDrawnLayoutPage : BasePageReloadable
{
public PreviewDrawnLayoutPage(SkiaControl layout)
{
_layout = layout;
}
Canvas Canvas;
private readonly SkiaControl _layout;
protected override void Dispose(bool isDisposing)
{
if (isDisposing)
{
this.Content = null;
Canvas?.Dispose();
}
base.Dispose(isDisposing);
}
public override void Build()
{
Canvas?.Dispose();
Canvas = new Canvas()
{
//RenderingScale = 0.5f,
Gestures = GesturesMode.Enabled,
RenderingMode = RenderingModeType.Accelerated,
VerticalOptions = LayoutOptions.Fill,
HorizontalOptions = LayoutOptions.Fill,
BackgroundColor = Colors.Black,
Content = new SkiaLayout()
{
VerticalOptions = LayoutOptions.Fill,
HorizontalOptions = LayoutOptions.Fill,
Children = new List<SkiaControl>()
{
_layout
}
}
};
this.Content = Canvas;
}
} |
Beta Was this translation helpful? Give feedback.
-
|
Ah yeas that's not all. Images might load their sources in public interface IContentReadyAware
{
bool ContentIsLoaded { get; }
}
// I had map isnide my report so never mind it, i will put the full xaml page code-behind here for an idea..
public partial class ReportFull : IContentReadyAware
{
public bool ContentIsLoaded { get; set; }
public ReportFull()
{
InitializeComponent();
if (!Map.IsVisible)
{
Tasks.StartDelayed(TimeSpan.FromMilliseconds(250), () =>
{
ContentIsLoaded = true;
});
}
else
{
Tasks.StartDelayed(TimeSpan.FromMilliseconds(9000), () =>
{
//just DIE with empty map
ContentIsLoaded = true;
});
}
}
private void Map_OnLoadingChanged(object sender, bool e)
{
if (!e)
{
//tiles loaded!
ContentIsLoaded = true;
}
}
}I had no images from urls, cared about the map only if you have images you can use their event |
Beta Was this translation helpful? Give feedback.
Hi @sswi, this is totally possible, in fact this is covered in the PDF examples inside sandbox and demo projects.
I am doing similar stuff inside one of the apps where i need to export a "report". Basically i am using a drawnui layout to serve as a report template, then its rendered with BindingContext applied so data is applied and then exported as an image or as pdf upon user's selection.
So, let's say you define your layout as a class. You can do it in xaml or code-behind, i do report in xaml, because they are not speed-critical and xaml is easier to preview with hotreload. So i can preview what i will be exporting while designing.
I will put a snippet from my app here, if you have add…