Related
I am running the latest version and I am getting the error as preceding.
Severity Code Description Project File Line Suppression State Error
CS1061 'IImageProcessingContext' does not contain a definition for
'ApplyScalingWaterMark' and no accessible extension method
'ApplyScalingWaterMark' accepting a first argument of type
'IImageProcessingContext' could be found (are you missing a using
directive or an assembly reference?) GitHubFuncs
C:\Sibeesh\Github\GitHubFuncs\GetLatestPosts.cs 39 Active
When I run the code below.
using(var img = Image.Load("canvas.jpg")) {
Font font = SystemFonts.CreateFont("Arial", 10);
using
var img2 = img.Clone(ctx => ctx.ApplyScalingWaterMark(font, feedItem.Summary.Text, Color.HotPink, 5, true));
img2.Save("images/wrapped.png");
}
Already added the using statements.
using SixLabors.Fonts;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
What am I missing here? Is this an issue with the latest version?
Finally, I got it working. I just had to add a few extension methods.
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Processing;
using System;
namespace GitHubFuncs.ExtensionMethods {
public static class ImageSharpExtensions {
public static IImageProcessingContext ApplyScalingWaterMark(this IImageProcessingContext processingContext,
Font font,
string text,
Color color,
float padding,
bool wordwrap) {
if (wordwrap) {
return processingContext.ApplyScalingWaterMarkWordWrap(font, text, color, padding);
} else {
return processingContext.ApplyScalingWaterMarkSimple(font, text, color, padding);
}
}
private static IImageProcessingContext ApplyScalingWaterMarkSimple(this IImageProcessingContext processingContext,
Font font,
string text,
Color color,
float padding) {
Size imgSize = processingContext.GetCurrentSize();
float targetWidth = imgSize.Width - (padding * 2);
float targetHeight = imgSize.Height - (padding * 2);
// measure the text size
FontRectangle size = TextMeasurer.Measure(text, new RendererOptions(font));
//find out how much we need to scale the text to fill the space (up or down)
float scalingFactor = Math.Min(imgSize.Width / size.Width, imgSize.Height / size.Height);
//create a new font
Font scaledFont = new Font(font, scalingFactor * font.Size);
var center = new PointF(imgSize.Width / 2, imgSize.Height / 2);
var textGraphicOptions = new TextGraphicsOptions() {
TextOptions = {
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
return processingContext.DrawText(textGraphicOptions, text, scaledFont, color, center);
}
private static IImageProcessingContext ApplyScalingWaterMarkWordWrap(this IImageProcessingContext processingContext,
Font font,
string text,
Color color,
float padding) {
Size imgSize = processingContext.GetCurrentSize();
float targetWidth = imgSize.Width - (padding * 2);
float targetHeight = imgSize.Height - (padding * 2);
float targetMinHeight = imgSize.Height - (padding * 3); // must be with in a margin width of the target height
// now we are working i 2 dimensions at once and can't just scale because it will cause the text to
// reflow we need to just try multiple times
var scaledFont = font;
FontRectangle s = new FontRectangle(0, 0, float.MaxValue, float.MaxValue);
float scaleFactor = (scaledFont.Size / 2); // every time we change direction we half this size
int trapCount = (int) scaledFont.Size * 2;
if (trapCount < 10) {
trapCount = 10;
}
bool isTooSmall = false;
while ((s.Height > targetHeight || s.Height < targetMinHeight) && trapCount > 0) {
if (s.Height > targetHeight) {
if (isTooSmall) {
scaleFactor = scaleFactor / 2;
}
scaledFont = new Font(scaledFont, scaledFont.Size - scaleFactor);
isTooSmall = false;
}
if (s.Height < targetMinHeight) {
if (!isTooSmall) {
scaleFactor = scaleFactor / 2;
}
scaledFont = new Font(scaledFont, scaledFont.Size + scaleFactor);
isTooSmall = true;
}
trapCount--;
s = TextMeasurer.Measure(text, new RendererOptions(scaledFont) {
WrappingWidth = targetWidth
});
}
var center = new PointF(padding, imgSize.Height / 2);
var textGraphicOptions = new TextGraphicsOptions() {
TextOptions = {
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
WrapTextWidth = targetWidth
}
};
return processingContext.DrawText(textGraphicOptions, text, scaledFont, color, center);
}
}
}
And in the end, I could use this method as preceding.
private static string WriteOnImage(SyndicationItem feedItem) {
using var img = Image.Load("images/canvas.jpg");
var font = SystemFonts.CreateFont("Arial", 20);
using var img2 = img.Clone(ctx => ctx.ApplyScalingWaterMark(font, feedItem.Summary.Text, Color.White, 5, true));
return img2.ToBase64String(PngFormat.Instance);
}
I have created a program to draw square grids on a selected image. It works fine for images that has small resolution, but it doesn't work properly on large images.
The all grid lines are not visible seem when the image is saved as file.
The image I am testing has resolution 3600x4320 and can be shown in the link.
How can I fix this problem?
My code:
Image drawGrid(int n, string imgPath)
{
Image img = Image.FromFile(imgPath);
Graphics grp = Graphics.FromImage(img);
grp.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
float m = img.Width * 1f / n;
for (int i = 1; i < n; i++)
{
var x = new PointF(i * m, 0);
var y = new PointF(i * m, img.Height);
grp.DrawLine(Pens.Red, x, y);
}
for (int i = 1; i <= (int)(this.Height / m); i++)
{
var x = new PointF(0, i * m);
var y = new PointF(img.Width, i * m);
grp.DrawLine(new Pen(Color.Red, 5f), x, y);
}
return img;
}
void BtnExportClick(object sender, EventArgs e)
{
if(saveFileDialog1.ShowDialog() == DialogResult.OK)
{
int n = (int)numericUpDown1.Value;
drawGrid(n, txtImagePath.Text).Save(saveFileDialog1.FileName, System.Drawing.Imaging.ImageFormat.Jpeg);
MessageBox.Show("Done");
}
}
The result image is below (resolution reduced to upload)
The grid lines not shown correctly.
The major problem is in this line:
for (int i = 1; i <= (int)(this.Height / m); i++)
▶ this.Height is clearly not what you wanted to write, let's replace it with the Image.Height
▶ grp.DrawLine(Pens.Red, x, y); and grp.DrawLine(new Pen(Color.Red, 5f), x, y); will draw lines of different size (1 and 5 pixels). In the sample code, the two methods accept Color and float arguments that define the Pen color and size.
▶ grp.SmoothingMode: we don't want any smoothing mode here, not needed to draw straight lines and it will add anti-alias which will be clearly visible, especially when saving the Image in JPEG format (it will anti-alias - sort of, it actually mangles the colors - these lines by itself).
▶ You're not disposing of any of the Graphics object you create. This is quite important with both frequent graphics operations and when working with large Bitmaps.
The Pen and Graphics objects needs to be disposed.
Since it's not exactly clear if you want to generate a grid that has the same number of lines in both dimensions - hence, a grid with Cells in which the Width is not equal to the Height, most probably - or a grid with squared Cells (this is what the sample Image seems to show, not the code), I posted two method that draw both grid types:
First method, same number of lines in both width and height:
var gridSizeX = (float)image.Width / lines;
var gridSizeY = (float)image.Height / lines;
private Image DrawGridLines(int lines, string imgPath, Color penColor, float penSize)
{
var image = Image.FromStream(new MemoryStream(File.ReadAllBytes(imgPath)), true);
using (var g = Graphics.FromImage(image)) {
g.PixelOffsetMode = PixelOffsetMode.Half;
var gridSizeX = (float)image.Width / lines;
var gridSizeY = (float)image.Height / lines;
for (int i = 1; i < lines; i++) {
var pointX1 = new PointF(0, i * gridSizeY);
var pointX2 = new PointF(image.Width, i * gridSizeY);
var pointY1 = new PointF(i * gridSizeX, 0);
var pointY2 = new PointF(i * gridSizeX, image.Height);
using (var pen = new Pen(penColor, penSize)) {
g.DrawLine(pen, pointX1, pointX2);
g.DrawLine(pen, pointY1, pointY2);
}
}
return image;
}
}
Second method, drawing a squared grid. The integer value, gridSection, is used to define a grid Cell based on the minimum dimension of the Bitmap.
This dimension is then used to determine how many lines to draw in the other dimension.
The grid size is calculated on the minimum dimension:
var gridSize = (float)Math.Min(image.Width, image.Height) / gridSection;
And the Cell are determined as a consequence:
var gridStepMin = Math.Min(image.Width, image.Height) / gridSize;
var gridStepMax = Math.Max(image.Width, image.Height) / gridSize;
private Image DrawSquaredGrid(int gridSection, string imgPath, Color penColor, float penSize)
{
var image = Image.FromStream(new MemoryStream(File.ReadAllBytes(imgPath)), true);
using (var g = Graphics.FromImage(image)) {
g.PixelOffsetMode = PixelOffsetMode.Half;
var gridSize = (float)Math.Min(image.Width, image.Height) / gridSection;
var gridStepMin = Math.Min(image.Width, image.Height) / gridSize;
var gridStepMax = Math.Max(image.Width, image.Height) / gridSize;
for (int i = 1; i < gridStepMin; i++) {
var pointY1 = new PointF(i * gridSize, 0);
var pointY2 = new PointF(i * gridSize, image.Height);
using (var pen = new Pen(penColor, penSize)) {
g.DrawLine(pen, pointY1, pointY2);
}
}
for (int i = 1; i < gridStepMax; i++) {
var pointX1 = new PointF(0, i * gridSize);
var pointX2 = new PointF(image.Width, i * gridSize);
using (var pen = new Pen(penColor, penSize)) {
g.DrawLine(pen, pointX1, pointX2);
}
}
return image;
}
}
The SaveFileDialog is refactored to allow multiple Image formats. and to call one of the drawing methods based on a selection (in the sample code, a CheckBox (chkSquared) is used select one of the Grid types).
You can add more formats, the ImageFormatFromFileName() methods selects the ImageFormat type based on the SaveFileDialog.FielName extension.
private void BtnExportClick(object sender, EventArgs e)
{
string imagePath = [Some Path];
using (var sfd = new SaveFileDialog()) {
sfd.Filter = "PNG Image (*.png)|*.png|TIFF Image (*.tif)|*.tif|JPEG Image (*.jpg)|*.jpg";
sfd.RestoreDirectory = true;
sfd.AddExtension = true;
if (sfd.ShowDialog() == DialogResult.OK) {
Image image = null;
if (chkSquared.Checked) {
image = DrawSquaredGrid((int)numericUpDown1.Value, imagePath, Color.Red, 5.0f);
}
else {
image = DrawGridLines((int)numericUpDown1.Value, imagePath, Color.Red, 5.0f);
}
image.Save(sfd.FileName, ImageFormatFromFileName(sfd.FileName));
MessageBox.Show("Done");
image.Dispose();
}
}
}
private ImageFormat ImageFormatFromFileName(string fileName)
{
string fileType = Path.GetExtension(fileName).Remove(0, 1);
if (fileType.Equals("tif")) fileType = "tiff";
if (fileType.Equals("jpg")) fileType = "jpeg";
return (ImageFormat)new ImageFormatConverter().ConvertFromString(fileType);
}
I have the following method that renders text into an image. It makes a larger than necessary bitmap, draws the text, then hunts the bitmap for blank space and crops it off. At the point where the image is saved, it throws the error "A generic error occurred in GDI+". This code has always worked on this same machine, that I develop on, though it hasn't been run in a long time so a reasonable amount of windows updates are likely to have occurred since the last time it worked. Nothing else has changed, to my knowledge re the solution/.net framework etc - I just opened the solution, ran it in debug (like always), and it produced the error
private void CreateImageFromText(string text, string filename){
// Set global stage dimensions
int stageWidth = (int)(text.Length * 3 * _fontSizeNumericUpDown.Value);
int stageHeight = (int)(3 * _fontSizeNumericUpDown.Value);
// Create Bitmap placeholder for new image
Bitmap createdImage = new Bitmap(stageWidth, stageHeight);
Color blankPixel = createdImage.GetPixel(0, 0);
// Draw new blank image
Graphics imageCanvas = Graphics.FromImage(createdImage);
imageCanvas.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
imageCanvas.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
// Add text
if (!string.IsNullOrEmpty(text))
{
Font font = new Font("Arial", (int)_fontSizeNumericUpDown.Value);
Font bigFont = new Font("Arial", (int)(_fontSizeNumericUpDown.Value * (decimal)1.25));
Font veryBigFont = new Font("Arial", (int)(_fontSizeNumericUpDown.Value * (decimal)3));
if(text.StartsWith("tick:"))
imageCanvas.DrawString("✔", bigFont, Brushes.Green, 0, 0);
else if (text.StartsWith("cross:"))
imageCanvas.DrawString("X", bigFont, Brushes.Red, 0, 0);
else if (text.StartsWith("highlight:"))
imageCanvas.DrawString("•", veryBigFont, Brushes.Magenta, 0, 0);
else
imageCanvas.DrawString(text, font, Brushes.Black, 0, 0);
}
//clip to only part containing text
Rectangle r = ImageUtils.GetBoundsThatContainData(
createdImage,
blankPixel,
searchArea: (text.StartsWith("highlight:") ? new Rectangle?(new Rectangle(10, 20, createdImage.Width - 10, createdImage.Height - 20)) : null)
);
// Save cropped
var img = createdImage.Clone(r, createdImage.PixelFormat);
img.Save(filename, System.Drawing.Imaging.ImageFormat.Png);
imageCanvas.Dispose();
createdImage.Dispose();
}
The helper method that searches for completely blank rows of pixels is:
public static Rectangle GetBoundsThatContainData(Bitmap createdImage, Color blankPixel, int borderSizePixels = 5, Rectangle? searchArea = null)
{
Rectangle sa = new Rectangle(0, 0, createdImage.Width, createdImage.Height);
if (searchArea.HasValue)
{
if (searchArea.Value.X > sa.X)
sa.X = searchArea.Value.X;
if (searchArea.Value.Y > sa.Y)
sa.Y = searchArea.Value.Y;
if (searchArea.Value.Width < sa.Width)
sa.Width = searchArea.Value.Width;
if (searchArea.Value.Height < sa.Height)
sa.Height = searchArea.Value.Height;
}
//look for vertical
for (int i = (sa.Y + sa.Height) - 1; i >= sa.Y; i--)
{
if (!AllPixelsOnHorizontalLineMatch(blankPixel, i, sa, createdImage))
{
sa.Height = (i - sa.Y) + 1 + borderSizePixels;
break;
}
}
if (sa.Y + sa.Height > createdImage.Height)
sa.Height = createdImage.Height - sa.Y;
//look for the horizontal
for (int i = (sa.X + sa.Width) - 1; i >= sa.X; i--)
{
if (!AllPixelsOnVerticalLineMatch(blankPixel, i, sa, createdImage))
{
sa.Width = (i - sa.X) + 1 + borderSizePixels;
break;
}
}
if (sa.X + sa.Width > createdImage.Width)
sa.Width = createdImage.Width - sa.X;
return sa;
}
The helper functions OK, returns me a rect I'm expecting.
Is anyone else able to repro the GDI error on their machine (I don't have another machine here to test as a compare to see if it's affecting just my machine)? Any pointers as to how to diagnose the cause? A read that a lot of these kinds of errors relate to closing the stream the bitmap is resting on, but in this case there is no stream; the bitmap isn't loaded from anywhere - it's created entirely in the code..
While the Graphics object exists, the image object is considered to be in a state of being edited. The image is only considered "done" after the graphics object is disposed. You attempt to save the image before disposing that Graphics object, and that can cause problem. Adding proper using blocks to your code should solve this problem completely.
Except, that is, if the real problem is in the AllPixelsOnHorizontalLineMatch or AllPixelsOnVerticalLineMatch tools, which you didn't include in your question. If they do something that might mess up the GDI+ object, then that can affect the saving you do later.
Anyway, here's your function rewritten with proper using blocks:
public static void CreateImageFromText(String text, String filename, Int32 fontSize)
{
// Set global stage dimensions
Int32 stageWidth = (Int32)(text.Length * 3 * fontSize);
Int32 stageHeight = (Int32)(3 * fontSize);
using (Bitmap createdImage = new Bitmap(stageWidth, stageHeight))
{
Color blankPixel = createdImage.GetPixel(0, 0);
// Draw new blank image
using (Graphics imageCanvas = Graphics.FromImage(createdImage))
{
imageCanvas.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
imageCanvas.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
// Add text
if (!string.IsNullOrEmpty(text))
{
if (text.StartsWith("tick:"))
using (Font bigFont = new Font("Arial", (Int32)(fontSize * (decimal)1.25)))
imageCanvas.DrawString("✔", bigFont, Brushes.Green, 0, 0);
else if (text.StartsWith("cross:"))
using (Font bigFont = new Font("Arial", (Int32)(fontSize * (decimal)1.25)))
imageCanvas.DrawString("X", bigFont, Brushes.Red, 0, 0);
else if (text.StartsWith("highlight:"))
using (Font veryBigFont = new Font("Arial", (Int32)(fontSize * (decimal)3)))
imageCanvas.DrawString("•", veryBigFont, Brushes.Magenta, 0, 0);
else
using (Font font = new Font("Arial", (Int32)fontSize))
imageCanvas.DrawString(text, font, Brushes.Black, 0, 0);
}
}
// Honestly not sure what the point of this is, especially given the complete inaccuracy of the original image size calculation.
Rectangle? searchArea = text.StartsWith("highlight:") ? new Rectangle(10, 20, createdImage.Width - 10, createdImage.Height - 20) : (Rectangle?)null;
Rectangle r = ImageUtils.GetCropBounds(createdImage, blankPixel, searchArea: searchArea);
// Save cropped
using (Image img = createdImage.Clone(r, createdImage.PixelFormat))
img.Save(filename, ImageFormat.Png);
}
}
I didn't feel like rewriting these missing tool functions, since it's much more efficient to work with bytes all the way through and pass those on to these tools function, so I just ended up writing my own crop function altogether. I'm not sure it does exactly what yours does, but the constrained search area and the border thing seemed to work, so here it is, for reference:
public static Rectangle GetCropBounds(Bitmap image, Color blankPixel, Int32 borderSizePixels = 5, Rectangle? searchArea = null)
{
// Not too worried about the other boundaries; the "for" loops will exclude those anyway.
Int32 yStart = searchArea.HasValue ? Math.Max(0, searchArea.Value.Y) : 0;
Int32 yEnd = searchArea.HasValue ? Math.Min(image.Height, searchArea.Value.Y + searchArea.Value.Height) : image.Height;
Int32 xStart = searchArea.HasValue ? Math.Max(0, searchArea.Value.X) : 0;
Int32 xEnd = searchArea.HasValue ? Math.Min(image.Width, searchArea.Value.X + searchArea.Value.Width) : image.Width;
// Values to calculate
Int32 top;
Int32 bottom;
Int32 left;
Int32 right;
// Convert to 32bppARGB and get bytes and stride out.
Byte[] data;
Int32 stride;
using (Bitmap bm = new Bitmap(image))
{
BitmapData sourceData = bm.LockBits(new Rectangle(0, 0, bm.Width, bm.Height), ImageLockMode.ReadOnly, bm.PixelFormat);
stride = sourceData.Stride;
data = new Byte[stride*bm.Height];
Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
bm.UnlockBits(sourceData);
}
// ============= Y =============
// Top = first found row which contains data
for (top = yStart; top < yEnd; top++)
{
Int32 index = top * stride;
if (!RowClear(data, index, 4, xStart, xEnd, blankPixel))
break;
}
// Sanity check: no data on image. Abort.
if (top == yEnd)
return new Rectangle(xStart, yStart, 0, 0);
// Bottom = last found row which contains data
for (bottom = yEnd - 1; bottom > top; bottom--)
{
Int32 index = bottom * stride;
if (!RowClear(data, index, 4, xStart, xEnd, blankPixel))
break;
}
// Make bottom the first actually clear row.
bottom++;
// ============= X =============
// Left = first found column which contains data
for (left = xStart; left < xEnd; left++)
{
Int32 index = left * 4;
if (!ColClear(data, index, stride, yStart, yEnd, blankPixel))
break;
}
// Right = last found row which contains data
for (right = xEnd - 1; right > left; right--)
{
Int32 index = right * 4;
if (!ColClear(data, index, stride, yStart, yEnd, blankPixel))
break;
}
// Make right the first actually clear column
right++;
// Calculate final rectangle values, including border.
Int32 rectX = Math.Max(0, left - borderSizePixels);
Int32 rectY = Math.Max(0, top - borderSizePixels);
Int32 rectW = Math.Min(image.Width, right + borderSizePixels) - rectX;
Int32 rectH = Math.Min(image.Height, bottom + borderSizePixels) - rectY;
return new Rectangle(rectX, rectY, rectW, rectH);
}
public static Boolean RowClear(Byte[] data, Int32 index, Int32 pixelWidth, Int32 xStart, Int32 xEnd, Color blankPixel)
{
Boolean rowOk = true;
Int32 start = index + pixelWidth * xStart;
Int32 end = index + pixelWidth * xEnd;
for (Int32 x = start; x < end; x += pixelWidth)
{
if (blankPixel.A != data[x + 3]) rowOk = false;
else if (blankPixel.R != data[x + 2]) rowOk = false;
else if (blankPixel.G != data[x + 1]) rowOk = false;
else if (blankPixel.B != data[x + 0]) rowOk = false;
if (!rowOk)
return false;
}
return true;
}
public static Boolean ColClear(Byte[] data, Int32 index, Int32 stride, Int32 yStart, Int32 yEnd, Color blankPixel)
{
Boolean colOk = true;
Int32 start = index + stride * yStart;
Int32 end = index + stride * yEnd;
for (Int32 y = start; y < end; y += stride)
{
if (blankPixel.A != data[y + 3]) colOk = false;
else if (blankPixel.R != data[y + 2]) colOk = false;
else if (blankPixel.G != data[y + 1]) colOk = false;
else if (blankPixel.B != data[y + 0]) colOk = false;
if (!colOk)
return false;
}
return true;
}
Note that you may want to use a more accurate way to determine the size needed for the image. The .Net framework has inbuilt methods for that. Also note that since you always paint to (0,0), the 5-pixel border that the crop function leaves tends to not work at the top. Given the complete inaccuracy of the original image size estimation, I also have no idea why the "highlight:" prefix gives that constraining rectangle (based on said inaccurate image size) to the crop function.
I messed around a little when fiddling with all that stuff, and wondered if the StartsWith calls actually meant that the symbols were supposed to act as prefix rather than the whole string... so I ended up implementing it that way. Here's the final rewritten function. It automatically does vertical centering of the smaller font on the larger one.
public static void CreateImageFromText(String text, String filename, Int32 fontSize, Int32 padding)
{
if (text == null)
text = String.Empty;
Boolean prefixTick = text.StartsWith("tick:");
Boolean prefixCross = !prefixTick && text.StartsWith("cross:");
Boolean highlight = !prefixTick && !prefixCross && text.StartsWith("highlight:");
const String symbTick = "✔";
const String symbCross = "X";
const String symbBullet = "•";
// Cut off the prefix part
if (prefixTick || prefixCross || highlight)
text = text.Substring(text.IndexOf(":", StringComparison.Ordinal) + 1).TrimStart();
using (Font font = new Font("Arial", fontSize))
using (Font prefixFont = new Font("Arial", fontSize * (highlight ? 3f : 1.25f), highlight ? FontStyle.Bold : FontStyle.Regular))
{
// Calculate accurate dimensions of required image.
Single textWidth;
Single prefixWidth = 0;
Single requiredHeight = 0;
Single textHeight;
Single prefixHeight = 0;
// Dummy image will have the same dpi as the final one.
using (Bitmap dummy = new Bitmap(1, 1))
using (Graphics g = Graphics.FromImage(dummy))
{
if (prefixTick)
{
SizeF tickSize = g.MeasureString(symbTick, prefixFont);
requiredHeight = Math.Max(tickSize.Height, requiredHeight);
prefixWidth = tickSize.Width;
}
else if (prefixCross)
{
SizeF crossSize = g.MeasureString(symbCross, prefixFont);
requiredHeight = Math.Max(crossSize.Height, requiredHeight);
prefixWidth = crossSize.Width;
}
else if (highlight)
{
SizeF bulletSize = g.MeasureString(symbBullet, prefixFont);
requiredHeight = Math.Max(bulletSize.Height, requiredHeight);
prefixWidth = bulletSize.Width;
}
prefixHeight = requiredHeight;
SizeF textSize = g.MeasureString(text.Length == 0 ? " " : text, font);
textWidth = text.Length == 0 ? 0 : textSize.Width;
textHeight= textSize.Height;
requiredHeight = Math.Max(textSize.Height, requiredHeight);
}
if (!prefixTick && !prefixCross && !highlight && text.Length == 0)
{
Int32 width = padding*2;
Int32 height = (Int32)Math.Round(textHeight + padding*2, MidpointRounding.AwayFromZero);
if (width == 0)
width = 1;
// Creates an image of the expected height for the font, and a width consisting of only the padding, or 1 for no padding.
using (Image img = new Bitmap(width, height))
img.Save(filename, ImageFormat.Png);
return;
}
Single prefixX = 5;
Single prefixY = 5 + padding + prefixWidth > 0 && requiredHeight > prefixHeight ? (requiredHeight - prefixHeight) / 2 : 0;
Single textX = 5 + prefixWidth;
Single textY = 5 + padding + requiredHeight > textHeight ? (requiredHeight - textHeight) / 2 : 0;
// Set global stage dimensions. Add 10 Pixels to each to allow for 5-pixel border.
Int32 stageWidth = (Int32)Math.Round(prefixWidth + textWidth, MidpointRounding.AwayFromZero) + 10 + padding * 2;
Int32 stageHeight = (Int32)Math.Round(requiredHeight, MidpointRounding.AwayFromZero) + 10 + padding * 2;
// Create Bitmap placeholder for new image
using (Bitmap createdImage = new Bitmap(stageWidth, stageHeight))
{
Color blankPixel = createdImage.GetPixel(0, 0);
// Draw new blank image
using (Graphics imageCanvas = Graphics.FromImage(createdImage))
{
imageCanvas.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
imageCanvas.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
// Add text
if (prefixTick)
imageCanvas.DrawString(symbTick, prefixFont, Brushes.Green, prefixX, prefixY);
else if (prefixCross)
imageCanvas.DrawString(symbCross, prefixFont, Brushes.Red, prefixX, prefixY);
else if (highlight)
imageCanvas.DrawString(symbBullet, prefixFont, Brushes.Magenta, prefixX, prefixY);
if (text.Length > 0)
imageCanvas.DrawString(text, font, Brushes.Black, textX, textY);
}
//clip to only part containing text.
Rectangle r = ImageUtils.GetCropBounds(createdImage, blankPixel, padding);
if (r.Width <= 0 || r.Height <= 0)
return; // Possibly throw exception; image formats can't handle 0x0.
// Save cropped
createdImage.Save(Path.Combine(Path.GetDirectoryName(filename), Path.GetFileNameWithoutExtension(filename)) + "_orig" + Path.GetExtension(filename), ImageFormat.Png);
using (Image img = createdImage.Clone(r, createdImage.PixelFormat))
img.Save(filename, ImageFormat.Png);
}
}
}
The question is simple, assuming you have a shape (lets say a rectangle) and a text ("hello" for example), so it'll write the text all accross the borders of the rectangle for as many times as it fits, for example:
hello hello hello hello
hello hello
hello hello
hello hello hello hello
In order to do it I assume you'd need to use a graphics variable, I just dont know how to do it.
A code for drawing a string in a bitmap object:
Bitmap tempp = new Bitmap(1, 1);
Graphics g = Graphics.FromImage(tempp);
SizeF w = g.MeasureString("22", new Font("Tahoma", 200));//in order to get the size of the string as a pixel measurement
Bitmap bmp = new Bitmap((int)w.Width+1, (int)w.Height+1);//the bitmap that will contain the text as a picture
RectangleF rectf = new RectangleF(0, 0, (int)w.Width+1, (int)w.Height+1);
g = Graphics.FromImage(bmp);
g.SmoothingMode = SmoothingMode.AntiAlias;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
StringFormat format = new StringFormat()
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center
};
g.DrawString("22", new Font("Tahoma", 200), Brushes.Black, rectf, format);
g.Flush();
Thanks in advance.
For second comment:
hello hello hello
hel llo
hel llo
hello hello hello
I hope this is what you need. There isn't much to explain here. The logic is pretty straightforward.
public string MyString = "Hello"
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var g = e.Graphics;
var strFont = new Font("Tahoma", 10);
var strSizeF = g.MeasureString(MyString, strFont);
var canvas = new Rectangle
{
Height = 200,
Width = 200,
Location = new Point(10, 10),
};
g.DrawRectangle(new Pen(new SolidBrush(Color.Blue)), canvas);
var nx = (int)(canvas.Width / strSizeF.Width);
var ny = (int)(canvas.Height / strSizeF.Height);
var spacingX = (canvas.Width - nx * strSizeF.Width) / (nx-1);
var spacingY = (canvas.Height - ny * strSizeF.Height) / (ny-1);
//draw top row and bottom row
int i;
for (i = 0; i < nx; i++)
{
g.DrawString(
MyString,
strFont,
Brushes.Black,
new PointF(canvas.X + i*(strSizeF.Width + spacingX), canvas.Y)
);
g.DrawString(
MyString,
strFont,
Brushes.Black,
new PointF(canvas.X + i * (strSizeF.Width + spacingX), canvas.Y + canvas.Height - strSizeF.Height)
);
}
//divide the string into half
var isLengthOdd = MyString.Length % 2 != 0;
var substr1 = MyString.Substring(0, MyString.Length / 2 + (isLengthOdd ? 1 : 0));
var substr2 = MyString.Substring(MyString.Length / 2, MyString.Length - MyString.Length / 2);
var substr2SizeF = g.MeasureString(substr2, strFont);
//draw side rows
for (i = 1; i < ny - 1; i++)
{
g.DrawString(
substr1,
strFont,
Brushes.Black,
new PointF(canvas.X, canvas.Y + i * (strSizeF.Height + spacingY))
);
g.DrawString(
substr2,
strFont,
Brushes.Black,
new PointF(canvas.X + canvas.Width - substr2SizeF.Width, canvas.Y + i * (strSizeF.Height + spacingY))
);
}
}
Result:
I have a method that takes in a bitmap object and overlays dates and times strings over it and returns that new bitmap. The code is below.
public static Bitmap overlayBitmap(Bitmap sourceBMP, int width, int height, List<String> times, List<String> dates, IEnumerable<Color> colors) {
// Determine the new width
float newWidth = width + (width / 3.0f);
float newHeight = height + (height / 3.0f);
// Intelligent vertical + horizontal text distance calculator
float verticalDistance = height / (times.Count - 1.0f);
float horizontalDistance = width / (dates.Count - 1.0f);
Bitmap result = new Bitmap((int)newWidth, (int)newHeight);
using (Graphics g = Graphics.FromImage(result)) {
// Background color
Brush brush = new SolidBrush(colors.First());
g.FillRectangle(brush, 0, 0, newWidth, newHeight);
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
// Times text configs
StringFormat stringFormatTimes = new StringFormat();
stringFormatTimes.LineAlignment = StringAlignment.Center;
stringFormatTimes.Alignment = StringAlignment.Center;
Font drawFontY = new Font("Whitney", newHeight / 70);
// Dates text configs
StringFormat stringFormatDates = new StringFormat();
stringFormatDates.LineAlignment = StringAlignment.Center;
stringFormatTimes.Alignment = StringAlignment.Center;
stringFormatDates.FormatFlags = StringFormatFlags.DirectionVertical;
Font drawFontX = new Font("Whitney", newHeight / 70);
// Location of times text
for (int i = 0; i < times.Count; i++) {
if (i % determineIncrementTimes(times.Count) == 0) {
g.DrawString(times[i], drawFontX, Brushes.White, (((newWidth - width) / 2) / 2), ((newHeight - height) / 2) + (verticalDistance * i), stringFormatTimes);
}
}
// Location of dates text
for (int i = 0; i < dates.Count; i++) {
if (i % determineIncrementDates(dates.Count) == 0) {
g.DrawString(dates[i], drawFontY, Brushes.White, ((newWidth - width) / 2) + (horizontalDistance * i), ((newHeight - height) / 2) + height, stringFormatDates);
}
}
// New X and Y Position of the sourceBMP within the new BMP.
int XPos = width / 6;
int YPos = height / 6;
// Int -> Float casting for the outline
float fXPos = width / 6.0f;
float fYPos = height / 6.0f;
float fWidth = width / 1.0f;
float fHeight = height / 1.0f;
// Draw new image at the position width/6 and height/6 with the size at width and height
g.DrawImage(sourceBMP, fXPos, fYPos, fWidth, fHeight);
g.DrawRectangle(Pens.White, fXPos, fYPos, fWidth, fHeight); // white outline
g.Dispose();
}
return result;
}
My concern is, I would like to be able, for the next developer, to easily access and set particular values that currently I've only "hardcoded" in. An example being the x-position of the time text calculated via this snippet of code:
(((newWidth - width) / 2) / 2)
Realistically I'd like to have the developer be able to access and/or set this value through simply typing in:
something.XPos = [someFloat];
How my method above is used (is pseudo-code) is as the following:
private readonly Bitmap _image;
private readonly Bitmap _overlayedImage;
public myConstructor(int someInputValues){
// some code that generates the first bitmap called _image
_newImage = overlayImage(_image, ....);
}
For reference this is the image drawn:
My question is - since some values need to be casted and initialized first, can I set my instance variables at the end of the method, before the closing brace?
public Bitmap overlayBitmap
{
get
{
// Build bitmap overlay
return overlayBitmapOutput;
}
...
}
[Edit: Answer Insufficient >> Wait]