Reputation: 2146
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;
}
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.
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.
Upvotes: 2
Views: 7744
Reputation: 1
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);
}
Upvotes: 0
Reputation: 2146
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
Upvotes: 2
Reputation: 45135
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>
Upvotes: 1
Reputation: 175
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.
Upvotes: 2