Creating rotated text in WPF - c#

I am trying to create some rotated text and save that image to a PNG file. The resulting PNG should be no larger than needed (or minimal padding). I have it working as long as there is no rotation, but as soon as I rotate the text, it is getting clipped off in the file. I am sure it has something to do with adjusting the either the CenterX and CenterY of the RotateTransform or creating a TranslateTransform, but I can't find anything on how to do it correctly and my trial-and-error testing has turned into trial-and-frustration.
My sample code is below. I looking for a solution that would work with an arbitrary angle and not just -45 degrees.
Finally, if someone knows how to meet these requirements, but say using an "old style" Graphics object instead of WPF tools, I am open to that solution to that too.
private static void CreateImageFile()
{
FormattedText ft;
Geometry textBox;
string fontName;
Typeface face;
DrawingVisual viz;
RotateTransform rt;
TranslateTransform tt;
Rect rect;
RenderTargetBitmap bmp;
PngBitmapEncoder encoder;
ft = CreateText("Lorem ipsum dolor sit amet, consectetur adipisicing" + Environment.NewLine + "elit, sed do eiusmod tempor", "Verdana", 12, false, false);
textBox = ft.BuildHighlightGeometry(new Point());
fontName = "Arial";
face = new Typeface(fontName);
// now create the visual we'll draw them to
viz = new DrawingVisual();
rt = new RotateTransform() { Angle = -45 };
rect = rt.TransformBounds(ft.BuildHighlightGeometry(new Point(0, 0)).Bounds);
using (DrawingContext dc = viz.RenderOpen())
{
dc.PushTransform(rt);
dc.DrawText(ft, new Point(0, 0));
dc.Pop();
}
bmp = new RenderTargetBitmap((int)rect.Width, (int)rect.Height, 96, 96, PixelFormats.Pbgra32);
bmp.Render(viz);
encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bmp));
using (FileStream file = new FileStream("TextImage.png", FileMode.Create))
encoder.Save(file);
}
private static FormattedText CreateText(string text, string typeface, double fontSize, bool bold, bool italic)
{
FontStyle fontStyle = FontStyles.Normal;
FontWeight fontWeight = FontWeights.Medium;
if (bold == true) fontWeight = FontWeights.Bold;
if (italic == true) fontStyle = FontStyles.Italic;
// Create the formatted text based on the properties set.
FormattedText formattedText = new FormattedText(
text,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(new FontFamily(typeface),
fontStyle,
fontWeight,
FontStretches.Normal),
fontSize,
Brushes.Black, // This brush does not matter since we use the geometry of the text.
null,
TextFormattingMode.Display
);
return formattedText;
}
Update
Based upon some of the suggestions below, I decided to try a different tack and experiment in the GUI. I created a window like this:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="160" Width="160" Loaded="Window_Loaded">
<Grid>
<Canvas Name="WorkCanvas" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock RenderTransformOrigin="0.5,0.5">
<TextBlock.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="-45"/>
<TranslateTransform/>
</TransformGroup>
</TextBlock.RenderTransform>This is a test</TextBlock>
</Canvas>
</Grid>
</Window>
As you can see, it uses both the RotateTransform and the suggested RenderTransformOrigin, and the result is like the image below. And, as you can see, the text does not go through the middle of the Canvas. And that seems to be my entire problem. How to rotate the text and get it correctly centered.
Update 2
I decided to try a Grid instead of a Canvas this time and I can now get the text properly centered in the grid, but since I can't use a FormattedText object, I can't seem to measure the actual bounding box. All measurements of the TextBlock or the Grid come back as if it was not rotated at all (looking at ActualWidth, ActualHeight, and DesiredSize). If I can't get the rotated bounding box size, I can't save the PNG without it getting clipped.
Oh, and I tried rotating the text in an unrotated grid and rotating the grid itself, both give the same results when trying to determine the dimensions.

You could try to wrap your text in an element that has a rendertransformOrigin. Make you changes to that element. Try a canvas or a grid.

I think what you are missing is that you need to set RenderTransformOrigin to 0.5,0.5 so that your rotation transformation is around the center of you image and not the upper left-hand edge.
Update
In response to your update above. The problem is using canvas. If you remove your transform altogether, you'll see your TextBlock isn't centered to start with. It actually is rotating around it's center, it's just that the center isn't the center of the canvas. Try this:
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock TextAlignment="Center" RenderTransformOrigin="0.5,0.5">
<TextBlock.RenderTransform>
<RotateTransform Angle="-45"/>
</TextBlock.RenderTransform>
This is a test
</TextBlock>
</Grid>

After much poking around and with help from Matt and kbo4sho88, I finally found the correct way of doing it. In addition to the help from the other posters, I finally found that I need to call TransformToVisual and TransformBounds to get the bounding box that I need for the correct file size. But, before that, I had to call Measure and Arrange since these objects are not shown on a screen.
Phew!
private static void CreateImageFile()
{
Grid workGrid;
TextBlock workTextBlock;
RenderTargetBitmap bitmap;
PngBitmapEncoder encoder;
Rect textBlockBounds;
GeneralTransform transform;
workGrid = new Grid()
{
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
workTextBlock = new TextBlock()
{
Text = "Lorem ipsum dolor sit amet, consectetur adipisicing" + Environment.NewLine + "elit, sed do eiusmod tempor",
FontFamily = new FontFamily("Verdana"),
FontSize = 36,
TextAlignment = TextAlignment.Center,
RenderTransformOrigin = new Point(0.5, 0.5),
LayoutTransform = new RotateTransform(-45)
};
workGrid.Children.Add(workTextBlock);
/*
* We now must measure and arrange the controls we just created to fill in the details (like
* ActualWidth and ActualHeight before we call TransformToVisual() and TransformBounds()
*/
workGrid.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
workGrid.Arrange(new Rect(0, 0, workGrid.DesiredSize.Width, workGrid.DesiredSize.Height));
transform = workTextBlock.TransformToVisual(workGrid);
textBlockBounds = transform.TransformBounds(new Rect(0, 0, workTextBlock.ActualWidth, workTextBlock.ActualHeight));
/*
* Now, create the bitmap that will be used to save the image. We will make the image the
* height and width we need at 96DPI and 32-bit RGBA (so the background will be transparent).
*/
bitmap = new RenderTargetBitmap((int)textBlockBounds.Width, (int)textBlockBounds.Height, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(workGrid);
encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using (FileStream file = new FileStream("TextImage.png", FileMode.Create))
encoder.Save(file);
}
Of course this is just a sample (but working) method and the final one will be parameterized.
Final part of the puzzle was found at WPF: Getting new coordinates after a Rotation

The way I generally handle custom rendering is using my own FrameworkElement and overriding the OnRender method. So, as an example:
using System;
using System.Windows; // For the FrameworkElement baseclass
using System.Windows.Media; // To get the drawing context
public class CustomThing : FrameworkElement
{
// Constructor
CustomThing()
{
}
// Custom render code called whenever control is invalidated
protected override OnRender(DrawingContext context)
{
if (!this.IsVisible || this.ActualHeight <= 0 || this.ActualWidth <= 0)
{
return; // Don't do anything if this thing isn't renderable
}
Typeface tf = new Typeface(this.FontFamily, FontStyles.Normal, this.FontWeight, FontStretches.Normal);
FormattedText fText = new FormattedText(this.Text, System.Globalization.CultureInfo.CurrentCulture, FlowDirection.LeftToRight, tf, this.FontSize, this.Foreground);
// You could have accessors so that the various properties such as Fonts, etc. are
// Properties of the class, and using DependencyProperties, they can be set so they
// automatically cause an invalidation when they change.
double txWidth = fText.Width;
double txHeight = fText.Height;
// This measures the text
double w = this.ActualWidth;
double h = this.ActualHeight;
double w2 = w / 2.0;
double h2 = h / 2.0;
// Get the center point for the rotation
// In this case, the center of the control
Transform trans = new RotateTransform(-90.0, w2, h2);
// The transform is for counter-clockwise 90 degrees, centered
// in the center of the control.
context.PushTransform(trans);
// All drawing operations will be performed with the
// transformation applied
Point txPos = new Point(w2 - (txWidth / 2.0), h2 - (txHeight / 2.0));
// The render origin for the text is the upper left
// hand corner of the bounding box. This uses the same origin for
// the rotation, then shifts it by half the dimensions of the text.
context.DrawText(fText, txPos);
context.Pop();
// The pop method is only needed if you need to continue work
// with the drawing context and don't want the transform
// operating on future rendering.
}
}
There are other details, such as if you want the control to be mouse interactive (include a using System.Windows.Input), then you'll want to start the render by painting the entire control region a background color. The main thing is you don't need to measure the shape of the rotated text, only the unrotated text. If you want to position the final text in any other place besides the center of the control, just make sure your rotation center is also the same reference point as your text offset.
The other thing I typically do is to use Dependency Properties so that all the styling can be done in XAML. This way, the custom object can be previewed in the designer while changing the properties dynamically, a typical entry for an item would look like so:
public static readonly RoutedEvent TextChangedEvent = EventManager.RegisterRoutedEvent("TextChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler<string>), typeof(CustomThing));
public event RoutedPropertyChangedEventHandler<string> TextChanged
{
add { AddHandler(TextChangedEvent, value); }
remove { RemoveHandler(TextChangedEvent, value); }
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(CustomThing), new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.AffectsRender, new PropertyChangedCallback(OnTextChanged)));
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
private static void OnTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
CustomThing cntrl = (CustomThing)obj;
RoutedPropertyChangedEventArgs<string> e = new RoutedPropertyChangedEventArgs<string>((string)args.OldValue, (string)args.NewValue, TextChangedEvent);
cntrl.OnTextChanged(e);
}
protected virtual void OnTextChanged(RoutedPropertyChangedEventArgs<string> e)
{
RaiseEvent(e);
}

Related

Clip an ellipse in WPF - positioning off

I am trying to 'cut' of part of several ellipses, in what is quite a big program. Since I had troubles making this work, I have started a new project in which I tried to solve the solution on a small scale. Again, I get some very weird results - which I expect have to do with positioning. Please see the below code for a minimal working example.
The program creates an ellipse, gives it a pretty colour and places it on the stage. It then proceeds to create what is called a 'RectangleGeometry', which we will use for the clipping. Please note that the geometry is placed at 0,0 with a width of 40 and height of 200. The result can be seen in the following screenshot;
My end goal is this: to be able to place a RectangleGeometry at any position (lets say 200,300) and have it clip the ellipse at exactly that position.
Ellipse abcEllipse = new Ellipse{
Margin = new Thickness(100, 100, 0, 0),
Fill = Brushes.HotPink,
Height = 80,
Width = 60
};
DrawCanvas.Children.Add(abcEllipse);
RectangleGeometry clipRectangle = new RectangleGeometry {
Rect = new Rect(0, 0, 40, 200)
};
GeometryGroup myGeometryGroup1 = new GeometryGroup();
myGeometryGroup1.Children.Add(clipRectangle);
Path myPath1 = new Path { Stroke = Brushes.Black, StrokeThickness = 1 };
SolidColorBrush mySolidColorBrush = new SolidColorBrush();
mySolidColorBrush.Color = Color.FromArgb(255, 204, 204, 255);
myPath1.Opacity = 0.2;
myPath1.Fill = mySolidColorBrush;
myPath1.Data = myGeometryGroup1;
DrawCanvas.Children.Add(myPath1);
abcEllipse.Clip = clipRectangle;
Update: Some more clarification might indeed be required; I want to achieve the following effect - as seen in the attached image. An ellipse is placed on the stage, it is rotated and the top of the ellipse is cut off. (In such a way that it 'respects' the new rotation and thus cuts at what is now the new top)
However; this is the result I get when I apply the knowledge that "the clip geometry assigned to the shape will be relative to that shape;". As you can see, it doesn't really cut at the 'new' top of the ellipse.
Update 2: Added the code used to rotate the ellipse;
TransformGroup tg = new TransformGroup();
tg.Children.Add(new RotateTransform(40));
abcEllipse.RenderTransform = tg;

Create image from WPF System.Windows.Shapes.Path

I have a Path. It displays correctly in a ContentControl:
<ContentControl
Name="PathContainer"
Content="{Binding Path}"
RenderTransform="{StaticResource ScaleTransform}"
Width="Auto"
Height="Auto"
Background="Transparent"
/>
But I'd like to copy it to the clipboard. The following creates what appears to be a solid black bitmap of the correct size. If I change the brush to White, it creates a solid white bitmap of the correct size. If, instead of creating this Canvas, I render from the actual ContentControl which hosts the Path in the UI, I get a bitmap of the correct size, with the Path drawn on it, but with what appears to be an opaque black background (unless I change the background to PapayaWhip or something, which does what you'd expect).
When I say "appears to be an opaque black background", I mean that when I copy it to the clipboard with System.Windows.Clipboard.CopyImage() and paste it into the VS icon or bitmap editors, the background of the pasted image is black rather than transparent, and the anti-aliased pixels that ought to be semi-transparent are the color they'd appear to be on a black background.
So, two questions:
First: Is the following code broken, or am I just trying to do something you can't do? It doesn't throw any exceptions, FWIW in WPF.
Second: How do you make a bitmap with an alpha channel in WPF? Should I quit wasting my time on all these labor-saving abstractions, whack up a BITMAPINFOHEADER, and create the ARGB quads in a loop by hand?
public BitmapSource CreateImage(Transform tfm)
{
var path = CreatePath();
var rcBounds = path.Data.GetRenderBounds(GetPen());
if (null != tfm)
{
rcBounds = tfm.TransformBounds(rcBounds);
}
int pxWidth = (int)Math.Round(rcBounds.Width);
int pxHeight = (int)Math.Round(rcBounds.Height);
var canvas = new System.Windows.Controls.Canvas();
canvas.Width = pxWidth;
canvas.Height = pxHeight;
canvas.Margin = new System.Windows.Thickness(0);
canvas.Background = Brushes.Transparent;
canvas.Measure(new Size(canvas.Width, canvas.Height));
canvas.Arrange(new Rect(new Size(canvas.Width, canvas.Height)));
canvas.UpdateLayout();
canvas.Children.Add(path);
canvas.RenderTransform = tfm;
RenderTargetBitmap rtb = new RenderTargetBitmap(pxWidth, pxHeight,
96, 96, PixelFormats.Pbgra32);
rtb.Render(canvas);
return BitmapFrame.Create(rtb);
}
There seems to be no need for the Canvas. You could directly render the Path into the RenderTargetBitmap, like e.g. the following blue circle with transparent background:
var path = new Path
{
Data = new EllipseGeometry(new Point(100, 100), 100, 100),
Fill = Brushes.Blue
};
var bounds = path.Data.GetRenderBounds(null);
path.Measure(bounds.Size);
path.Arrange(bounds);
var bitmap = new RenderTargetBitmap(
(int)bounds.Width, (int)bounds.Height, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(path);

AdornerLayer goes outside Border if I zoom the picture WPF

I created the logic that crops an image that is contained inside a border that is inside a grid. The grid has many borders, so this grid will have many pictures. The problem is that when I zoom the picture the logic zoomed the picture (which is okay) but when I use the crop logic the AdornerLayer goes outside the border like the picture:
On this image the pic doesn't have zoom, so the AdornerLayer is correct:
The code that I'm using to add the crop to the image:
private void AddCropToElement(FrameworkElement fel, System.Drawing.Image img)
{
if (!cropElements.ContainsKey(Convert.ToString(((Image)fel).Source)))
{
if (_felCur != null)
{
RemoveCropFromCur();
}
rcInterior = new Rect(
fel.ActualWidth * 0.2,
fel.ActualHeight * 0.2,
fel.ActualWidth * 0.6,
fel.ActualHeight * 0.6);
rectMoving = false;
Rect newRect = scaleRect(rcInterior, img);
imgCropMove = img;
AdornerLayer aly = AdornerLayer.GetAdornerLayer(fel);
_clp = new CroppingAdorner(fel, rcInterior);
aly.Add(_clp);
cropElements.Add(Convert.ToString(((Image)fel).Source), fel);
imageCropped = _clp.Crop(new System.Drawing.Bitmap(img), newRect);
_clp.CropChanged += HandleCropChanged;
_felCur = fel;
}
}
In this case the object named fel is the picture that I want to crop and the Border is his parent.
How I can fix the problem of the AdornerLayout that goes outside if the image is zoomed?
Are you using the default Window Adorner or have you created a custom AdornerDecorator around your Border in your XAML?
<AdornerDecorator>
<Border>...</Border>
</AdornerDecorator>
Additionally, if you are applying a zoom factor on your Border, you can add a Binding on your cropping display rectangle to match the Scale on your Border object.

How to generate a image with text and images in C#

I have several paragraphs of text and couple of pictures between these paragraphs.
Now, I want to generate a picture using these materials, merging them vertically. But all the blocks of the text and pictures can not have bigger width than that of the generating picture, which means I have to zoom out the origin pictures, and fill each paragraph of text into a rectangle to fit the width.
Here is the tough thing:
To figure out the size of the rectangle to contain the text, I need use Graphics.MeasureString() method, which needs an instance of Graphics used to generate my picture(now, I'm using a blank template picture). But I do not know the exact size of this Graphics until I figure out all the sizes of rectangles and pictures.
Is there any method to get an instance of Graphics without source image?
Or is there any other method to do this work?
hope this could help dude .
http://chiragrdarji.wordpress.com/2008/05/09/generate-image-from-text-using-c-or-convert-text-in-to-image-using-c/
https://web.archive.org/web/20131231000000/http://tech.pro/tutorial/654/csharp-snippet-tutorial-how-to-draw-text-on-an-image
http://www.codeproject.com/Questions/388845/HOW-TO-MAKE-HIGH-QAULITY-IMAGE-WITH-TEXT-IN-Csharp
thank you
For people how are intrested in a WPF solution (as asked):
public static BitmapSource CreateImage(string text, double width, double heigth)
{
// create WPF control
var size = new Size(width, heigth);
var stackPanel = new StackPanel();
var header = new TextBlock();
header.Text = "Header";
header.FontWeight = FontWeights.Bold;
var content = new TextBlock();
content.TextWrapping = TextWrapping.Wrap;
content.Text = text;
stackPanel.Children.Add(header);
stackPanel.Children.Add(content);
// process layouting
stackPanel.Measure(size);
stackPanel.Arrange(new Rect(size));
// Render control to an image
RenderTargetBitmap rtb = new RenderTargetBitmap((int)stackPanel.ActualWidth, (int)stackPanel.ActualHeight, 96, 96, PixelFormats.Pbgra32);
rtb.Render(stackPanel);
return rtb;
}

Replace Image with cropped BitmapSource

I have an Image on my wpf control
and I am trying to generate croped part of it - this is ok more or less.
I have used a codeproject solution to generate BitmapSource of croped image (http://www.codeproject.com/KB/WPF/CropAdorner.aspx) but when I am trying to
replace current image with generated BitmapSource like this
imgCurrent.Source = generatedBitmapSource;
I see very strange behaviour ((
I need an advice how to change current Image with new based on BitmapSource.
my XAML(there is nothing extraordinary - and by the right click I am trying to replace currentImage with croped):
<DockPanel Height="395" Width="926">
<!--Went with a DockPanel here so that the image would always be centered in its parent control.-->
<Image x:Name="imgCurrent" VerticalAlignment="Center" HorizontalAlignment="Center" MouseRightButtonDown="imgCurrent_MouseRightButtonDown"/>
</DockPanel>
right click:
private void imgCurrent_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
generatedBitmapSource = _clp.BpsCrop();
//this clears croping adonder
AdornerLayer aly = AdornerLayer.GetAdornerLayer(_felCur);
aly.Remove(_clp);
//
imageCurrent.Source = generatedBitmapSource;
}
croping method (from codeproject):
public BitmapSource BpsCrop()
{
Thickness margin = AdornerMargin();
Rect rcInterior = _prCropMask.RectInterior;
Point pxFromSize = UnitsToPx(rcInterior.Width, rcInterior.Height);
// It appears that CroppedBitmap indexes from the upper left of the margin whereas RenderTargetBitmap renders the
// control exclusive of the margin. Hence our need to take the margins into account here...
Point pxFromPos = UnitsToPx(rcInterior.Left + margin.Left, rcInterior.Top + margin.Top);
Point pxWhole = UnitsToPx(AdornedElement.RenderSize.Width + margin.Left, AdornedElement.RenderSize.Height + margin.Left);
pxFromSize.X = Math.Max(Math.Min(pxWhole.X - pxFromPos.X, pxFromSize.X), 0);
pxFromSize.Y = Math.Max(Math.Min(pxWhole.Y - pxFromPos.Y, pxFromSize.Y), 0);
if (pxFromSize.X == 0 || pxFromSize.Y == 0)
{
return null;
}
System.Windows.Int32Rect rcFrom = new System.Windows.Int32Rect(pxFromPos.X, pxFromPos.Y, pxFromSize.X, pxFromSize.Y);
RenderTargetBitmap rtb = new RenderTargetBitmap(pxWhole.X, pxWhole.Y, s_dpiX, s_dpiY, PixelFormats.Default);
rtb.Render(AdornedElement);
return new CroppedBitmap(rtb, rcFrom);
}
Are you sure you don't have issue with your "BitmapSource of croped image"? Can you for test purpose replace it with another valid Bitmap and try if it works. If it works with another one, but not "BitmapSource of croped image" then maybe you have issue with creating "BitmapSource of croped image".

Categories