Skip to content

Commit a83784a

Browse files
committed
Add feature to copy rectangular text selection as a bitmap image to clipboard.
This feature copies the rectangular text selection as a bitmap image to the clipboard, allowing users to paste it directly into any image editor or application that accepts images from the clipboard. The copied bitmap can then be saved or edited in various image formats supported by those tools.
1 parent 11a3c43 commit a83784a

File tree

3 files changed

+114
-3
lines changed

3 files changed

+114
-3
lines changed

src/CodeSnip/MainWindow.xaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,12 @@
419419
Width="16" Height="16" Stretch="Uniform" />
420420
</MenuItem.Icon>
421421
</MenuItem>
422+
<MenuItem Header="Image" Click="CopyAsImage_Click">
423+
<MenuItem.Icon>
424+
<Path Data="{StaticResource ImageSave}" Fill="{DynamicResource MahApps.Brushes.AccentBase}"
425+
Width="16" Height="16" Stretch="Uniform" />
426+
</MenuItem.Icon>
427+
</MenuItem>
422428
</MenuItem>
423429
<Separator />
424430
<MenuItem Header="Select All" InputGestureText="Ctrl+A" Click="SelectAll_Click">

src/CodeSnip/MainWindow.xaml.cs

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using CodeSnip.Services.Exporters;
44
using CodeSnip.Views.HighlightingEditorView;
55
using CodeSnip.Views.SnippetView;
6+
using ICSharpCode.AvalonEdit;
67
using ICSharpCode.AvalonEdit.Document;
78
using ICSharpCode.AvalonEdit.Folding;
89
using ICSharpCode.AvalonEdit.Indentation;
@@ -15,6 +16,8 @@
1516
using System.Windows;
1617
using System.Windows.Controls;
1718
using System.Windows.Input;
19+
using System.Windows.Media;
20+
using System.Windows.Media.Imaging;
1821

1922
namespace CodeSnip
2023
{
@@ -436,7 +439,7 @@ private async void FormatAll_Click(object sender, RoutedEventArgs e)
436439
}
437440
else
438441
{
439-
MessageBox.Show($"Formatting (rustfmt) failed:\n{ errorRust}");
442+
MessageBox.Show($"Formatting (rustfmt) failed:\n{errorRust}");
440443
}
441444
break;
442445

@@ -635,6 +638,25 @@ private void CopyAsBase64String_Click(object sender, RoutedEventArgs e)
635638
}
636639
}
637640

641+
private void CopyAsImage_Click(object sender, RoutedEventArgs e)
642+
{
643+
if (textEditor.TextArea.Selection is not ICSharpCode.AvalonEdit.Editing.RectangleSelection selection || selection.IsEmpty)
644+
{
645+
MessageBox.Show("This action requires a rectangular selection (Alt + Mouse Drag or Alt + Shift + Arrow Keys).", "No Rectangular Selection", MessageBoxButton.OK, MessageBoxImage.Information);
646+
return;
647+
}
648+
try
649+
{
650+
var bitmap = RenderSelectionToBitmap(selection);
651+
Clipboard.SetImage(bitmap);
652+
}
653+
catch (Exception ex)
654+
{
655+
MessageBox.Show($"Failed to copy selection as image: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
656+
Clipboard.SetText(ex.Message);
657+
}
658+
}
659+
638660
private void ExportToHtml_Click(object sender, RoutedEventArgs e)
639661
{
640662
if (mainViewModel != null && mainViewModel.SelectedSnippet != null)
@@ -726,6 +748,87 @@ private void SetupFolding(Snippet snippet)
726748
textEditor.Options.AllowScrollBelowDocument = foldingStrategy != null;
727749
}
728750

751+
private RenderTargetBitmap RenderSelectionToBitmap(ICSharpCode.AvalonEdit.Editing.RectangleSelection selection)
752+
{
753+
// 1. Get the selected text
754+
string selectedText = selection.GetText();
755+
756+
// 2. Create a new, off-screen TextEditor
757+
var virtualEditor = new TextEditor
758+
{
759+
// 3. Apply the same properties as the main editor
760+
FontFamily = textEditor.FontFamily,
761+
FontSize = textEditor.FontSize,
762+
Background = textEditor.Background,
763+
Foreground = textEditor.Foreground,
764+
SyntaxHighlighting = textEditor.SyntaxHighlighting,
765+
Text = selectedText,
766+
Options = textEditor.Options
767+
};
768+
769+
// 4. Force the layout to be calculated
770+
var viewbox = new Viewbox { Child = virtualEditor };
771+
viewbox.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
772+
viewbox.Arrange(new Rect(viewbox.DesiredSize));
773+
774+
// 5. Get the actual dimensions of the content
775+
double contentWidth = virtualEditor.TextArea.TextView.ActualWidth;
776+
double contentHeight = virtualEditor.TextArea.TextView.ActualHeight;
777+
778+
if (contentWidth <= 0 || contentHeight <= 0)
779+
{
780+
throw new InvalidOperationException("The selected content has no visible area to capture.");
781+
}
782+
783+
// Define padding and footer for the watermark ---
784+
double padding = 4;
785+
double footerHeight = 20;
786+
double totalWidth = contentWidth + (2 * padding);
787+
double totalHeight = contentHeight + (2 * padding) + footerHeight;
788+
789+
// Create a DrawingVisual to compose the final image
790+
var drawingVisual = new DrawingVisual();
791+
using (var dc = drawingVisual.RenderOpen())
792+
{
793+
// Layer 1: Draw the outer background/border
794+
// For a dark theme, a slightly lighter background works well.
795+
// For a light theme, a slightly darker one.
796+
var editorBg = (textEditor.Background as SolidColorBrush)?.Color ?? Colors.White;
797+
var borderColor = Color.Add(editorBg, Color.FromRgb(10, 10, 10));
798+
dc.DrawRectangle(new SolidColorBrush(borderColor), null, new Rect(0, 0, totalWidth, totalHeight));
799+
800+
// Layer 2: Draw the code itself by rendering the virtual editor
801+
var codeBrush = new VisualBrush(virtualEditor);
802+
dc.DrawRectangle(codeBrush, null, new Rect(padding, padding, contentWidth, contentHeight));
803+
804+
// Layer 3: Draw the watermark
805+
var watermarkText = new FormattedText(
806+
"Generated by CodeSnip",
807+
System.Globalization.CultureInfo.CurrentCulture,
808+
FlowDirection.LeftToRight,
809+
new Typeface(new FontFamily("Segoe UI"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
810+
12,
811+
new SolidColorBrush(Color.FromArgb(128, 170, 170, 170)), // Semi-transparent gray
812+
VisualTreeHelper.GetDpi(this).PixelsPerDip);
813+
814+
// Position it in the bottom right corner
815+
var watermarkPosition = new Point(totalWidth - padding - watermarkText.Width, totalHeight - padding - watermarkText.Height);
816+
dc.DrawText(watermarkText, watermarkPosition);
817+
}
818+
819+
// 6. Render the composed DrawingVisual to a bitmap
820+
var dpi = VisualTreeHelper.GetDpi(this);
821+
var rtb = new RenderTargetBitmap(
822+
(int)Math.Ceiling(totalWidth * dpi.DpiScaleX),
823+
(int)Math.Ceiling(totalHeight * dpi.DpiScaleY),
824+
dpi.PixelsPerInchX,
825+
dpi.PixelsPerInchY,
826+
PixelFormats.Pbgra32);
827+
828+
rtb.Render(drawingVisual);
829+
return rtb;
830+
}
831+
729832
private static string MapLangCodeToMarkdown(string code)
730833
{
731834
return code.ToLower() switch

src/CodeSnip/Resources/Icons.xaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,12 @@
117117
<PathGeometry x:Key="InfoCircle">M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z</PathGeometry>
118118

119119
<PathGeometry x:Key="FormatIndentDecrease">M 11,17 H 21 V 15 H 11 Z M 3,12 7,16 V 8 Z m 0,9 H 21 V 19 H 3 Z M 3,3 V 5 H 21 V 3 Z m 8,6 H 21 V 7 H 11 Z m 0,4 H 21 V 11 H 11 Z</PathGeometry>
120-
120+
121+
<PathGeometry x:Key="ImageSave">M22.71,6.29a1,1,0,0,0-1.42,0L20,7.59V2a1,1,0,0,0-2,0V7.59l-1.29-1.3a1,1,0,0,0-1.42,1.42l3,3a1,1,0,0,0,.33.21.94.94,0,0,0,.76,0,1,1,0,0,0,.33-.21l3-3A1,1,0,0,0,22.71,6.29ZM19,13a1,1,0,0,0-1,1v.38L16.52,12.9a2.79,2.79,0,0,0-3.93,0l-.7.7L9.41,11.12a2.85,2.85,0,0,0-3.93,0L4,12.6V7A1,1,0,0,1,5,6h8a1,1,0,0,0,0-2H5A3,3,0,0,0,2,7V19a3,3,0,0,0,3,3H17a3,3,0,0,0,3-3V14A1,1,0,0,0,19,13ZM5,20a1,1,0,0,1-1-1V15.43l2.9-2.9a.79.79,0,0,1,1.09,0l3.17,3.17,0,0L15.46,20Zm13-1a.89.89,0,0,1-.18.53L13.31,15l.7-.7a.77.77,0,0,1,1.1,0L18,17.21Z</PathGeometry>
122+
121123

122124

123-
<Geometry x:Key="ContentCopy">M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z</Geometry>
125+
<Geometry x:Key="ContentCopy">M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z</Geometry>
124126
<Geometry x:Key="CodeBrackets">M15,4V6H18V18H15V20H20V4M4,4V20H9V18H6V6H9V4H4Z</Geometry>
125127

126128
</ResourceDictionary>

0 commit comments

Comments
 (0)