Calculate coordinates for rotated text plus boundi

2019-07-20 09:09发布

问题:

I have a form that is going to allow a user to create custom "stamps" to place on a PDF. The form displays with a image of the first page of the pdf and I want the user to basically click on the screen where they want their stamp and be able to preview what its going to look like. Don't worry about any of the PDF stuff, I have that handled.

To make things snazzy, I have two copies of the image, the normal one and one with reduced brightness. I display the low brightness image and as the user moves the mouse over, a chunk of the original image is revealed or highlighted. I then display in that area the text the user is going to put on the PDF.

I allow the user to use the mousewheel to scroll and change the angle of the text they are placing (from -45 degrees to +45 degrees).

Here is my problem: I can't calculate the proper rectangles/coordinates. Sometimes everything looks great, other times (as font sizes change) they don't quite fit.

How do I calculate the x and y coordinates for: placement of the rotated text AND a bounding rectangle padding the text at its width and height with 10px

The code below works, until I start to crank up the font size, then everything gets out of skew.

First two images show text + bounding rectangle at smaller fonts. It looks good:

The next image shows that as the text size gets larger, my pixels are moving all around and gets chopped off. In even larger text, the widths/heights end being way off as well.

Sorry the example images don't show much detail. I have actual data that I can't share.

        Private Sub PanelMouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) '// handles the mouse move (handler added elsehwere)

        With CType(sender, PictureBox)

            .Image.Dispose() '// get rid of old image

            Dim b As Bitmap = _curGray.Clone '// the low brightness image as the base image

            '// stamp font and text values are initiated from another form
            Using g As Graphics = Graphics.FromImage(b),
                f As New Font(DefaultFont.FontFamily, CSng(_stmpTools.StampTextSize), If(_stmpTools.StampBold, FontStyle.Bold, FontStyle.Regular))

                Const borderWidth As Integer = 10
                Const borderPadding As Integer = 5

                '// measure the string
                Dim szx As SizeF = g.MeasureString(_stmpTools.StampText, f, Integer.MaxValue, StringFormat.GenericDefault)
                Dim strLength As Single = szx.Width
                Dim strHeight As Single = szx.Height

                Dim x As Single = e.X - borderWidth - borderPadding,
                    y As Single = e.Y

                Dim w As Double, h As Double

                If Math.Abs(_angle) > Double.Epsilon Then
                    h = CDbl(strLength) * Math.Sin(CDbl(Math.Abs(_angle)) * Math.PI / 180.0F)
                    w = Math.Sqrt(CDbl(strLength) * CDbl(strLength) - h * h)
                Else
                    '// its zero. so use calculated values
                    h = strHeight
                    w = strLength
                End If

                '// add space for the 10px border plus 5px of padding
                Dim r As New Rectangle(0, 0, w, h)
                r.Inflate(borderWidth + borderPadding, borderWidth + borderPadding)

                h = r.Height
                w = r.Width

                '// keep box from moving off the left
                If x < .Location.X Then
                    x = .Location.X
                End If

                '// keep box from moving off the right
                If x > .Location.X + .Width - w Then
                    x = .Location.X + .Width - w
                End If

                '// I don't know, but these values work for most smaller fonts, but
                '// it has got to be a fluke
                If _angle > 0 Then
                    y = y - h + borderWidth + borderWidth
                Else
                    y = y - borderWidth
                End If

                '// can't go off the top
                If y < .Location.Y Then
                    y = .Location.Y
                End If

                '// can't go off the bottom
                If y > .Location.Y + .Height - h Then
                    y = .Location.Y + .Height - h
                End If

                Dim rect As New Rectangle(x, y, w, h)
                g.DrawImage(_curImg, rect, rect, GraphicsUnit.Pixel)

                Using br As New SolidBrush(_stmpTools.StampTextColor)
                    RotateString(_stmpTools.StampText, _angle, e.X, e.Y, f, g, br)
                End Using

                '//draw bounding rectangle
                Using p As New Pen(Color.Black, borderWidth)
                    g.DrawRectangle(p, rect)
                End Using

            End Using
            '// set the picture box to show the new image
            .Image = b
        End With

    End Sub

    Private Sub RotateString(ByVal Text As String, ByVal angle As Integer, _
                      ByVal x As Integer, ByVal y As Integer, myfont As Font, mydrawing As Graphics, myColor As Brush)
        Dim myMatrix As New Matrix
        myMatrix.RotateAt(angle * -1, New Point(x, y)) 'Rotate drawing 
        mydrawing.Transform = myMatrix
        mydrawing.DrawString(Text, myFont, myColor, x, y) 'Draw the text string 
        myMatrix.RotateAt(angle, New Point(x, y)) 'Rotate back 
        mydrawing.Transform = myMatrix
    End Sub

I'm not the greatest when it comes to drawing. So any help would be great

Using the solution below from @LarsTech. I replaced the g.FillRectangle with:

g.DrawImage(_curImg, r, r, GraphicsUnit.Pixel)

_curImg is a copy of the original image with the brightness tuned up. When I change the code from below I end up with:

Note the double lines. They rotate with the stamp, even though they are acting as a background image and should be unrotated

Per suggestion, I changed the DrawStamp from @LarsTech to the following:

        Private Sub DrawStamp(g As Graphics, text As String,
                  f As Font, center As Point, angle As Integer, backImg As Image)

        Dim s As Size = g.MeasureString(text, f).ToSize
        Dim r As New Rectangle(center.X - (s.Width / 2) - 16,
                                 center.Y - (s.Height / 2) - 16,
                                 s.Width + 32,
                                 s.Height + 32)

        g.DrawImage(backImg, r, r, GraphicsUnit.Pixel)

        Using m As New Matrix
            m.RotateAt(angle, center)
            g.Transform = m

            Using p As New Pen(Color.Black, 6)
                g.DrawRectangle(p, r)
            End Using
            Using sf As New StringFormat
                sf.LineAlignment = StringAlignment.Center
                sf.Alignment = StringAlignment.Center
                g.DrawString(text, f, Brushes.Black, r, sf)
            End Using
            g.ResetTransform()
        End Using
    End Sub

However, I am now left with

Notice it drew the background, then did the rotation and drew the stamp. It ALMOST works. In this example the straight lines show the intended behavior... however i'm looking to fill the entire stamp with the background. That extra white on the sides would have been what was rotated into the stamp's background. I'm confused because the 'grey' portions I would then suspect to be clipping out parts of the image, but they aren't (if i move it over other areas that I unfortunately can't post on here) notice is out of skew except for the fact that the sides of the rectangle paint as such.

Another Edit with hopefully some more info

Here is hopefully a better explaination of what I am trying to do. I use a third party PDF viewer and I need to allow the user to add an image to the PDF. The viewer doesn't allow me to raise click events on it, so in order to grab user mouse clicks, I do the following:

  1. Take a screen grab of form
  2. Hide the PDF Viewer
  3. Add a PictureBox control to my form, replacing the area where the PDF viewer was
  4. With my screen grab, I make a copy of the image with the brightness reduced
  5. Display the gray scale copy of the image and draw directly on the image using mouse over events on the picturebox
  6. I draw a stamp on the picturebox, but want the background of it to be the original (non adjusted brightness image). However, since the area might be transformed using a rotation, I can't grab the background image. If no angle is provided, the source rectangle matches. However if its rotated, I cannot grab the same rotated rectangle off the source image.

Button Click Event:

  Dim bds As Rectangle = AxDPVActiveX1.Bounds
  Dim pt As Point = AxDPVActiveX1.PointToScreen(bds.Location)

    Using bit As Bitmap = New Bitmap(bds.Width, bds.Height)
        Using g As Graphics = Graphics.FromImage(bit)
            g.CopyFromScreen(New Point(pt.X - AxDPVActiveX1.Location.X, pt.Y - AxDPVActiveX1.Location.Y), Point.Empty, bds.Size)
        End Using

        _angle = 0

        _curImg = bit.Clone
        _curGray = Utils.CopyImageAndAdjustBrightness(bit, -100)

 End Using

   Dim p As New PictureBox

        Utils.SetControlDoubleBuffered(p)

        p.Dock = DockStyle.Fill
        p.BackColor = Color.Transparent
        AxDPVActiveX1.Visible = False
        p.Image = _curImg.Clone

        AddHandler p.MouseClick, AddressOf PanelDownMouse
        AddHandler p.MouseMove, AddressOf PanelMouseMove
        AddHandler p.MouseWheel, Sub(s As Object, ee As MouseEventArgs)
                                     _angle = Math.Max(Math.Min(_angle + (ee.Delta / 30), 45), -45)
                                     PanelMouseMove(s, ee)
                                 End Sub
        AddHandler p.MouseEnter, Sub(s As Object, ee As EventArgs)
                                     CType(s, Control).Focus()
                                 End Sub


        AxDPVActiveX1.Parent.Controls.Add(p)

After that code I end up with two images. _curgray is an image with adjusted brightness, and _curImg is my original screen grab.

_curGray: _curImg:

A mouseMove move event is applied to my new picture box. This is where all the code from earlier in the question comes into play.

Using the code above, my mouseMove event keeps creating a new imageto display in my picture box. If there is no rotation involved, I get pretty much what I'm looking for. Notice in the below image how the background of the stamp is brighter than everything. The portion over the blue square is slightly lighter. I am using this a way to draw the viewers eye to this area... its important for what I'm doing.

However, when applying a rotation to it, I cannot seem to copy from the original image. Look at the following image, the backgroundisn't rotating with it. I need to grab a rotated rectangle from the ORIGINAL image.

http://msdn.microsoft.com/en-us/library/ms142040(v=vs.110).aspx Graphics.DrawImage() accepts

Public Sub DrawImage ( _
    image As Image, _
    destRect As Rectangle, _
    srcRect As Rectangle, _
    srcUnit As GraphicsUnit _
)

where I can specify copy this source rectangle from my source image (in this case _curImg) and place onto my new drawing. It does not allow me to apply a transformation to the source rectangle. Basically I want to copy from my source image an area equivalent to the rotated rectangle (based on the transformation from @larstech )

I don't know how to express this concept any clearer. If it still doesn't make sense I will just accept LarsTech answer as the best answer and scrap my idea.

回答1:

I would try drawing the rectangle and the text together:

Private Sub DrawStamp(g As Graphics, text As String,
                      f As Font, center As Point, angle As Integer)
  Using m As New Matrix
    g.SmoothingMode = SmoothingMode.AntiAlias
    g.TextRenderingHint = TextRenderingHint.AntiAlias
    m.RotateAt(angle, center)
    g.Transform = m
    Dim s As Size = g.MeasureString(text, f).ToSize
    Dim r As New Rectangle(center.X - (s.Width / 2) - 16,
                           center.Y - (s.Height / 2) - 16,
                           s.Width + 32,
                           s.Height + 32)
    g.FillRectangle(Brushes.White, r)
    Using p As New Pen(Color.Black, 6)
      g.DrawRectangle(p, r)
    End Using
    Using sf As New StringFormat
      sf.LineAlignment = StringAlignment.Center
      sf.Alignment = StringAlignment.Center
      g.DrawString(text, f, Brushes.Black, r, sf)
    End Using
    g.ResetTransform()
  End Using
End Sub

The paint example:

Protected Overrides Sub OnPaint(e As PaintEventArgs)
  MyBase.OnPaint(e)
  e.Graphics.Clear(Color.LightGray)
  Using f As New Font("Calibri", 16, FontStyle.Bold)
    DrawStamp(e.Graphics,
              "Reviewed By Doctor Papa",
              f,
              New Point(Me.ClientSize.Width / 2, Me.ClientSize.Height / 2),
              -25)
  End Using
End Sub

Result:

Here I updated the code to "clip" the rotated rectangle so that I can copy that same area from the original image before applying the text and border:

Private Sub DrawStamp(g As Graphics, text As String,
                      f As Font, center As Point, angle As Integer)

  Dim s As Size = g.MeasureString(text, f).ToSize
  Dim r As New Rectangle(center.X - (s.Width / 2) - 16,
                         center.Y - (s.Height / 2) - 16,
                         s.Width + 32,
                         s.Height + 32)

  Using bmp As New Bitmap(_curImg.Width, _curImg.Height)
    Using gx As Graphics = Graphics.FromImage(bmp)
      Using m As New Matrix
        m.RotateAt(angle, center)
        gx.Transform = m
        gx.SetClip(r)
        gx.ResetTransform()
      End Using
      gx.DrawImage(_curImg, Point.Empty)
    End Using
    g.DrawImage(bmp, Point.Empty)
  End Using

  Using m As New Matrix
    g.SmoothingMode = SmoothingMode.AntiAlias
    g.TextRenderingHint = TextRenderingHint.AntiAlias
    m.RotateAt(angle, center)
    g.Transform = m      
    Using p As New Pen(Color.Black, 6)
      g.DrawRectangle(p, r)
    End Using
    Using sf As New StringFormat
      sf.LineAlignment = StringAlignment.Center
      sf.Alignment = StringAlignment.Center
      g.DrawString(text, f, Brushes.Black, r, sf)
    End Using
    g.ResetTransform()
  End Using
End Sub

New Result:



回答2:

It's just trigonometry:

You know c because you know how wide the original text is, and you know h because you know the height of your text. You also need to know alpha, it's the angle that you rotated your text.

Now you need to work the power of math: First take the small rectangles at the end. In the bottom left you can see, that the angle right of the x is actually 180°-90°-alpha, or 90°-alpha. So alpha is also found on the opposite site. So you can find x:

x = h * sin(alpha)

The same goes for y, but it's either sin(90°-alpha), or cos(alpha)

y = h * cos(alpha)

Next you need to find a and b to complete the rectangle. The large triangle gives you

a = w * cos(alpha)

and

b = w * sin(alpha)

Then just add the parts together:

NewWidth = a + x
NewHeight = b + y

That way you get the size of the bounding box. As for the coordinates, it depends on which point is actually defined when you print the rotated text.