Browse Source

Fixed asymmetrical range interaction with scanline and winding reversal, default format in non-PNG build changed to BMP

Chlumsky 2 weeks ago
parent
commit
b25851c859
3 changed files with 81 additions and 70 deletions
  1. 32 18
      core/rasterization.cpp
  2. 7 4
      core/rasterization.h
  3. 42 48
      main.cpp

+ 32 - 18
core/rasterization.cpp

@@ -16,26 +16,28 @@ void rasterize(BitmapSection<float, 1> output, const Shape &shape, const Project
     }
     }
 }
 }
 
 
-void distanceSignCorrection(BitmapSection<float, 1> sdf, const Shape &shape, const Projection &projection, FillRule fillRule) {
+void distanceSignCorrection(BitmapSection<float, 1> sdf, const Shape &shape, const Projection &projection, float sdfZeroValue, FillRule fillRule) {
     sdf.reorient(shape.getYAxisOrientation());
     sdf.reorient(shape.getYAxisOrientation());
+    float doubleSdfZeroValue = sdfZeroValue+sdfZeroValue;
     Scanline scanline;
     Scanline scanline;
     for (int y = 0; y < sdf.height; ++y) {
     for (int y = 0; y < sdf.height; ++y) {
         shape.scanline(scanline, projection.unprojectY(y+.5));
         shape.scanline(scanline, projection.unprojectY(y+.5));
         for (int x = 0; x < sdf.width; ++x) {
         for (int x = 0; x < sdf.width; ++x) {
             bool fill = scanline.filled(projection.unprojectX(x+.5), fillRule);
             bool fill = scanline.filled(projection.unprojectX(x+.5), fillRule);
             float &sd = *sdf(x, y);
             float &sd = *sdf(x, y);
-            if ((sd > .5f) != fill)
-                sd = 1.f-sd;
+            if ((sd > sdfZeroValue) != fill)
+                sd = doubleSdfZeroValue-sd;
         }
         }
     }
     }
 }
 }
 
 
 template <int N>
 template <int N>
-static void multiDistanceSignCorrection(BitmapSection<float, N> sdf, const Shape &shape, const Projection &projection, FillRule fillRule) {
+static void multiDistanceSignCorrection(BitmapSection<float, N> sdf, const Shape &shape, const Projection &projection, float sdfZeroValue, FillRule fillRule) {
     int w = sdf.width, h = sdf.height;
     int w = sdf.width, h = sdf.height;
     if (!(w && h))
     if (!(w && h))
         return;
         return;
     sdf.reorient(shape.getYAxisOrientation());
     sdf.reorient(shape.getYAxisOrientation());
+    float doubleSdfZeroValue = sdfZeroValue+sdfZeroValue;
     Scanline scanline;
     Scanline scanline;
     bool ambiguous = false;
     bool ambiguous = false;
     std::vector<char> matchMap;
     std::vector<char> matchMap;
@@ -47,17 +49,17 @@ static void multiDistanceSignCorrection(BitmapSection<float, N> sdf, const Shape
             bool fill = scanline.filled(projection.unprojectX(x+.5), fillRule);
             bool fill = scanline.filled(projection.unprojectX(x+.5), fillRule);
             float *msd = sdf(x, y);
             float *msd = sdf(x, y);
             float sd = median(msd[0], msd[1], msd[2]);
             float sd = median(msd[0], msd[1], msd[2]);
-            if (sd == .5f)
+            if (sd == sdfZeroValue)
                 ambiguous = true;
                 ambiguous = true;
-            else if ((sd > .5f) != fill) {
-                msd[0] = 1.f-msd[0];
-                msd[1] = 1.f-msd[1];
-                msd[2] = 1.f-msd[2];
+            else if ((sd > sdfZeroValue) != fill) {
+                msd[0] = doubleSdfZeroValue-msd[0];
+                msd[1] = doubleSdfZeroValue-msd[1];
+                msd[2] = doubleSdfZeroValue-msd[2];
                 *match = -1;
                 *match = -1;
             } else
             } else
                 *match = 1;
                 *match = 1;
-            if (N >= 4 && (msd[3] > .5f) != fill)
-                msd[3] = 1.f-msd[3];
+            if (N >= 4 && (msd[3] > sdfZeroValue) != fill)
+                msd[3] = doubleSdfZeroValue-msd[3];
             ++match;
             ++match;
         }
         }
     }
     }
@@ -74,9 +76,9 @@ static void multiDistanceSignCorrection(BitmapSection<float, N> sdf, const Shape
                     if (y < h-1) neighborMatch += *(match+w);
                     if (y < h-1) neighborMatch += *(match+w);
                     if (neighborMatch < 0) {
                     if (neighborMatch < 0) {
                         float *msd = sdf(x, y);
                         float *msd = sdf(x, y);
-                        msd[0] = 1.f-msd[0];
-                        msd[1] = 1.f-msd[1];
-                        msd[2] = 1.f-msd[2];
+                        msd[0] = doubleSdfZeroValue-msd[0];
+                        msd[1] = doubleSdfZeroValue-msd[1];
+                        msd[2] = doubleSdfZeroValue-msd[2];
                     }
                     }
                 }
                 }
                 ++match;
                 ++match;
@@ -85,12 +87,12 @@ static void multiDistanceSignCorrection(BitmapSection<float, N> sdf, const Shape
     }
     }
 }
 }
 
 
-void distanceSignCorrection(BitmapSection<float, 3> sdf, const Shape &shape, const Projection &projection, FillRule fillRule) {
-    multiDistanceSignCorrection(sdf, shape, projection, fillRule);
+void distanceSignCorrection(BitmapSection<float, 3> sdf, const Shape &shape, const Projection &projection, float sdfZeroValue, FillRule fillRule) {
+    multiDistanceSignCorrection(sdf, shape, projection, sdfZeroValue, fillRule);
 }
 }
 
 
-void distanceSignCorrection(BitmapSection<float, 4> sdf, const Shape &shape, const Projection &projection, FillRule fillRule) {
-    multiDistanceSignCorrection(sdf, shape, projection, fillRule);
+void distanceSignCorrection(BitmapSection<float, 4> sdf, const Shape &shape, const Projection &projection, float sdfZeroValue, FillRule fillRule) {
+    multiDistanceSignCorrection(sdf, shape, projection, sdfZeroValue, fillRule);
 }
 }
 
 
 // Legacy API
 // Legacy API
@@ -99,6 +101,18 @@ void rasterize(const BitmapSection<float, 1> &output, const Shape &shape, const
     rasterize(output, shape, Projection(scale, translate), fillRule);
     rasterize(output, shape, Projection(scale, translate), fillRule);
 }
 }
 
 
+void distanceSignCorrection(BitmapSection<float, 1> sdf, const Shape &shape, const Projection &projection, FillRule fillRule) {
+    distanceSignCorrection(sdf, shape, projection, .5f, fillRule);
+}
+
+void distanceSignCorrection(BitmapSection<float, 3> sdf, const Shape &shape, const Projection &projection, FillRule fillRule) {
+    distanceSignCorrection(sdf, shape, projection, .5f, fillRule);
+}
+
+void distanceSignCorrection(BitmapSection<float, 4> sdf, const Shape &shape, const Projection &projection, FillRule fillRule) {
+    distanceSignCorrection(sdf, shape, projection, .5f, fillRule);
+}
+
 void distanceSignCorrection(const BitmapSection<float, 1> &sdf, const Shape &shape, const Vector2 &scale, const Vector2 &translate, FillRule fillRule) {
 void distanceSignCorrection(const BitmapSection<float, 1> &sdf, const Shape &shape, const Vector2 &scale, const Vector2 &translate, FillRule fillRule) {
     distanceSignCorrection(sdf, shape, Projection(scale, translate), fillRule);
     distanceSignCorrection(sdf, shape, Projection(scale, translate), fillRule);
 }
 }

+ 7 - 4
core/rasterization.h

@@ -12,12 +12,15 @@ namespace msdfgen {
 /// Rasterizes the shape into a monochrome bitmap.
 /// Rasterizes the shape into a monochrome bitmap.
 void rasterize(BitmapSection<float, 1> output, const Shape &shape, const Projection &projection, FillRule fillRule = FILL_NONZERO);
 void rasterize(BitmapSection<float, 1> output, const Shape &shape, const Projection &projection, FillRule fillRule = FILL_NONZERO);
 /// Fixes the sign of the input signed distance field, so that it matches the shape's rasterized fill.
 /// Fixes the sign of the input signed distance field, so that it matches the shape's rasterized fill.
-void distanceSignCorrection(BitmapSection<float, 1> sdf, const Shape &shape, const Projection &projection, FillRule fillRule = FILL_NONZERO);
-void distanceSignCorrection(BitmapSection<float, 3> sdf, const Shape &shape, const Projection &projection, FillRule fillRule = FILL_NONZERO);
-void distanceSignCorrection(BitmapSection<float, 4> sdf, const Shape &shape, const Projection &projection, FillRule fillRule = FILL_NONZERO);
+void distanceSignCorrection(BitmapSection<float, 1> sdf, const Shape &shape, const Projection &projection, float sdfZeroValue = .5f, FillRule fillRule = FILL_NONZERO);
+void distanceSignCorrection(BitmapSection<float, 3> sdf, const Shape &shape, const Projection &projection, float sdfZeroValue = .5f, FillRule fillRule = FILL_NONZERO);
+void distanceSignCorrection(BitmapSection<float, 4> sdf, const Shape &shape, const Projection &projection, float sdfZeroValue = .5f, FillRule fillRule = FILL_NONZERO);
 
 
-// Old version of the function API's kept for backwards compatibility
+// Old versions of the function API's kept for backwards compatibility
 void rasterize(const BitmapSection<float, 1> &output, const Shape &shape, const Vector2 &scale, const Vector2 &translate, FillRule fillRule = FILL_NONZERO);
 void rasterize(const BitmapSection<float, 1> &output, const Shape &shape, const Vector2 &scale, const Vector2 &translate, FillRule fillRule = FILL_NONZERO);
+void distanceSignCorrection(BitmapSection<float, 1> sdf, const Shape &shape, const Projection &projection, FillRule fillRule);
+void distanceSignCorrection(BitmapSection<float, 3> sdf, const Shape &shape, const Projection &projection, FillRule fillRule);
+void distanceSignCorrection(BitmapSection<float, 4> sdf, const Shape &shape, const Projection &projection, FillRule fillRule);
 void distanceSignCorrection(const BitmapSection<float, 1> &sdf, const Shape &shape, const Vector2 &scale, const Vector2 &translate, FillRule fillRule = FILL_NONZERO);
 void distanceSignCorrection(const BitmapSection<float, 1> &sdf, const Shape &shape, const Vector2 &scale, const Vector2 &translate, FillRule fillRule = FILL_NONZERO);
 void distanceSignCorrection(const BitmapSection<float, 3> &sdf, const Shape &shape, const Vector2 &scale, const Vector2 &translate, FillRule fillRule = FILL_NONZERO);
 void distanceSignCorrection(const BitmapSection<float, 3> &sdf, const Shape &shape, const Vector2 &scale, const Vector2 &translate, FillRule fillRule = FILL_NONZERO);
 void distanceSignCorrection(const BitmapSection<float, 4> &sdf, const Shape &shape, const Vector2 &scale, const Vector2 &translate, FillRule fillRule = FILL_NONZERO);
 void distanceSignCorrection(const BitmapSection<float, 4> &sdf, const Shape &shape, const Vector2 &scale, const Vector2 &translate, FillRule fillRule = FILL_NONZERO);

+ 42 - 48
main.cpp

@@ -35,8 +35,8 @@
 #define DEFAULT_IMAGE_EXTENSION "png"
 #define DEFAULT_IMAGE_EXTENSION "png"
 #define SAVE_DEFAULT_IMAGE_FORMAT savePng
 #define SAVE_DEFAULT_IMAGE_FORMAT savePng
 #else
 #else
-#define DEFAULT_IMAGE_EXTENSION "tiff"
-#define SAVE_DEFAULT_IMAGE_FORMAT saveTiff
+#define DEFAULT_IMAGE_EXTENSION "bmp"
+#define SAVE_DEFAULT_IMAGE_FORMAT saveBmp
 #endif
 #endif
 
 
 using namespace msdfgen;
 using namespace msdfgen;
@@ -193,15 +193,6 @@ static FontHandle *loadVarFont(FreetypeHandle *library, const char *filename) {
 #endif
 #endif
 #endif
 #endif
 
 
-template <int N>
-static void invertColor(const BitmapSection<float, N> &bitmap) {
-    for (int y = 0; y < bitmap.height; ++y) {
-        float *p = bitmap(0, y);
-        for (const float *end = p+N*bitmap.width; p < end; ++p)
-            *p = 1.f-*p;
-    }
-}
-
 static bool writeTextBitmap(FILE *file, const float *values, int cols, int rows, int rowStride) {
 static bool writeTextBitmap(FILE *file, const float *values, int cols, int rows, int rowStride) {
     for (int row = 0; row < rows; ++row) {
     for (int row = 0; row < rows; ++row) {
         const float *cur = values;
         const float *cur = values;
@@ -452,7 +443,7 @@ static const char *const helpText =
     "  -format <bmp / tiff / rgba / fl32 / text / textfloat / bin / binfloat / binfloatbe>\n"
     "  -format <bmp / tiff / rgba / fl32 / text / textfloat / bin / binfloat / binfloatbe>\n"
 #endif
 #endif
         "\tSpecifies the output format of the distance field. Otherwise it is chosen based on output file extension.\n"
         "\tSpecifies the output format of the distance field. Otherwise it is chosen based on output file extension.\n"
-    "  -guessorder\n"
+    "  -guesswinding\n"
         "\tAttempts to detect if shape contours have the wrong winding and generates the SDF with the right one.\n"
         "\tAttempts to detect if shape contours have the wrong winding and generates the SDF with the right one.\n"
     "  -help\n"
     "  -help\n"
         "\tDisplays this help.\n"
         "\tDisplays this help.\n"
@@ -483,7 +474,7 @@ static const char *const helpText =
         "\tSets the width of the range between the lowest and highest signed distance in pixels.\n"
         "\tSets the width of the range between the lowest and highest signed distance in pixels.\n"
     "  -range <range>\n"
     "  -range <range>\n"
         "\tSets the width of the range between the lowest and highest signed distance in shape units.\n"
         "\tSets the width of the range between the lowest and highest signed distance in shape units.\n"
-    "  -reverseorder\n"
+    "  -reversewinding\n"
         "\tGenerates the distance field as if the shape's vertices were in reverse order.\n"
         "\tGenerates the distance field as if the shape's vertices were in reverse order.\n"
     "  -scale <scale>\n"
     "  -scale <scale>\n"
         "\tSets the scale used to convert shape units to pixels.\n"
         "\tSets the scale used to convert shape units to pixels.\n"
@@ -499,10 +490,10 @@ static const char *const helpText =
 #if defined(MSDFGEN_EXTENSIONS) && !defined(MSDFGEN_DISABLE_PNG)
 #if defined(MSDFGEN_EXTENSIONS) && !defined(MSDFGEN_DISABLE_PNG)
         "\tRenders an image preview using the generated distance field and saves it as a PNG file.\n"
         "\tRenders an image preview using the generated distance field and saves it as a PNG file.\n"
 #else
 #else
-        "\tRenders an image preview using the generated distance field and saves it as a TIFF file.\n"
+        "\tRenders an image preview using the generated distance field and saves it as a BMP file.\n"
 #endif
 #endif
     "  -testrendermulti <filename." DEFAULT_IMAGE_EXTENSION "> <width> <height>\n"
     "  -testrendermulti <filename." DEFAULT_IMAGE_EXTENSION "> <width> <height>\n"
-        "\tRenders an image preview without flattening the color channels.\n"
+        "\tRenders an image preview without resolving the color channels.\n"
     "  -translate <x> <y>\n"
     "  -translate <x> <y>\n"
         "\tSets the translation of the shape in shape units.\n"
         "\tSets the translation of the shape in shape units.\n"
     "  -version\n"
     "  -version\n"
@@ -510,7 +501,7 @@ static const char *const helpText =
     "  -windingpreprocess\n"
     "  -windingpreprocess\n"
         "\tAttempts to fix only the contour windings assuming no self-intersections and even-odd fill rule.\n"
         "\tAttempts to fix only the contour windings assuming no self-intersections and even-odd fill rule.\n"
     "  -yflip\n"
     "  -yflip\n"
-        "\tInverts the Y axis in the output distance field. The default order is bottom to top.\n"
+        "\tInverts the Y-axis in the output distance field. The default orientation is upward.\n"
     "\n";
     "\n";
 
 
 static const char *errorCorrectionHelpText =
 static const char *errorCorrectionHelpText =
@@ -612,7 +603,7 @@ int main(int argc, const char *const *argv) {
         KEEP,
         KEEP,
         REVERSE,
         REVERSE,
         GUESS
         GUESS
-    } orientation = KEEP;
+    } winding = KEEP;
     unsigned long long coloringSeed = 0;
     unsigned long long coloringSeed = 0;
     void (*edgeColoring)(Shape &, double, unsigned long long) = &edgeColoringSimple;
     void (*edgeColoring)(Shape &, double, unsigned long long) = &edgeColoringSimple;
     bool explicitErrorCorrectionMode = false;
     bool explicitErrorCorrectionMode = false;
@@ -982,16 +973,16 @@ int main(int argc, const char *const *argv) {
             estimateError = true;
             estimateError = true;
             continue;
             continue;
         }
         }
-        ARG_CASE("-keeporder", 0) {
-            orientation = KEEP;
+        ARG_CASE("-keepwinding" ARG_CASE_OR "-keeporder", 0) {
+            winding = KEEP;
             continue;
             continue;
         }
         }
-        ARG_CASE("-reverseorder", 0) {
-            orientation = REVERSE;
+        ARG_CASE("-reversewinding" ARG_CASE_OR "-reverseorder", 0) {
+            winding = REVERSE;
             continue;
             continue;
         }
         }
-        ARG_CASE("-guessorder", 0) {
-            orientation = GUESS;
+        ARG_CASE("-guesswinding" ARG_CASE_OR "-guessorder", 0) {
+            winding = GUESS;
             continue;
             continue;
         }
         }
         ARG_CASE("-seed", 1) {
         ARG_CASE("-seed", 1) {
@@ -1137,9 +1128,20 @@ int main(int argc, const char *const *argv) {
 
 
     double avgScale = .5*(scale.x+scale.y);
     double avgScale = .5*(scale.x+scale.y);
     Shape::Bounds bounds = { };
     Shape::Bounds bounds = { };
-    if (autoFrame || mode == METRICS || printMetrics || orientation == GUESS || svgExport)
+    if (autoFrame || mode == METRICS || printMetrics || winding == GUESS || svgExport)
         bounds = shape.getBounds();
         bounds = shape.getBounds();
 
 
+    if (winding == GUESS) {
+        // Get sign of signed distance outside bounds
+        Point2 p(bounds.l-(bounds.r-bounds.l)-1, bounds.b-(bounds.t-bounds.b)-1);
+        double distance = SimpleTrueShapeDistanceFinder::oneShotDistance(shape, p);
+        winding = distance <= 0 ? KEEP : REVERSE;
+    }
+    if (winding == REVERSE) {
+        for (std::vector<Contour>::iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour)
+            contour->reverse();
+    }
+
     if (outputDistanceShift) {
     if (outputDistanceShift) {
         Range &rangeRef = rangeMode == RANGE_PX ? pxRange : range;
         Range &rangeRef = rangeMode == RANGE_PX ? pxRange : range;
         double rangeShift = -outputDistanceShift*(rangeRef.upper-rangeRef.lower);
         double rangeShift = -outputDistanceShift*(rangeRef.upper-rangeRef.lower);
@@ -1276,39 +1278,19 @@ int main(int argc, const char *const *argv) {
         default:;
         default:;
     }
     }
 
 
-    if (orientation == GUESS) {
-        // Get sign of signed distance outside bounds
-        Point2 p(bounds.l-(bounds.r-bounds.l)-1, bounds.b-(bounds.t-bounds.b)-1);
-        double distance = SimpleTrueShapeDistanceFinder::oneShotDistance(shape, p);
-        orientation = distance <= 0 ? KEEP : REVERSE;
-    }
-    if (orientation == REVERSE) {
-        switch (mode) {
-            case SINGLE:
-            case PERPENDICULAR:
-                invertColor<1>(sdf);
-                break;
-            case MULTI:
-                invertColor<3>(msdf);
-                break;
-            case MULTI_AND_TRUE:
-                invertColor<4>(mtsdf);
-                break;
-            default:;
-        }
-    }
     if (scanlinePass) {
     if (scanlinePass) {
+        float sdfZeroValue = range.lower != range.upper ? float(range.lower/(range.lower-range.upper)) : .5f;
         switch (mode) {
         switch (mode) {
             case SINGLE:
             case SINGLE:
             case PERPENDICULAR:
             case PERPENDICULAR:
-                distanceSignCorrection(sdf, shape, transformation, fillRule);
+                distanceSignCorrection(sdf, shape, transformation, sdfZeroValue, fillRule);
                 break;
                 break;
             case MULTI:
             case MULTI:
-                distanceSignCorrection(msdf, shape, transformation, fillRule);
+                distanceSignCorrection(msdf, shape, transformation, sdfZeroValue, fillRule);
                 msdfErrorCorrection(msdf, shape, transformation, postErrorCorrectionConfig);
                 msdfErrorCorrection(msdf, shape, transformation, postErrorCorrectionConfig);
                 break;
                 break;
             case MULTI_AND_TRUE:
             case MULTI_AND_TRUE:
-                distanceSignCorrection(mtsdf, shape, transformation, fillRule);
+                distanceSignCorrection(mtsdf, shape, transformation, sdfZeroValue, fillRule);
                 msdfErrorCorrection(mtsdf, shape, transformation, postErrorCorrectionConfig);
                 msdfErrorCorrection(mtsdf, shape, transformation, postErrorCorrectionConfig);
                 break;
                 break;
             default:;
             default:;
@@ -1344,12 +1326,16 @@ int main(int argc, const char *const *argv) {
             if (testRenderMulti) {
             if (testRenderMulti) {
                 Bitmap<float, 3> render(testWidthM, testHeightM);
                 Bitmap<float, 3> render(testWidthM, testHeightM);
                 renderSDF(render, sdf, avgScale*range);
                 renderSDF(render, sdf, avgScale*range);
+                if (!cmpExtension(testRenderMulti, "." DEFAULT_IMAGE_EXTENSION))
+                    fputs("Warning: -testrendermulti specified with an extension other than ." DEFAULT_IMAGE_EXTENSION " but will be saved in that format anyway.\n", stderr);
                 if (!SAVE_DEFAULT_IMAGE_FORMAT(render, testRenderMulti))
                 if (!SAVE_DEFAULT_IMAGE_FORMAT(render, testRenderMulti))
                     fputs("Failed to write test render file.\n", stderr);
                     fputs("Failed to write test render file.\n", stderr);
             }
             }
             if (testRender) {
             if (testRender) {
                 Bitmap<float, 1> render(testWidth, testHeight);
                 Bitmap<float, 1> render(testWidth, testHeight);
                 renderSDF(render, sdf, avgScale*range);
                 renderSDF(render, sdf, avgScale*range);
+                if (!cmpExtension(testRender, "." DEFAULT_IMAGE_EXTENSION))
+                    fputs("Warning: -testrender specified with an extension other than ." DEFAULT_IMAGE_EXTENSION " but will be saved in that format anyway.\n", stderr);
                 if (!SAVE_DEFAULT_IMAGE_FORMAT(render, testRender))
                 if (!SAVE_DEFAULT_IMAGE_FORMAT(render, testRender))
                     fputs("Failed to write test render file.\n", stderr);
                     fputs("Failed to write test render file.\n", stderr);
             }
             }
@@ -1368,12 +1354,16 @@ int main(int argc, const char *const *argv) {
             if (testRenderMulti) {
             if (testRenderMulti) {
                 Bitmap<float, 3> render(testWidthM, testHeightM);
                 Bitmap<float, 3> render(testWidthM, testHeightM);
                 renderSDF(render, msdf, avgScale*range);
                 renderSDF(render, msdf, avgScale*range);
+                if (!cmpExtension(testRenderMulti, "." DEFAULT_IMAGE_EXTENSION))
+                    fputs("Warning: -testrendermulti specified with an extension other than ." DEFAULT_IMAGE_EXTENSION " but will be saved in that format anyway.\n", stderr);
                 if (!SAVE_DEFAULT_IMAGE_FORMAT(render, testRenderMulti))
                 if (!SAVE_DEFAULT_IMAGE_FORMAT(render, testRenderMulti))
                     fputs("Failed to write test render file.\n", stderr);
                     fputs("Failed to write test render file.\n", stderr);
             }
             }
             if (testRender) {
             if (testRender) {
                 Bitmap<float, 1> render(testWidth, testHeight);
                 Bitmap<float, 1> render(testWidth, testHeight);
                 renderSDF(render, msdf, avgScale*range);
                 renderSDF(render, msdf, avgScale*range);
+                if (!cmpExtension(testRender, "." DEFAULT_IMAGE_EXTENSION))
+                    fputs("Warning: -testrender specified with an extension other than ." DEFAULT_IMAGE_EXTENSION " but will be saved in that format anyway.\n", stderr);
                 if (!SAVE_DEFAULT_IMAGE_FORMAT(render, testRender))
                 if (!SAVE_DEFAULT_IMAGE_FORMAT(render, testRender))
                     fputs("Failed to write test render file.\n", stderr);
                     fputs("Failed to write test render file.\n", stderr);
             }
             }
@@ -1392,12 +1382,16 @@ int main(int argc, const char *const *argv) {
             if (testRenderMulti) {
             if (testRenderMulti) {
                 Bitmap<float, 4> render(testWidthM, testHeightM);
                 Bitmap<float, 4> render(testWidthM, testHeightM);
                 renderSDF(render, mtsdf, avgScale*range);
                 renderSDF(render, mtsdf, avgScale*range);
+                if (!cmpExtension(testRenderMulti, "." DEFAULT_IMAGE_EXTENSION))
+                    fputs("Warning: -testrendermulti specified with an extension other than ." DEFAULT_IMAGE_EXTENSION " but will be saved in that format anyway.\n", stderr);
                 if (!SAVE_DEFAULT_IMAGE_FORMAT(render, testRenderMulti))
                 if (!SAVE_DEFAULT_IMAGE_FORMAT(render, testRenderMulti))
                     fputs("Failed to write test render file.\n", stderr);
                     fputs("Failed to write test render file.\n", stderr);
             }
             }
             if (testRender) {
             if (testRender) {
                 Bitmap<float, 1> render(testWidth, testHeight);
                 Bitmap<float, 1> render(testWidth, testHeight);
                 renderSDF(render, mtsdf, avgScale*range);
                 renderSDF(render, mtsdf, avgScale*range);
+                if (!cmpExtension(testRender, "." DEFAULT_IMAGE_EXTENSION))
+                    fputs("Warning: -testrender specified with an extension other than ." DEFAULT_IMAGE_EXTENSION " but will be saved in that format anyway.\n", stderr);
                 if (!SAVE_DEFAULT_IMAGE_FORMAT(render, testRender))
                 if (!SAVE_DEFAULT_IMAGE_FORMAT(render, testRender))
                     fputs("Failed to write test render file.\n", stderr);
                     fputs("Failed to write test render file.\n", stderr);
             }
             }