private async void ImportButton_Click(object sender, RoutedEventArgs e) { if (_canvas == null) { return; } //###################################################### // These measurements are coming out all over the place. // Needs a more-or-less complete re-working. The wet ink // is functioning much better -- try using that? //###################################################### // scale target based on canvas zoom var zoom = _canvas.CurrentZoom(); int expectedWidth = (int)(ActualWidth * zoom); int expectedHeight = (int)(ActualHeight * zoom); // render to image var rtb = new RenderTargetBitmap(); await rtb.RenderAsync(ImageToImport, expectedWidth, expectedHeight).NotNull(); // Render control to RenderTargetBitmap // DPI mismatch -- use first attempt to calculate the real scale and render again. var scaleW = expectedWidth / (float)rtb.PixelWidth; var scaleH = expectedHeight / (float)rtb.PixelHeight; // render to image rtb = new RenderTargetBitmap(); await rtb.RenderAsync(ImageToImport, (int)(expectedWidth * scaleW), (int)(expectedHeight * scaleH)).NotNull(); // Render control to RenderTargetBitmap // get pixels from RTB var pixelBuffer = await rtb.GetPixelsAsync().NotNull(); // BGRA 8888 format var rawImage = new RawImageInterleaved_UInt8 { Data = pixelBuffer.ToArray(), Height = rtb.PixelHeight, Width = rtb.PixelWidth }; // render to canvas int left = (int)(Margin.Left); int top = (int)(Margin.Top); int right = left + (int)Width; int bottom = top + (int)Height; _canvas.ImportBytesScaled(rawImage, left, top, right, bottom); // ReSharper disable RedundantAssignment pixelBuffer = null; rtb = null; GC.Collect(); // ReSharper restore RedundantAssignment // make ourself invisible Visibility = Visibility.Collapsed; }
private async void ImportButton_Click(object sender, RoutedEventArgs e) { if (_canvas == null || textBlockToRender == null) { return; } var margin = textBlockToRender.Margin.Top; // scale target based on canvas zoom var zoom = _canvas.CurrentZoom(); int expectedWidth = (int)(ActualWidth * zoom); int expectedHeight = (int)((ActualHeight - margin) * zoom); // render to image var rtb = new RenderTargetBitmap(); await rtb.RenderAsync(textBlockToRender, expectedWidth, expectedHeight).NotNull(); // Render control to RenderTargetBitmap // DPI mismatch -- use first attempt to calculate the real scale and render again. var scaleW = expectedWidth / (float)rtb.PixelWidth; var scaleH = expectedHeight / (float)rtb.PixelHeight; // render to image rtb = new RenderTargetBitmap(); await rtb.RenderAsync(textBlockToRender, (int)(expectedWidth * scaleW), (int)(expectedHeight * scaleH)).NotNull(); // Render control to RenderTargetBitmap // get pixels from RTB var pixelBuffer = await rtb.GetPixelsAsync().NotNull(); // BGRA 8888 format var rawImage = new RawImageInterleaved_UInt8 { Data = pixelBuffer.ToArray(), Height = rtb.PixelHeight, Width = rtb.PixelWidth }; // render to canvas int left = (int)(Margin.Left); int top = (int)(Margin.Top + textBlockToRender.Margin.Top); int right = left + (int)Width; int bottom = top + (int)(Height - margin); _canvas.ImportBytesScaled(rawImage, left, top, right, bottom); // ReSharper disable RedundantAssignment pixelBuffer = null; rtb = null; GC.Collect(); // ReSharper restore RedundantAssignment // make ourself invisible Visibility = Visibility.Collapsed; }
private void AntiAliasRow([NotNull] RawImageInterleaved_UInt8 img, int img_yi, double scale_y, double imgyo, [NotNull] byte[] src, [NotNull] int[] src_aa) { // Measure AA var rowbytes = img.Width * 4; var yoff_f = Range(0.0, (img_yi * scale_y) + imgyo, img.Height - 1); var yoff = (int)yoff_f; double y_frac = yoff_f - yoff; var imul = (int)(255 * y_frac); var mul = 255 - imul; var end = img.Width * 4; var src_yi = Range(0, yoff * rowbytes, src.Length - (rowbytes + 4 + end)); // pre-calc AA row data from two source lines for (int x = 0; x < end; x += 4) { src_aa[x + 0] = ((src[src_yi + 0 + x] * mul) + (src[src_yi + 0 + x + rowbytes] * imul)) >> 8; src_aa[x + 1] = ((src[src_yi + 1 + x] * mul) + (src[src_yi + 1 + x + rowbytes] * imul)) >> 8; src_aa[x + 2] = ((src[src_yi + 2 + x] * mul) + (src[src_yi + 2 + x + rowbytes] * imul)) >> 8; src_aa[x + 3] = ((src[src_yi + 3 + x] * mul) + (src[src_yi + 3 + x + rowbytes] * imul)) >> 8; } }
private void DryWaitingStroke([NotNull] TileCanvas tileCanvas) { // Try to get a waiting stroke (peek, so we can draw the waiting stroke) DPoint[][] waitingStrokes; lock (_dryLock) { waitingStrokes = _dryingInk.ToArray(); _dryingInk.Clear(); if (waitingStrokes != null) { _renderingInk.UnionWith(waitingStrokes); } } tileCanvas.Invalidate(); // show progress if the render is slow. _renderTarget.Invalidate(); if (waitingStrokes == null) { return; } foreach (var strokeToRender in waitingStrokes) { if (strokeToRender.Length < 1) { continue; } // Figure out what part of the screen is covered var clipRegion = MeasureDrawing(strokeToRender, tileCanvas.CurrentZoom()); var pixelWidth = (int)clipRegion.Width; var pixelHeight = (int)clipRegion.Height; // draw to an image byte[] bytes; using (var offscreen = new CanvasRenderTarget(CanvasDevice.GetSharedDevice(), pixelWidth, pixelHeight, 96, DirectXPixelFormat.B8G8R8A8UIntNormalized, CanvasAlphaMode.Premultiplied)) { using (var ds = offscreen.CreateDrawingSession()) { ds?.Clear(Colors.Transparent); DrawToSession(ds, strokeToRender, clipRegion, tileCanvas.CurrentZoom()); } bytes = offscreen.GetPixelBytes(); } // render into tile cache var uncropped = new RawImageInterleaved_UInt8 { Data = bytes, Width = pixelWidth, Height = pixelHeight }; var visualWidth = (int)Math.Ceiling(pixelWidth / tileCanvas.CurrentZoom()); var visualHeight = (int)Math.Ceiling(pixelHeight / tileCanvas.CurrentZoom()); var visualTop = (int)Math.Floor(clipRegion.Y + 0.5); var visualLeft = (int)Math.Floor(clipRegion.X + 0.5); var visualRight = visualLeft + visualWidth; var visualBottom = visualTop + visualHeight; ThreadPool.QueueUserWorkItem(canv => { var ok = tileCanvas.ImportBytesScaled(uncropped, visualLeft, visualTop, visualRight, visualBottom); if (!ok) { Logging.WriteLogMessage("Tile byte import failed when drawing strokes"); } lock (_dryLock) { _renderingInk.Remove(strokeToRender); } tileCanvas.Invalidate(); // show finished strokes _renderTarget.Invalidate(); }); } }
/// <summary> /// Write an image onto a tile, with transparency. /// Returns true if any pixels were changed. /// `tileArea` is in tile space. `imageArea` is in image space. /// </summary> /// <remarks>The scaling here is only intended for small variations (between 0.6x and 1.4x). The /// Source image should be scaled if you are outside of this range</remarks> private bool AlphaMapImageToTileScaled(RawImageInterleaved_UInt8 img, ICachedTile tile, Quad imageArea, Quad tileArea) { // This needs to be improved var dst = tile?.GetTileData(); var src = img?.Data; if (src == null || dst == null || imageArea == null || tileArea == null) { return(false); } if (img.Width < 1 || img.Height < 1) { return(false); } bool changed = false; // start and end limits on tile int x0 = (int)Math.Max(tileArea.X, 0); int x1 = (int)Math.Min(tileArea.X + tileArea.Width, TileImageSize); int y0 = (int)Math.Max(tileArea.Y, 0); int y1 = (int)Math.Min(tileArea.Y + tileArea.Height, TileImageSize); int dst_width = x1 - x0; int dst_height = y1 - y0; if (dst_width < 1 || dst_height < 1) { return(false); } double scale_x = imageArea.Width / dst_width; double scale_y = imageArea.Height / dst_height; var imgyo = imageArea.Y; var imgxo = imageArea.X; // AA caches int[] src_aa = new int[(img.Width + 1) * 4]; for (int y = y0; y < y1; y++) { var img_yi = y - y0; // prepare a scaled row here AntiAliasRow(img, img_yi, scale_y, imgyo, src, src_aa); // copy image row for (int x = x0; x < x1; x++) { var img_xi = x - x0; // Measure AA var xoff_f = Range(0.0, (img_xi * scale_x) + imgxo, img.Width); var xoff = (int)xoff_f; double x_frac = xoff_f - xoff; var imul = (int)(255 * x_frac); var mul = 255 - imul; var src_xi = xoff * 4; var src_i = Range(0, src_xi, src_aa.Length - 4); // offset into source raw image var src_i2 = Range(0, src_xi + 4, src_aa.Length - 4); // offset into source raw image var dst_i = y * (TileImageSize * 4) + (x * 4); // offset into tile data if (dst_i < 0) { continue; } if (dst_i >= dst.Length) { continue; } // Threshold alpha if (src_aa[src_i + 3] < 2 && src_aa[src_i2 + 3] < 2) { continue; } // Take source samples and do AA var srcB = ((src_aa[src_i + 0] * mul) + (src_aa[src_i2 + 0] * imul)) >> 8; var srcG = ((src_aa[src_i + 1] * mul) + (src_aa[src_i2 + 1] * imul)) >> 8; var srcR = ((src_aa[src_i + 2] * mul) + (src_aa[src_i2 + 2] * imul)) >> 8; var srcA = ((src_aa[src_i + 3] * mul) + (src_aa[src_i2 + 3] * imul)) >> 8; var newAlpha = srcA / 255.0f; var oldAlpha = 1.0f - newAlpha; // Alpha blend over existing color // This for plain alpha: //dst[dst_i + 0] = Clip((dst[dst_i + 0] * oldAlpha) + (src[src_i + 0] * newAlpha)); //dst[dst_i + 1] = Clip((dst[dst_i + 1] * oldAlpha) + (src[src_i + 1] * newAlpha)); //dst[dst_i + 2] = Clip((dst[dst_i + 2] * oldAlpha) + (src[src_i + 2] * newAlpha)); // This for pre-multiplied alpha dst[dst_i + 0] = Clip((dst[dst_i + 0] * oldAlpha) + srcB); dst[dst_i + 1] = Clip((dst[dst_i + 1] * oldAlpha) + srcG); dst[dst_i + 2] = Clip((dst[dst_i + 2] * oldAlpha) + srcR); dst[dst_i + 3] = 255; // tile alpha is always 100% changed = true; } } return(changed); }
/// <summary> /// Draw an entire image onto the canvas, scaled to fit in the given rectangle. /// Expects screen-space co-ordinates. /// <para></para> /// Returns false if any of the tiles couldn't be written to /// </summary> public bool ImportBytesScaled(RawImageInterleaved_UInt8 img, int left, int top, int right, int bottom) { if (img == null) { return(false); } if (left >= right || top >= bottom) { return(true); } var tl = ScreenToCanvas(left, top); var br = ScreenToCanvas(right, bottom); var tileLeft = tl.TilePosition?.X ?? throw new Exception("Invalid tile calculation"); var tileTop = tl.TilePosition.Y; var tileRight = br.TilePosition?.X ?? throw new Exception("Invalid tile calculation"); var tileBottom = br.TilePosition.Y; double targetNativeTop = ((double)tileTop * TileImageSize) + tl.Y; double targetNativeBottom = ((double)tileBottom * TileImageSize) + br.Y; double targetNativeLeft = ((double)tileLeft * TileImageSize) + tl.X; double targetNativeRight = ((double)tileRight * TileImageSize) + br.X; double scale_x = img.Width / (targetNativeRight - targetNativeLeft); double scale_y = img.Height / (targetNativeBottom - targetNativeTop); // scan through the tiles we cover, and scale segments to fit. for (int ty = tileTop; ty <= tileBottom; ty++) { // see if we're at the top or bottom of the tiles covered var localNativeTop = (double)ty * TileImageSize; var localNativeBottom = ((double)ty + 1) * TileImageSize; var tgtTop = (localNativeTop < targetNativeTop) ? tl.Y : 0; var tgtHeight = (localNativeBottom > targetNativeBottom) ? br.Y : TileImageSize; tgtHeight -= tgtTop; for (int tx = tileLeft; tx <= tileRight; tx++) { // see if we're at the left or right of the tiles covered var localNativeLeft = (double)tx * TileImageSize; var localNativeRight = ((double)tx + 1) * TileImageSize; var tgtLeft = (localNativeLeft < targetNativeLeft) ? tl.X : 0; var tgtWidth = (localNativeRight > targetNativeRight) ? br.X : TileImageSize; tgtWidth -= tgtLeft; // calculate what area of the image to use var imgLeft = localNativeLeft - targetNativeLeft; var imgTop = localNativeTop - targetNativeTop; if (imgLeft < 0) { imgLeft = 0; } if (imgTop < 0) { imgTop = 0; } // Ensure the tile is ready to be drawn on: var key = new PositionKey(tx, ty); var ok = PrepareTileForDraw(key, out var tile); if (!ok) { Logging.WriteLogMessage($"Preparing tile failed: {tx}, {ty}"); return(false); } // This could be kicked out to threads?: var changed = AlphaMapImageToTileScaled(img, tile, imageArea: new Quad(imgLeft, imgTop, tgtWidth * scale_x, tgtHeight * scale_y), tileArea: new Quad(tgtLeft, tgtTop, tgtWidth, tgtHeight) ); if (changed) { _lastChangedTiles.Add(key); tile.Invalidate(); tile.SetState(TileState.Ready); ThreadPool.UnsafeQueueUserWorkItem(xkey => { WriteTileToBackingStoreSync((PositionKey)xkey, tile); }, key); } } } return(true); }