Add experimental FPDFPageObj_GetRotatedBounds() API.

This new API allows callers to get a closer bound for rotated objects,
compared to FPDFPageObj_GetBounds().

Also fix some existing IWYU issues along the way.

Bug: pdfium:1840
Change-Id: Iba5b12b8ca0094682c895e9db70840aaea84f0f0
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/94470
Reviewed-by: Nigi <nigi@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
Reviewed-by: Tom Sepez <tsepez@chromium.org>
diff --git a/core/fpdfapi/page/cpdf_imageobject.cpp b/core/fpdfapi/page/cpdf_imageobject.cpp
index 278fb84..4d99a89 100644
--- a/core/fpdfapi/page/cpdf_imageobject.cpp
+++ b/core/fpdfapi/page/cpdf_imageobject.cpp
@@ -9,6 +9,7 @@
 #include "core/fpdfapi/page/cpdf_docpagedata.h"
 #include "core/fpdfapi/page/cpdf_image.h"
 #include "core/fpdfapi/parser/cpdf_stream.h"
+#include "core/fxcrt/fx_coordinates.h"
 #include "core/fxge/dib/cfx_dibbase.h"
 #include "core/fxge/dib/cfx_dibitmap.h"
 
@@ -45,6 +46,7 @@
 
 void CPDF_ImageObject::CalcBoundingBox() {
   static constexpr CFX_FloatRect kRect(0.0f, 0.0f, 1.0f, 1.0f);
+  SetOriginalRect(kRect);
   SetRect(m_Matrix.TransformRect(kRect));
 }
 
diff --git a/core/fpdfapi/page/cpdf_pageobject.cpp b/core/fpdfapi/page/cpdf_pageobject.cpp
index bde1704..4ce2504 100644
--- a/core/fpdfapi/page/cpdf_pageobject.cpp
+++ b/core/fpdfapi/page/cpdf_pageobject.cpp
@@ -6,6 +6,8 @@
 
 #include "core/fpdfapi/page/cpdf_pageobject.h"
 
+#include "core/fxcrt/fx_coordinates.h"
+
 CPDF_PageObject::CPDF_PageObject(int32_t content_stream)
     : m_ContentStream(content_stream) {}
 
diff --git a/core/fpdfapi/page/cpdf_pageobject.h b/core/fpdfapi/page/cpdf_pageobject.h
index 60626af..b88be0c 100644
--- a/core/fpdfapi/page/cpdf_pageobject.h
+++ b/core/fpdfapi/page/cpdf_pageobject.h
@@ -60,6 +60,8 @@
   void TransformClipPath(const CFX_Matrix& matrix);
   void TransformGeneralState(const CFX_Matrix& matrix);
 
+  void SetOriginalRect(const CFX_FloatRect& rect) { m_OriginalRect = rect; }
+  const CFX_FloatRect& GetOriginalRect() const { return m_OriginalRect; }
   void SetRect(const CFX_FloatRect& rect) { m_Rect = rect; }
   const CFX_FloatRect& GetRect() const { return m_Rect; }
   FX_RECT GetBBox() const;
@@ -89,6 +91,7 @@
   CFX_FloatRect m_Rect;
 
  private:
+  CFX_FloatRect m_OriginalRect;
   CPDF_ContentMarks m_ContentMarks;
   bool m_bDirty = false;
   int32_t m_ContentStream;
diff --git a/core/fpdfapi/page/cpdf_textobject.cpp b/core/fpdfapi/page/cpdf_textobject.cpp
index 33e1b0b..79e9300 100644
--- a/core/fpdfapi/page/cpdf_textobject.cpp
+++ b/core/fpdfapi/page/cpdf_textobject.cpp
@@ -10,6 +10,7 @@
 
 #include "core/fpdfapi/font/cpdf_cidfont.h"
 #include "core/fpdfapi/font/cpdf_font.h"
+#include "core/fxcrt/fx_coordinates.h"
 #include "third_party/base/check.h"
 #include "third_party/base/span.h"
 
@@ -325,9 +326,11 @@
     max_y = max_y * fontsize / 1000;
   }
 
-  CFX_FloatRect rect =
-      GetTextMatrix().TransformRect(CFX_FloatRect(min_x, min_y, max_x, max_y));
+  SetOriginalRect(CFX_FloatRect(min_x, min_y, max_x, max_y));
+  CFX_FloatRect rect = GetTextMatrix().TransformRect(GetOriginalRect());
   if (TextRenderingModeIsStrokeMode(m_TextState.GetTextMode())) {
+    // TODO(crbug.com/pdfium/1840): Does the original rect need a similar
+    // adjustment?
     const float half_width = m_GraphState.GetLineWidth() / 2;
     rect.Inflate(half_width, half_width);
   }
diff --git a/core/fpdfapi/page/cpdf_textobject.h b/core/fpdfapi/page/cpdf_textobject.h
index d52aad7..5e7adfc 100644
--- a/core/fpdfapi/page/cpdf_textobject.h
+++ b/core/fpdfapi/page/cpdf_textobject.h
@@ -14,6 +14,7 @@
 #include <vector>
 
 #include "core/fpdfapi/page/cpdf_pageobject.h"
+#include "core/fxcrt/fx_coordinates.h"
 #include "core/fxcrt/fx_string.h"
 #include "core/fxcrt/retain_ptr.h"
 
diff --git a/fpdfsdk/fpdf_editpage.cpp b/fpdfsdk/fpdf_editpage.cpp
index 8a2d269..985823a 100644
--- a/fpdfsdk/fpdf_editpage.cpp
+++ b/fpdfsdk/fpdf_editpage.cpp
@@ -786,6 +786,44 @@
 }
 
 FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFPageObj_GetRotatedBounds(FPDF_PAGEOBJECT page_object,
+                             FS_QUADPOINTSF* quad_points) {
+  CPDF_PageObject* cpage_object = CPDFPageObjectFromFPDFPageObject(page_object);
+  if (!cpage_object || !quad_points)
+    return false;
+
+  CFX_Matrix matrix;
+  switch (cpage_object->GetType()) {
+    case CPDF_PageObject::Type::kText:
+      matrix = cpage_object->AsText()->GetTextMatrix();
+      break;
+    case CPDF_PageObject::Type::kImage:
+      matrix = cpage_object->AsImage()->matrix();
+      break;
+    default:
+      // TODO(crbug.com/pdfium/1840): Support more object types.
+      return false;
+  }
+
+  const CFX_FloatRect& bbox = cpage_object->GetOriginalRect();
+  const CFX_PointF bottom_left = matrix.Transform({bbox.left, bbox.bottom});
+  const CFX_PointF bottom_right = matrix.Transform({bbox.right, bbox.bottom});
+  const CFX_PointF top_right = matrix.Transform({bbox.right, bbox.top});
+  const CFX_PointF top_left = matrix.Transform({bbox.left, bbox.top});
+
+  // See PDF 32000-1:2008, figure 64 for the QuadPoints ordering.
+  quad_points->x1 = bottom_left.x;
+  quad_points->y1 = bottom_left.y;
+  quad_points->x2 = bottom_right.x;
+  quad_points->y2 = bottom_right.y;
+  quad_points->x3 = top_right.x;
+  quad_points->y3 = top_right.y;
+  quad_points->x4 = top_left.x;
+  quad_points->y4 = top_left.y;
+  return true;
+}
+
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
 FPDFPageObj_SetStrokeColor(FPDF_PAGEOBJECT page_object,
                            unsigned int R,
                            unsigned int G,
diff --git a/fpdfsdk/fpdf_editpage_embeddertest.cpp b/fpdfsdk/fpdf_editpage_embeddertest.cpp
index 2af4e54..fe2719b 100644
--- a/fpdfsdk/fpdf_editpage_embeddertest.cpp
+++ b/fpdfsdk/fpdf_editpage_embeddertest.cpp
@@ -292,3 +292,167 @@
 
   UnloadPage(page);
 }
+
+TEST_F(FPDFEditPageEmbedderTest, GetRotatedBoundsBadParameters) {
+  ASSERT_TRUE(OpenDocument("hello_world.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, 0);
+  ASSERT_EQ(FPDF_PAGEOBJ_TEXT, FPDFPageObj_GetType(obj));
+
+  FS_QUADPOINTSF quad;
+  ASSERT_FALSE(FPDFPageObj_GetRotatedBounds(nullptr, nullptr));
+  ASSERT_FALSE(FPDFPageObj_GetRotatedBounds(obj, nullptr));
+  ASSERT_FALSE(FPDFPageObj_GetRotatedBounds(nullptr, &quad));
+
+  UnloadPage(page);
+}
+
+TEST_F(FPDFEditPageEmbedderTest, GetBoundsForNormalText) {
+  ASSERT_TRUE(OpenDocument("hello_world.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, 0);
+  ASSERT_EQ(FPDF_PAGEOBJ_TEXT, FPDFPageObj_GetType(obj));
+
+  constexpr float kExpectedLeft = 20.348f;
+  constexpr float kExpectedBottom = 48.164f;
+  constexpr float kExpectedRight = 83.36f;
+  constexpr float kExpectedTop = 58.328f;
+
+  float left;
+  float bottom;
+  float right;
+  float top;
+  ASSERT_TRUE(FPDFPageObj_GetBounds(obj, &left, &bottom, &right, &top));
+  EXPECT_FLOAT_EQ(kExpectedLeft, left);
+  EXPECT_FLOAT_EQ(kExpectedBottom, bottom);
+  EXPECT_FLOAT_EQ(kExpectedRight, right);
+  EXPECT_FLOAT_EQ(kExpectedTop, top);
+
+  FS_QUADPOINTSF quad;
+  ASSERT_TRUE(FPDFPageObj_GetRotatedBounds(obj, &quad));
+  EXPECT_FLOAT_EQ(kExpectedLeft, quad.x1);
+  EXPECT_FLOAT_EQ(kExpectedBottom, quad.y1);
+  EXPECT_FLOAT_EQ(kExpectedRight, quad.x2);
+  EXPECT_FLOAT_EQ(kExpectedBottom, quad.y2);
+  EXPECT_FLOAT_EQ(kExpectedRight, quad.x3);
+  EXPECT_FLOAT_EQ(kExpectedTop, quad.y3);
+  EXPECT_FLOAT_EQ(kExpectedLeft, quad.x4);
+  EXPECT_FLOAT_EQ(kExpectedTop, quad.y4);
+
+  UnloadPage(page);
+}
+
+TEST_F(FPDFEditPageEmbedderTest, GetBoundsForRotatedText) {
+  ASSERT_TRUE(OpenDocument("rotated_text.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, 0);
+  ASSERT_EQ(FPDF_PAGEOBJ_TEXT, FPDFPageObj_GetType(obj));
+
+  constexpr float kExpectedLeft = 98.9478f;
+  constexpr float kExpectedBottom = 78.2607f;
+  constexpr float kExpectedRight = 126.32983f;
+  constexpr float kExpectedTop = 105.64272f;
+
+  float left;
+  float bottom;
+  float right;
+  float top;
+  ASSERT_TRUE(FPDFPageObj_GetBounds(obj, &left, &bottom, &right, &top));
+  EXPECT_FLOAT_EQ(kExpectedLeft, left);
+  EXPECT_FLOAT_EQ(kExpectedBottom, bottom);
+  EXPECT_FLOAT_EQ(kExpectedRight, right);
+  EXPECT_FLOAT_EQ(kExpectedTop, top);
+
+  FS_QUADPOINTSF quad;
+  ASSERT_TRUE(FPDFPageObj_GetRotatedBounds(obj, &quad));
+  EXPECT_FLOAT_EQ(kExpectedLeft, quad.x1);
+  EXPECT_FLOAT_EQ(98.4557f, quad.y1);
+  EXPECT_FLOAT_EQ(119.14279f, quad.x2);
+  EXPECT_FLOAT_EQ(kExpectedBottom, quad.y2);
+  EXPECT_FLOAT_EQ(kExpectedRight, quad.x3);
+  EXPECT_FLOAT_EQ(85.447739f, quad.y3);
+  EXPECT_FLOAT_EQ(106.13486f, quad.x4);
+  EXPECT_FLOAT_EQ(kExpectedTop, quad.y4);
+
+  UnloadPage(page);
+}
+
+TEST_F(FPDFEditPageEmbedderTest, GetBoundsForNormalImage) {
+  ASSERT_TRUE(OpenDocument("matte.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, 2);
+  ASSERT_EQ(FPDF_PAGEOBJ_IMAGE, FPDFPageObj_GetType(obj));
+
+  constexpr float kExpectedLeft = 0.0f;
+  constexpr float kExpectedBottom = 90.0f;
+  constexpr float kExpectedRight = 40.0f;
+  constexpr float kExpectedTop = 150.0f;
+
+  float left;
+  float bottom;
+  float right;
+  float top;
+  ASSERT_TRUE(FPDFPageObj_GetBounds(obj, &left, &bottom, &right, &top));
+  EXPECT_FLOAT_EQ(kExpectedLeft, left);
+  EXPECT_FLOAT_EQ(kExpectedBottom, bottom);
+  EXPECT_FLOAT_EQ(kExpectedRight, right);
+  EXPECT_FLOAT_EQ(kExpectedTop, top);
+
+  FS_QUADPOINTSF quad;
+  ASSERT_TRUE(FPDFPageObj_GetRotatedBounds(obj, &quad));
+  EXPECT_FLOAT_EQ(kExpectedLeft, quad.x1);
+  EXPECT_FLOAT_EQ(kExpectedBottom, quad.y1);
+  EXPECT_FLOAT_EQ(kExpectedRight, quad.x2);
+  EXPECT_FLOAT_EQ(kExpectedBottom, quad.y2);
+  EXPECT_FLOAT_EQ(kExpectedRight, quad.x3);
+  EXPECT_FLOAT_EQ(kExpectedTop, quad.y3);
+  EXPECT_FLOAT_EQ(kExpectedLeft, quad.x4);
+  EXPECT_FLOAT_EQ(kExpectedTop, quad.y4);
+
+  UnloadPage(page);
+}
+
+TEST_F(FPDFEditPageEmbedderTest, GetBoundsForRotatedImage) {
+  ASSERT_TRUE(OpenDocument("rotated_image.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, 0);
+  ASSERT_EQ(FPDF_PAGEOBJ_IMAGE, FPDFPageObj_GetType(obj));
+
+  constexpr float kExpectedLeft = 100.0f;
+  constexpr float kExpectedBottom = 70.0f;
+  constexpr float kExpectedRight = 170.0f;
+  constexpr float kExpectedTop = 140.0f;
+
+  float left;
+  float bottom;
+  float right;
+  float top;
+  ASSERT_TRUE(FPDFPageObj_GetBounds(obj, &left, &bottom, &right, &top));
+  EXPECT_FLOAT_EQ(kExpectedLeft, left);
+  EXPECT_FLOAT_EQ(kExpectedBottom, bottom);
+  EXPECT_FLOAT_EQ(kExpectedRight, right);
+  EXPECT_FLOAT_EQ(kExpectedTop, top);
+
+  FS_QUADPOINTSF quad;
+  ASSERT_TRUE(FPDFPageObj_GetRotatedBounds(obj, &quad));
+  EXPECT_FLOAT_EQ(kExpectedLeft, quad.x1);
+  EXPECT_FLOAT_EQ(100.0f, quad.y1);
+  EXPECT_FLOAT_EQ(130.0f, quad.x2);
+  EXPECT_FLOAT_EQ(kExpectedBottom, quad.y2);
+  EXPECT_FLOAT_EQ(kExpectedRight, quad.x3);
+  EXPECT_FLOAT_EQ(110.0f, quad.y3);
+  EXPECT_FLOAT_EQ(140.0f, quad.x4);
+  EXPECT_FLOAT_EQ(kExpectedTop, quad.y4);
+
+  UnloadPage(page);
+}
diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c
index e524f30..6e47876 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -208,6 +208,7 @@
     CHK(FPDFPageObj_GetLineJoin);
     CHK(FPDFPageObj_GetMark);
     CHK(FPDFPageObj_GetMatrix);
+    CHK(FPDFPageObj_GetRotatedBounds);
     CHK(FPDFPageObj_GetStrokeColor);
     CHK(FPDFPageObj_GetStrokeWidth);
     CHK(FPDFPageObj_GetType);
diff --git a/public/fpdf_edit.h b/public/fpdf_edit.h
index 8f4e7f0..c902998 100644
--- a/public/fpdf_edit.h
+++ b/public/fpdf_edit.h
@@ -761,7 +761,7 @@
 // right        - pointer where the right coordinate will be stored
 // top          - pointer where the top coordinate will be stored
 //
-// Returns TRUE on success.
+// On success, returns TRUE and fills in the 4 coordinates.
 FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
 FPDFPageObj_GetBounds(FPDF_PAGEOBJECT page_object,
                       float* left,
@@ -769,6 +769,25 @@
                       float* right,
                       float* top);
 
+// Experimental API.
+// Get the quad points that bounds |page_object|.
+//
+// page_object  - handle to a page object.
+// quad_points  - pointer where the quadrilateral points will be stored.
+//
+// On success, returns TRUE and fills in |quad_points|.
+//
+// Similar to FPDFPageObj_GetBounds(), this returns the bounds of a page
+// object. When the object is rotated by a non-multiple of 90 degrees, this API
+// returns a tighter bound that cannot be represented with just the 4 sides of
+// a rectangle.
+//
+// Currently only works the following |page_object| types: FPDF_PAGEOBJ_TEXT and
+// FPDF_PAGEOBJ_IMAGE.
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFPageObj_GetRotatedBounds(FPDF_PAGEOBJECT page_object,
+                             FS_QUADPOINTSF* quad_points);
+
 // Set the blend mode of |page_object|.
 //
 // page_object  - handle to a page object.
diff --git a/testing/resources/rotated_image.in b/testing/resources/rotated_image.in
new file mode 100644
index 0000000..cf82fe8
--- /dev/null
+++ b/testing/resources/rotated_image.in
@@ -0,0 +1,52 @@
+{{header}}
+{{object 1 0}} <<
+  /Type /Catalog
+  /Pages 2 0 R
+>>
+endobj
+{{object 2 0}} <<
+  /Type /Pages
+  /MediaBox [0 0 200 200]
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+{{object 3 0}} <<
+  /Type /Page
+  /Parent 2 0 R
+  /Contents 4 0 R
+  /Resources <<
+    /XObject <<
+      /Img 5 0 R
+    >>
+  >>
+>>
+endobj
+{{object 4 0}} <<
+  {{streamlen}}
+>>
+stream
+q
+30 -30 40 40 100 100 cm
+/Img Do
+Q
+endstream
+endobj
+{{object 5 0}} <<
+  /Type /XObject
+  /Subtype /Image
+  /Width 50
+  /Height 50
+  /BitsPerComponent 8
+  /ColorSpace /DeviceRGB
+  /Filter [/ASCIIHexDecode /FlateDecode]
+  {{streamlen}}
+>>
+stream
+789cedc2310d00000c03a07f2aaab3ea7bcf03842655555555555555f5bf01cc7818dc
+endstream
+endobj
+{{xref}}
+{{trailer}}
+{{startxref}}
+%%EOF
diff --git a/testing/resources/rotated_image.pdf b/testing/resources/rotated_image.pdf
new file mode 100644
index 0000000..5bb1836
--- /dev/null
+++ b/testing/resources/rotated_image.pdf
@@ -0,0 +1,64 @@
+%PDF-1.7
+% ò¤ô
+1 0 obj <<
+  /Type /Catalog
+  /Pages 2 0 R
+>>
+endobj
+2 0 obj <<
+  /Type /Pages
+  /MediaBox [0 0 200 200]
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+3 0 obj <<
+  /Type /Page
+  /Parent 2 0 R
+  /Contents 4 0 R
+  /Resources <<
+    /XObject <<
+      /Img 5 0 R
+    >>
+  >>
+>>
+endobj
+4 0 obj <<
+  /Length 36
+>>
+stream
+q
+30 -30 40 40 100 100 cm
+/Img Do
+Q
+endstream
+endobj
+5 0 obj <<
+  /Type /XObject
+  /Subtype /Image
+  /Width 50
+  /Height 50
+  /BitsPerComponent 8
+  /ColorSpace /DeviceRGB
+  /Filter [/ASCIIHexDecode /FlateDecode]
+  /Length 71
+>>
+stream
+789cedc2310d00000c03a07f2aaab3ea7bcf03842655555555555555f5bf01cc7818dc
+endstream
+endobj
+xref
+0 6
+0000000000 65535 f 
+0000000015 00000 n 
+0000000068 00000 n 
+0000000157 00000 n 
+0000000287 00000 n 
+0000000374 00000 n 
+trailer <<
+  /Root 1 0 R
+  /Size 6
+>>
+startxref
+644
+%%EOF