aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar halcanary <halcanary@google.com>2016-02-24 15:46:46 -0800
committerGravatar Commit bot <commit-bot@chromium.org>2016-02-24 15:46:46 -0800
commit8e9f5e39d774198a5a5d9345bc9f863e855c593b (patch)
treeb13c2ed1a24d9791863f254515ecbfaf25927a4c
parentfbe355208d6aff6bdf69a1bd41a93852edf692e7 (diff)
SkPDF: fix scalar serialization
-rw-r--r--gm/skbug_4868.cpp23
-rw-r--r--src/pdf/SkPDFUtils.cpp151
-rw-r--r--src/pdf/SkPDFUtils.h8
-rw-r--r--tests/PDFPrimitivesTest.cpp94
4 files changed, 212 insertions, 64 deletions
diff --git a/gm/skbug_4868.cpp b/gm/skbug_4868.cpp
new file mode 100644
index 0000000000..4f33715a43
--- /dev/null
+++ b/gm/skbug_4868.cpp
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "gm.h"
+
+// clipRect and drawLine should line up exactly when they use the same point.
+// When SkPDF rounds large floats, this doesn't always happen.
+DEF_SIMPLE_GM(skbug_4868, canvas, 32, 32) {
+ canvas->translate(-68.0f, -3378.0f);
+ SkPaint paint;
+ paint.setAntiAlias(true);
+ paint.setStyle(SkPaint::kStroke_Style);
+ canvas->scale(0.56692914f, 0.56692914f);
+ SkRect rc = SkRect::MakeLTRB(158.0f, 5994.80273f, 165.0f, 5998.80225f);
+ canvas->clipRect(rc);
+ canvas->clear(0xFFCECFCE);
+ canvas->drawLine(rc.left(), rc.top(), rc.right(), rc.bottom(), paint);
+ canvas->drawLine(rc.right(), rc.top(), rc.left(), rc.bottom(), paint);
+}
diff --git a/src/pdf/SkPDFUtils.cpp b/src/pdf/SkPDFUtils.cpp
index 108f2c10ab..30d6ee7d68 100644
--- a/src/pdf/SkPDFUtils.cpp
+++ b/src/pdf/SkPDFUtils.cpp
@@ -16,6 +16,8 @@
#include "SkString.h"
#include "SkPDFTypes.h"
+#include <cmath>
+
//static
SkPDFArray* SkPDFUtils::RectToArray(const SkRect& rect) {
SkPDFArray* result = new SkPDFArray();
@@ -254,51 +256,120 @@ void SkPDFUtils::ApplyPattern(int objectIndex, SkWStream* content) {
}
void SkPDFUtils::AppendScalar(SkScalar value, SkWStream* stream) {
- // The range of reals in PDF/A is the same as SkFixed: +/- 32,767 and
- // +/- 1/65,536 (though integers can range from 2^31 - 1 to -2^31).
- // When using floats that are outside the whole value range, we can use
- // integers instead.
-
-#if !defined(SK_ALLOW_LARGE_PDF_SCALARS)
- if (value > 32767 || value < -32767) {
- stream->writeDecAsText(SkScalarRoundToInt(value));
- return;
- }
+ char result[kMaximumFloatDecimalLength];
+ size_t len = SkPDFUtils::FloatToDecimal(SkScalarToFloat(value), result);
+ SkASSERT(len < kMaximumFloatDecimalLength);
+ stream->write(result, len);
+}
+
+/** Write a string into result, includeing a terminating '\0' (for
+ unit testing). Return strlen(result) (for SkWStream::write) The
+ resulting string will be in the form /[-]?([0-9]*.)?[0-9]+/ and
+ sscanf(result, "%f", &x) will return the original value iff the
+ value is finite. This function accepts all possible input values.
+
+ Motivation: "PDF does not support [numbers] in exponential format
+ (such as 6.02e23)." Otherwise, this function would rely on a
+ sprintf-type function from the standard library. */
+size_t SkPDFUtils::FloatToDecimal(float value,
+ char result[kMaximumFloatDecimalLength]) {
+ /* The longest result is -FLT_MIN.
+ We serialize it as "-.0000000000000000000000000000000000000117549435"
+ which has 48 characters plus a terminating '\0'. */
+
+ /* section C.1 of the PDF1.4 spec (http://goo.gl/0SCswJ) says that
+ most PDF rasterizers will use fixed-point scalars that lack the
+ dynamic range of floats. Even if this is the case, I want to
+ serialize these (uncommon) very small and very large scalar
+ values with enough precision to allow a floating-point
+ rasterizer to read them in with perfect accuracy.
+ Experimentally, rasterizers such as pdfium do seem to benefit
+ from this. Rasterizers that rely on fixed-point scalars should
+ gracefully ignore these values that they can not parse. */
+ char* output = &result[0];
+ const char* const end = &result[kMaximumFloatDecimalLength - 1];
+ // subtract one to leave space for '\0'.
- char buffer[SkStrAppendScalar_MaxSize];
- char* end = SkStrAppendFixed(buffer, SkScalarToFixed(value));
- stream->write(buffer, end - buffer);
- return;
-#endif // !SK_ALLOW_LARGE_PDF_SCALARS
-
-#if defined(SK_ALLOW_LARGE_PDF_SCALARS)
- // Floats have 24bits of significance, so anything outside that range is
- // no more precise than an int. (Plus PDF doesn't support scientific
- // notation, so this clamps to SK_Max/MinS32).
- if (value > (1 << 24) || value < -(1 << 24)) {
- stream->writeDecAsText(value);
- return;
+ /* This function is written to accept any possible input value,
+ including non-finite values such as INF and NAN. In that case,
+ we ignore value-correctness and and output a syntacticly-valid
+ number. */
+ if (value == SK_FloatInfinity) {
+ value = FLT_MAX; // nearest finite float.
}
- // Continue to enforce the PDF limits for small floats.
- if (value < 1.0f/65536 && value > -1.0f/65536) {
- stream->writeDecAsText(0);
- return;
+ if (value == SK_FloatNegativeInfinity) {
+ value = -FLT_MAX; // nearest finite float.
}
- // SkStrAppendFloat might still use scientific notation, so use snprintf
- // directly..
- static const int kFloat_MaxSize = 19;
- char buffer[kFloat_MaxSize];
- int len = SNPRINTF(buffer, kFloat_MaxSize, "%#.8f", value);
- // %f always prints trailing 0s, so strip them.
- for (; buffer[len - 1] == '0' && len > 0; len--) {
- buffer[len - 1] = '\0';
+ if (!std::isfinite(value) || value == 0.0f) {
+ // NAN is unsupported in PDF. Always output a valid number.
+ // Also catch zero here, as a special case.
+ *output++ = '0';
+ *output = '\0';
+ return output - result;
}
- if (buffer[len - 1] == '.') {
- buffer[len - 1] = '\0';
+ // Inspired by:
+ // http://www.exploringbinary.com/quick-and-dirty-floating-point-to-decimal-conversion/
+
+ if (value < 0.0) {
+ *output++ = '-';
+ value = -value;
+ }
+ SkASSERT(value >= 0.0f);
+
+ // Must use double math to keep precision right.
+ double intPart;
+ double fracPart = std::modf(static_cast<double>(value), &intPart);
+ SkASSERT(intPart + fracPart == static_cast<double>(value));
+ size_t significantDigits = 0;
+ const size_t maxSignificantDigits = 9;
+ // Any fewer significant digits loses precision. The unit test
+ // checks round-trip correctness.
+ SkASSERT(intPart >= 0.0 && fracPart >= 0.0); // negative handled already.
+ SkASSERT(intPart > 0.0 || fracPart > 0.0); // zero already caught.
+ if (intPart > 0.0) {
+ // put the intPart digits onto a stack for later reversal.
+ char reversed[1 + FLT_MAX_10_EXP]; // 39 == 1 + FLT_MAX_10_EXP
+ // the largest integer part is FLT_MAX; it has 39 decimal digits.
+ size_t reversedIndex = 0;
+ do {
+ SkASSERT(reversedIndex < sizeof(reversed));
+ int digit = static_cast<int>(std::fmod(intPart, 10.0));
+ SkASSERT(digit >= 0 && digit <= 9);
+ reversed[reversedIndex++] = '0' + digit;
+ intPart = std::floor(intPart / 10.0);
+ } while (intPart > 0.0);
+ significantDigits = reversedIndex;
+ SkASSERT(reversedIndex <= sizeof(reversed));
+ SkASSERT(output + reversedIndex <= end);
+ while (reversedIndex-- > 0) { // pop from stack, append to result
+ *output++ = reversed[reversedIndex];
+ }
+ }
+ if (fracPart > 0 && significantDigits < maxSignificantDigits) {
+ *output++ = '.';
+ SkASSERT(output <= end);
+ do {
+ fracPart = std::modf(fracPart * 10.0, &intPart);
+ int digit = static_cast<int>(intPart);
+ SkASSERT(digit >= 0 && digit <= 9);
+ *output++ = '0' + digit;
+ SkASSERT(output <= end);
+ if (digit > 0 || significantDigits > 0) {
+ // start counting significantDigits after first non-zero digit.
+ ++significantDigits;
+ }
+ } while (fracPart > 0.0
+ && significantDigits < maxSignificantDigits
+ && output < end);
+ // When fracPart == 0, additional digits will be zero.
+ // When significantDigits == maxSignificantDigits, we can stop.
+ // when output == end, we have filed the string.
+ // Note: denormalized numbers will not have the same number of
+ // significantDigits, but do not need them to round-trip.
}
- stream->writeText(buffer);
- return;
-#endif // SK_ALLOW_LARGE_PDF_SCALARS
+ SkASSERT(output <= end);
+ *output = '\0';
+ return output - result;
}
SkString SkPDFUtils::FormatString(const char* cin, size_t len) {
diff --git a/src/pdf/SkPDFUtils.h b/src/pdf/SkPDFUtils.h
index 0aa05a088a..814d77193c 100644
--- a/src/pdf/SkPDFUtils.h
+++ b/src/pdf/SkPDFUtils.h
@@ -58,6 +58,14 @@ public:
static void DrawFormXObject(int objectIndex, SkWStream* content);
static void ApplyGraphicState(int objectIndex, SkWStream* content);
static void ApplyPattern(int objectIndex, SkWStream* content);
+
+ // 3 = '-', '.', and '\0' characters.
+ // 9 = number of significant digits
+ // abs(FLT_MIN_10_EXP) = number of zeros in FLT_MIN
+ static const size_t kMaximumFloatDecimalLength = 3 + 9 - FLT_MIN_10_EXP;
+ // FloatToDecimal is exposed for unit tests.
+ static size_t FloatToDecimal(float value,
+ char output[kMaximumFloatDecimalLength]);
static void AppendScalar(SkScalar value, SkWStream* stream);
static SkString FormatString(const char* input, size_t len);
};
diff --git a/tests/PDFPrimitivesTest.cpp b/tests/PDFPrimitivesTest.cpp
index 9ca1bd619a..f618e6f68b 100644
--- a/tests/PDFPrimitivesTest.cpp
+++ b/tests/PDFPrimitivesTest.cpp
@@ -18,6 +18,7 @@
#include "SkPDFFont.h"
#include "SkPDFStream.h"
#include "SkPDFTypes.h"
+#include "SkPDFUtils.h"
#include "SkReadBuffer.h"
#include "SkScalar.h"
#include "SkStream.h"
@@ -199,20 +200,16 @@ static void TestPDFUnion(skiatest::Reporter* reporter) {
ASSERT_EMIT_EQ(reporter, int42, "42");
SkPDFUnion realHalf = SkPDFUnion::Scalar(SK_ScalarHalf);
- ASSERT_EMIT_EQ(reporter, realHalf, "0.5");
+ ASSERT_EMIT_EQ(reporter, realHalf, ".5");
SkPDFUnion bigScalar = SkPDFUnion::Scalar(110999.75f);
-#if !defined(SK_ALLOW_LARGE_PDF_SCALARS)
- ASSERT_EMIT_EQ(reporter, bigScalar, "111000");
-#else
ASSERT_EMIT_EQ(reporter, bigScalar, "110999.75");
- SkPDFUnion biggerScalar = SkPDFUnion::Scalar(50000000.1);
+ SkPDFUnion biggerScalar = SkPDFUnion::Scalar(50000000.1f);
ASSERT_EMIT_EQ(reporter, biggerScalar, "50000000");
- SkPDFUnion smallestScalar = SkPDFUnion::Scalar(1.0 / 65536);
- ASSERT_EMIT_EQ(reporter, smallestScalar, "0.00001526");
-#endif
+ SkPDFUnion smallestScalar = SkPDFUnion::Scalar(1.0f / 65536);
+ ASSERT_EMIT_EQ(reporter, smallestScalar, ".0000152587890");
SkPDFUnion stringSimple = SkPDFUnion::String("test ) string ( foo");
ASSERT_EMIT_EQ(reporter, stringSimple, "(test \\) string \\( foo)");
@@ -248,34 +245,34 @@ static void TestPDFArray(skiatest::Reporter* reporter) {
ASSERT_EMIT_EQ(reporter, *array, "[42]");
array->appendScalar(SK_ScalarHalf);
- ASSERT_EMIT_EQ(reporter, *array, "[42 0.5]");
+ ASSERT_EMIT_EQ(reporter, *array, "[42 .5]");
array->appendInt(0);
- ASSERT_EMIT_EQ(reporter, *array, "[42 0.5 0]");
+ ASSERT_EMIT_EQ(reporter, *array, "[42 .5 0]");
array->appendBool(true);
- ASSERT_EMIT_EQ(reporter, *array, "[42 0.5 0 true]");
+ ASSERT_EMIT_EQ(reporter, *array, "[42 .5 0 true]");
array->appendName("ThisName");
- ASSERT_EMIT_EQ(reporter, *array, "[42 0.5 0 true /ThisName]");
+ ASSERT_EMIT_EQ(reporter, *array, "[42 .5 0 true /ThisName]");
array->appendName(SkString("AnotherName"));
- ASSERT_EMIT_EQ(reporter, *array, "[42 0.5 0 true /ThisName /AnotherName]");
+ ASSERT_EMIT_EQ(reporter, *array, "[42 .5 0 true /ThisName /AnotherName]");
array->appendString("This String");
ASSERT_EMIT_EQ(reporter, *array,
- "[42 0.5 0 true /ThisName /AnotherName (This String)]");
+ "[42 .5 0 true /ThisName /AnotherName (This String)]");
array->appendString(SkString("Another String"));
ASSERT_EMIT_EQ(reporter, *array,
- "[42 0.5 0 true /ThisName /AnotherName (This String) "
+ "[42 .5 0 true /ThisName /AnotherName (This String) "
"(Another String)]");
SkAutoTUnref<SkPDFArray> innerArray(new SkPDFArray);
innerArray->appendInt(-1);
array->appendObject(innerArray.detach());
ASSERT_EMIT_EQ(reporter, *array,
- "[42 0.5 0 true /ThisName /AnotherName (This String) "
+ "[42 .5 0 true /ThisName /AnotherName (This String) "
"(Another String) [-1]]");
SkAutoTUnref<SkPDFArray> referencedArray(new SkPDFArray);
@@ -287,7 +284,7 @@ static void TestPDFArray(skiatest::Reporter* reporter) {
SkString result = emit_to_string(*array, &catalog);
ASSERT_EQ(reporter, result,
- "[42 0.5 0 true /ThisName /AnotherName (This String) "
+ "[42 .5 0 true /ThisName /AnotherName (This String) "
"(Another String) [-1] 1 0 R]");
}
@@ -310,7 +307,7 @@ static void TestPDFDict(skiatest::Reporter* reporter) {
SkAutoTUnref<SkPDFArray> innerArray(new SkPDFArray);
innerArray->appendInt(-100);
dict->insertObject(n3, innerArray.detach());
- ASSERT_EMIT_EQ(reporter, *dict, "<</n1 42\n/n2 0.5\n/n3 [-100]>>");
+ ASSERT_EMIT_EQ(reporter, *dict, "<</n1 42\n/n2 .5\n/n3 [-100]>>");
dict.reset(new SkPDFDict);
ASSERT_EMIT_EQ(reporter, *dict, "<<>>");
@@ -322,26 +319,26 @@ static void TestPDFDict(skiatest::Reporter* reporter) {
ASSERT_EMIT_EQ(reporter, *dict, "<</n1 24\n/n2 99>>");
dict->insertScalar("n3", SK_ScalarHalf);
- ASSERT_EMIT_EQ(reporter, *dict, "<</n1 24\n/n2 99\n/n3 0.5>>");
+ ASSERT_EMIT_EQ(reporter, *dict, "<</n1 24\n/n2 99\n/n3 .5>>");
dict->insertName("n4", "AName");
- ASSERT_EMIT_EQ(reporter, *dict, "<</n1 24\n/n2 99\n/n3 0.5\n/n4 /AName>>");
+ ASSERT_EMIT_EQ(reporter, *dict, "<</n1 24\n/n2 99\n/n3 .5\n/n4 /AName>>");
dict->insertName("n5", SkString("AnotherName"));
- ASSERT_EMIT_EQ(reporter, *dict, "<</n1 24\n/n2 99\n/n3 0.5\n/n4 /AName\n"
+ ASSERT_EMIT_EQ(reporter, *dict, "<</n1 24\n/n2 99\n/n3 .5\n/n4 /AName\n"
"/n5 /AnotherName>>");
dict->insertString("n6", "A String");
- ASSERT_EMIT_EQ(reporter, *dict, "<</n1 24\n/n2 99\n/n3 0.5\n/n4 /AName\n"
+ ASSERT_EMIT_EQ(reporter, *dict, "<</n1 24\n/n2 99\n/n3 .5\n/n4 /AName\n"
"/n5 /AnotherName\n/n6 (A String)>>");
dict->insertString("n7", SkString("Another String"));
- ASSERT_EMIT_EQ(reporter, *dict, "<</n1 24\n/n2 99\n/n3 0.5\n/n4 /AName\n"
+ ASSERT_EMIT_EQ(reporter, *dict, "<</n1 24\n/n2 99\n/n3 .5\n/n4 /AName\n"
"/n5 /AnotherName\n/n6 (A String)\n/n7 (Another String)>>");
dict.reset(new SkPDFDict("DType"));
ASSERT_EMIT_EQ(reporter, *dict, "<</Type /DType>>");
-
+
SkAutoTUnref<SkPDFArray> referencedArray(new SkPDFArray);
Catalog catalog;
catalog.numbers.addObject(referencedArray.get());
@@ -435,3 +432,52 @@ DEF_TEST(PDFFontCanEmbedTypeface, reporter) {
REPORTER_ASSERT(reporter,
SkPDFFont::CanEmbedTypeface(portableTypeface, &canon));
}
+
+
+// test to see that all finite scalars round trip via scanf().
+static void check_pdf_scalar_serialization(
+ skiatest::Reporter* reporter, float inputFloat) {
+ char floatString[SkPDFUtils::kMaximumFloatDecimalLength];
+ size_t len = SkPDFUtils::FloatToDecimal(inputFloat, floatString);
+ if (len >= sizeof(floatString)) {
+ ERRORF(reporter, "string too long: %u", (unsigned)len);
+ return;
+ }
+ if (floatString[len] != '\0' || strlen(floatString) != len) {
+ ERRORF(reporter, "terminator misplaced.");
+ return; // The terminator is needed for sscanf().
+ }
+ if (reporter->verbose()) {
+ SkDebugf("%15.9g = \"%s\"\n", inputFloat, floatString);
+ }
+ float roundTripFloat;
+ if (1 != sscanf(floatString, "%f", &roundTripFloat)) {
+ ERRORF(reporter, "unscannable result: %s", floatString);
+ return;
+ }
+ if (isfinite(inputFloat) && roundTripFloat != inputFloat) {
+ ERRORF(reporter, "roundTripFloat (%.9g) != inputFloat (%.9g)",
+ roundTripFloat, inputFloat);
+ }
+}
+
+// Test SkPDFUtils::AppendScalar for accuracy.
+DEF_TEST(PDFPrimitives_Scalar, reporter) {
+ SkRandom random(0x5EED);
+ int iterationCount = 512;
+ while (iterationCount-- > 0) {
+ union { uint32_t u; float f; };
+ u = random.nextU();
+ static_assert(sizeof(float) == sizeof(uint32_t), "");
+ check_pdf_scalar_serialization(reporter, f);
+ }
+ float alwaysCheck[] = {
+ 0.0f, -0.0f, 1.0f, -1.0f, SK_ScalarPI, 0.1f, FLT_MIN, FLT_MAX,
+ -FLT_MIN, -FLT_MAX, FLT_MIN / 16.0f, -FLT_MIN / 16.0f,
+ SK_FloatNaN, SK_FloatInfinity, SK_FloatNegativeInfinity,
+ -FLT_MIN / 8388608.0
+ };
+ for (float inputFloat: alwaysCheck) {
+ check_pdf_scalar_serialization(reporter, inputFloat);
+ }
+}