Serialize floats in page content to avoid exponential representation

CPDF_PageContentGenerator class uses std::ostringstream to serialize
floats to string while generating the content stream. Float values
close to 0 (e.g., 0.00000001) and large numbers (e.g., 10000000) get
converted to their exponential representation (e.g., 1e-7 and 1e+7
respectively). PDF specification does not recognize exponential format
representation for floats.

CPDF_PageContentGenerator is invoked when FPDFPage_GenerateContent API
is called. Existing code addressed a specific case in path generation
code by using SkFloatToDecimal to serialize floats. The fix in this
CL is to re-factor that code into a utility function and use it in
other places except for the color operators. Code that serializes the
color operators (rg and RG) and their operands, converts an integer in
CPDF_Color::GetRGB into float dividing by 256. Thus the floating point
serialization anomaly wasn't observed for these operators.

A new unit-test CPDF_PageContentGeneratorTest::BUG_937 was added to
test the product code changes. Existing unit-tests and embedder-tests
needed to be updated to reflect the serialization format.

R=thestig@chromium.org

Bug: pdfium:937
Change-Id: I38f066a0ad074cd0d68d6b7620fb581d04ce2c16
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/52070
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
diff --git a/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp b/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp
index c5f9568..d2c8bf2 100644
--- a/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp
+++ b/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp
@@ -39,9 +39,20 @@
 
 namespace {
 
+std::ostream& WriteFloat(std::ostream& stream, float value) {
+  char buffer[pdfium::skia::kMaximumSkFloatToDecimalLength];
+  unsigned size = pdfium::skia::SkFloatToDecimal(value, buffer);
+  stream.write(buffer, size);
+  return stream;
+}
+
 std::ostream& operator<<(std::ostream& ar, const CFX_Matrix& matrix) {
-  ar << matrix.a << " " << matrix.b << " " << matrix.c << " " << matrix.d << " "
-     << matrix.e << " " << matrix.f;
+  WriteFloat(ar, matrix.a) << " ";
+  WriteFloat(ar, matrix.b) << " ";
+  WriteFloat(ar, matrix.c) << " ";
+  WriteFloat(ar, matrix.d) << " ";
+  WriteFloat(ar, matrix.e) << " ";
+  WriteFloat(ar, matrix.f);
   return ar;
 }
 
@@ -358,19 +369,17 @@
   const auto& pPoints = pPathObj->path().GetPoints();
   if (pPathObj->path().IsRect()) {
     CFX_PointF diff = pPoints[2].m_Point - pPoints[0].m_Point;
-    *buf << pPoints[0].m_Point.x << " " << pPoints[0].m_Point.y << " " << diff.x
-         << " " << diff.y << " re";
+    WriteFloat(*buf, pPoints[0].m_Point.x) << " ";
+    WriteFloat(*buf, pPoints[0].m_Point.y) << " ";
+    WriteFloat(*buf, diff.x) << " ";
+    WriteFloat(*buf, diff.y) << " re";
   } else {
     for (size_t i = 0; i < pPoints.size(); i++) {
       if (i > 0)
         *buf << " ";
 
-      char buffer[pdfium::skia::kMaximumSkFloatToDecimalLength];
-      unsigned size =
-          pdfium::skia::SkFloatToDecimal(pPoints[i].m_Point.x, buffer);
-      buf->write(buffer, size) << " ";
-      size = pdfium::skia::SkFloatToDecimal(pPoints[i].m_Point.y, buffer);
-      buf->write(buffer, size);
+      WriteFloat(*buf, pPoints[i].m_Point.x) << " ";
+      WriteFloat(*buf, pPoints[i].m_Point.y);
 
       FXPT_TYPE pointType = pPoints[i].m_Type;
       if (pointType == FXPT_TYPE::MoveTo) {
@@ -386,9 +395,11 @@
           *buf << " h";
           break;
         }
-        *buf << " " << pPoints[i + 1].m_Point.x << " "
-             << pPoints[i + 1].m_Point.y << " " << pPoints[i + 2].m_Point.x
-             << " " << pPoints[i + 2].m_Point.y << " c";
+        *buf << " ";
+        WriteFloat(*buf, pPoints[i + 1].m_Point.x) << " ";
+        WriteFloat(*buf, pPoints[i + 1].m_Point.y) << " ";
+        WriteFloat(*buf, pPoints[i + 2].m_Point.x) << " ";
+        WriteFloat(*buf, pPoints[i + 2].m_Point.y) << " c";
         i += 2;
       }
       if (pPoints[i].m_CloseFigure)
@@ -427,7 +438,7 @@
   }
   float lineWidth = pPageObj->m_GraphState.GetLineWidth();
   if (lineWidth != 1.0f)
-    *buf << lineWidth << " w ";
+    WriteFloat(*buf, lineWidth) << " w ";
   CFX_GraphStateData::LineCap lineCap = pPageObj->m_GraphState.GetLineCap();
   if (lineCap != CFX_GraphStateData::LineCapButt)
     *buf << static_cast<int>(lineCap) << " J ";
@@ -545,8 +556,8 @@
     dictName = RealizeResource(pIndirectFont, "Font");
     m_pObjHolder->m_FontsMap[data] = dictName;
   }
-  *buf << "/" << PDF_NameEncode(dictName) << " " << pTextObj->GetFontSize()
-       << " Tf ";
+  *buf << "/" << PDF_NameEncode(dictName) << " ";
+  WriteFloat(*buf, pTextObj->GetFontSize()) << " Tf ";
   ByteString text;
   for (uint32_t charcode : pTextObj->GetCharCodes()) {
     if (charcode != CPDF_Font::kInvalidCharCode)
diff --git a/core/fpdfapi/edit/cpdf_pagecontentgenerator_unittest.cpp b/core/fpdfapi/edit/cpdf_pagecontentgenerator_unittest.cpp
index 8e72e90..ff61c01 100644
--- a/core/fpdfapi/edit/cpdf_pagecontentgenerator_unittest.cpp
+++ b/core/fpdfapi/edit/cpdf_pagecontentgenerator_unittest.cpp
@@ -70,9 +70,76 @@
                                false);
   pPathObj->path().AppendPoint(CFX_PointF(0, 3.78f), FXPT_TYPE::LineTo, true);
   buf.str("");
-
   TestProcessPath(&generator, &buf, pPathObj.get());
-  EXPECT_EQ("q 1 0 0 1 0 0 cm 0 0 5.2 3.78 re n Q\n", ByteString(buf));
+  EXPECT_EQ("q 1 0 0 1 0 0 cm 0 0 5.1999998 3.78 re n Q\n", ByteString(buf));
+}
+
+TEST_F(CPDF_PageContentGeneratorTest, BUG_937) {
+  static const std::vector<float> rgb = {0.000000000000000000001f, 0.7f, 0.35f};
+  CPDF_ColorSpace* pCS = CPDF_ColorSpace::GetStockCS(PDFCS_DEVICERGB);
+
+  {
+    auto pPathObj = pdfium::MakeUnique<CPDF_PathObject>();
+    pPathObj->set_filltype(FXFILL_WINDING);
+
+    // Test code in ProcessPath that generates re operator
+    pPathObj->path().AppendRect(0.000000000000000000001,
+                                0.000000000000000000001, 100, 100);
+
+    pPathObj->m_ColorState.SetFillColor(pCS, rgb);
+    pPathObj->m_ColorState.SetStrokeColor(pCS, rgb);
+    pPathObj->m_GraphState.SetLineWidth(200000000000000000001.0);
+    pPathObj->Transform(CFX_Matrix(1, 0, 0, 1, 0.000000000000000000001,
+                                   200000000000000.000002));
+
+    CPDF_Dictionary dummy_page_dict;
+    auto pTestPage =
+        pdfium::MakeRetain<CPDF_Page>(nullptr, &dummy_page_dict, false);
+    CPDF_PageContentGenerator generator(pTestPage.Get());
+    std::ostringstream buf;
+    TestProcessPath(&generator, &buf, pPathObj.get());
+    EXPECT_EQ(
+        "q 0 0.701961 0.34902 rg 0 0.701961 0.34902 RG 200000000000000000000 w"
+        " 1 0 0 1 .00000000000000000000099999997 200000000000000 cm .000000000"
+        "00000000000099999997 .00000000000000000000099999997 100 100 re f Q\n",
+        ByteString(buf));
+  }
+
+  {
+    // Test code in ProcessPath that handles bezier operator
+    auto pPathObj = pdfium::MakeUnique<CPDF_PathObject>();
+    pPathObj->m_ColorState.SetFillColor(pCS, rgb);
+    pPathObj->m_ColorState.SetStrokeColor(pCS, rgb);
+    pPathObj->m_GraphState.SetLineWidth(2.000000000000000000001);
+    pPathObj->Transform(CFX_Matrix(1, 0, 0, 1, 432, 500000000000000.000002));
+
+    pPathObj->set_filltype(FXFILL_WINDING);
+    pPathObj->path().AppendPoint(CFX_PointF(0.000000000000000000001f, 4.67f),
+                                 FXPT_TYPE::MoveTo, false);
+    pPathObj->path().AppendPoint(
+        CFX_PointF(0.000000000000000000001, 100000000000000.000002),
+        FXPT_TYPE::LineTo, false);
+    pPathObj->path().AppendPoint(CFX_PointF(0.0000000000001f, 3.15f),
+                                 FXPT_TYPE::BezierTo, false);
+    pPathObj->path().AppendPoint(CFX_PointF(3.57f, 2.98f), FXPT_TYPE::BezierTo,
+                                 false);
+    pPathObj->path().AppendPoint(
+        CFX_PointF(53.4f, 5000000000000000000.00000000000000004),
+        FXPT_TYPE::BezierTo, true);
+    CPDF_Dictionary dummy_page_dict;
+    auto pTestPage =
+        pdfium::MakeRetain<CPDF_Page>(nullptr, &dummy_page_dict, false);
+    CPDF_PageContentGenerator generator(pTestPage.Get());
+    std::ostringstream buf;
+
+    TestProcessPath(&generator, &buf, pPathObj.get());
+    EXPECT_EQ(
+        "q 0 0.701961 0.34902 rg 0 0.701961 0.34902 RG 2 w 1 0 0 1 432 4999999"
+        "90000000 cm .00000000000000000000099999997 4.6700001 m .0000000000000"
+        "0000000099999997 100000000000000 l .000000000000099999998 3.1500001 3"
+        ".5699999 2.98 53.400002 5000000000000000000 c h f Q\n",
+        ByteString(buf));
+  }
 }
 
 TEST_F(CPDF_PageContentGeneratorTest, ProcessPath) {
@@ -106,8 +173,9 @@
   TestProcessPath(&generator, &buf, pPathObj.get());
   EXPECT_EQ(
       "q 1 0 0 1 0 0 cm 3.102 4.6700001 m 5.4499998 .28999999 l 4.2399998 "
-      "3.1500001 4.65 2.98 3.456 0.24 c 10.6000004 11.1499996 l 11 12.5 "
-      "l 11.46 12.6700001 11.84 12.96 12 13.64 c h f Q\n",
+      "3.1500001 4.6500001 2.98 3.4560001 .23999999 c 10.6000004 11.149999"
+      "6 l 11 12.5 l 11.46 12.6700001 11.8400002 12.96 12 13.6400003 c h f"
+      " Q\n",
       ByteString(buf));
 }
 
@@ -333,5 +401,8 @@
   CPDF_PageContentGenerator generator(pTestForm.get());
   std::ostringstream process_buf;
   generator.ProcessPageObjects(&process_buf);
-  EXPECT_STREQ(content, ByteString(process_buf).c_str());
+  EXPECT_STREQ(
+      "q 1 0 0 1 0 0 cm 3.102 4.6700001 m 5.4500012 .28999999 l 4.2399998 3.14"
+      "99999 4.6500001 2.98 3.4560001 .24000001 c 3.102 4.6700001 l h f Q\n",
+      ByteString(process_buf).c_str());
 }
diff --git a/fpdfsdk/fpdf_annot_embeddertest.cpp b/fpdfsdk/fpdf_annot_embeddertest.cpp
index 59dcd99..b865619 100644
--- a/fpdfsdk/fpdf_annot_embeddertest.cpp
+++ b/fpdfsdk/fpdf_annot_embeddertest.cpp
@@ -689,19 +689,19 @@
 TEST_F(FPDFAnnotEmbedderTest, AddAndModifyPath) {
 #if _FX_PLATFORM_ == _FX_PLATFORM_APPLE_
   const char md5_original[] = "c35408717759562d1f8bf33d317483d2";
-  const char md5_modified_path[] = "873b92ea83ccf006e58415d866ce145b";
-  const char md5_two_paths[] = "6f1f1c91f50240e9cc9d7c87c48b93a7";
-  const char md5_new_annot[] = "078bf58f939645ac305854f31ee9a828";
+  const char md5_modified_path[] = "9059723a045e17478753d2f0eb33bc03";
+  const char md5_two_paths[] = "7eed0cfba780f1d4dd8068f717d3a6bf";
+  const char md5_new_annot[] = "1de8212d43b7066a6df042095c2aca61";
 #elif _FX_PLATFORM_ == _FX_PLATFORM_WINDOWS_
   const char md5_original[] = "6f3cc2dd37479ce7cc072bfb0c63c275";
-  const char md5_modified_path[] = "c66293426cbf1f568502d1f7c0728fc7";
-  const char md5_two_paths[] = "122ac4625393b1dfe4be8707b73cbb13";
-  const char md5_new_annot[] = "253c7fd739646ba2568e1e8bfc782336";
+  const char md5_modified_path[] = "c0c2eb2aba73ad15b3240e342fbe0d72";
+  const char md5_two_paths[] = "2306bf04915fe001b5f4726843d184c8";
+  const char md5_new_annot[] = "64a319f145768cb09944d2109efe394e";
 #else
   const char md5_original[] = "964f89bbe8911e540a465cf1a64b7f7e";
-  const char md5_modified_path[] = "5a4a6091cff648a4ece3ce7e245e3e38";
-  const char md5_two_paths[] = "d6e4072a4415cfc6ec17201fb6be0ee0";
-  const char md5_new_annot[] = "fc338b97bf66a656916c6198697a8a28";
+  const char md5_modified_path[] = "9a38048fb3ac1b2c9a4b34139caa993c";
+  const char md5_two_paths[] = "ece3d4df54b3395d6a2bf7a29b17239c";
+  const char md5_new_annot[] = "4299493de84c249f42f220f30f2bbb67";
 #endif
 
   // Open a file with two annotations and load its first page.
diff --git a/fpdfsdk/fpdf_edit_embeddertest.cpp b/fpdfsdk/fpdf_edit_embeddertest.cpp
index 6e03644..948d356 100644
--- a/fpdfsdk/fpdf_edit_embeddertest.cpp
+++ b/fpdfsdk/fpdf_edit_embeddertest.cpp
@@ -689,10 +689,13 @@
 
 #if _FX_PLATFORM_ == _FX_PLATFORM_APPLE_
   const char kNonPrimesMD5[] = "57e76dc7375d896704f0fd6d6d1b9e65";
+  const char kNonPrimesAfterSaveMD5[] = "6304512d0150bbd5578e8e22d3121103";
 #elif _FX_PLATFORM_ == _FX_PLATFORM_WINDOWS_
   const char kNonPrimesMD5[] = "4d906b57fba36c70c600cf50d60f508c";
+  const char kNonPrimesAfterSaveMD5[] = "4d906b57fba36c70c600cf50d60f508c";
 #else
   const char kNonPrimesMD5[] = "33d9c45bec41ead92a295e252f6b7922";
+  const char kNonPrimesAfterSaveMD5[] = "33d9c45bec41ead92a295e252f6b7922";
 #endif
   {
     ScopedFPDFBitmap page_bitmap = RenderPage(page);
@@ -711,7 +714,7 @@
 
   {
     ScopedFPDFBitmap page_bitmap = RenderPage(saved_page);
-    CompareBitmap(page_bitmap.get(), 200, 200, kNonPrimesMD5);
+    CompareBitmap(page_bitmap.get(), 200, 200, kNonPrimesAfterSaveMD5);
   }
 
   CloseSavedPage(saved_page);
@@ -2668,7 +2671,7 @@
 #if _FX_PLATFORM_ == _FX_PLATFORM_APPLE_
   const char md5[] = "17d2b6cd574cf66170b09c8927529a94";
 #elif _FX_PLATFORM_ == _FX_PLATFORM_WINDOWS_
-    const char md5[] = "d60ba39f9698e32360d99e727dd93165";
+  const char md5[] = "d60ba39f9698e32360d99e727dd93165";
 #else
   const char md5[] = "70592859010ffbf532a2237b8118bcc4";
 #endif