When saving bitmap to disk, solid paths show artifacts
Asked Answered
O

3

5

[Edit: I've made a minimal project to try to narrow down what's going on. The code at the bottom still generates the same artifacts when saved]

I have an app that draws simple 2D geometry using Paths. The shapes are all solid colors, sometimes with alpha < 255, and may be decorated with lines. In the View that draws the geometry, there has never been an issue with how things get drawn. However, when I use the same code to draw to a Bitmap, and then save it as either a JPEG (with 100 quality) or PNG, there is always the same artifacting in the solid-colored areas of the output files. It's a sort of mottling that is usually associated with JPEG compression.

Screenshot of View: Screenshot of Activity

Saved image: Saved image file

Zoom in on artifacts: Zoom in on artifacts

I have tried the following

  • Saving to either PNG and JPEG
  • Turning dithering and antialiasing on and off
  • Increasing the DPI of the Bitmap, and also allowed the Bitmap to use its default API
  • Applying the matrix I use as a camera to the geometric representation, instead of applying it to the Canvas for the bitmap
  • Turning HW Acceleration on and off app-wide
  • Using a 3rd party library to save the Bitmap to a .bmp file

All yield the same artifacts, neither making it worse nor better.

public class MainActivity extends AppCompatActivity {
Context context;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    this.context = getApplicationContext();
}

// button OnClick listener
public void saveImage(View view) {
    new saveBitmapToDisk().execute(false);
}

public Bitmap getBitmap() {
    final int bitmapHeight = 600, bitmapWidth = 600;
    Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
    Canvas bitmapCanvas = new Canvas(bitmap);

    float[] triangle = new float[6];
    triangle[0] = bitmapWidth / 2;
    triangle[1] = 0;
    triangle[2] = 0;
    triangle[3] = bitmapHeight / 2;
    triangle[4] = bitmapWidth / 2;
    triangle[5] = bitmapHeight / 2;

    Path solidPath = new Path();
    Paint solidPaint = new Paint();
    solidPaint.setStyle(Paint.Style.FILL);

    solidPath.moveTo(triangle[0], triangle[1]);

    for(int i = 2; i < triangle.length; i += 2)
        solidPath.lineTo(triangle[i], triangle[i+1]);

    solidPath.close();

    solidPaint.setColor(Color.GREEN);
    bitmapCanvas.drawPath(solidPath, solidPaint);
    return bitmap;
}

private class saveBitmapToDisk extends AsyncTask<Boolean, Integer, Uri> {
    Boolean toShare;

    @Override
    protected Uri doInBackground(Boolean... shareFile) {
        this.toShare = shareFile[0];
        final String appName = context.getResources().getString(R.string.app_name);
        final String IMAGE_SAVE_DIRECTORY = String.format("/%s/", appName);
        final String fullPath = Environment.getExternalStorageDirectory().getAbsolutePath() + IMAGE_SAVE_DIRECTORY;
        File dir, file;

        try {
            dir = new File(fullPath);
            if (!dir.exists())
                dir.mkdirs();

            OutputStream fOut;

            file = new File(fullPath, String.format("%s.png", appName));

            for (int suffix = 0; file.exists(); suffix++)
                file = new File(fullPath, String.format("%s%03d.png", appName, suffix));

            file.createNewFile();
            fOut = new FileOutputStream(file);

            Bitmap saveBitmap = getBitmap();
            saveBitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
            fOut.flush();
            fOut.close();
            MediaStore.Images.Media.insertImage(context.getContentResolver(), file.getAbsolutePath(), file.getName(), file.getName());

        } catch (OutOfMemoryError e) {
            Log.e("MainActivity", "Out of Memory saving bitmap; bitmap is too large");
            return null;
        } catch (Exception e) {
            Log.e("MainActivity", e.getMessage());
            return null;
        }

        return Uri.fromFile(file);
    }

    @Override
    protected void onPostExecute(Uri uri) {
        super.onPostExecute(uri);
        Toast.makeText(context, "Image saved", Toast.LENGTH_SHORT).show();
    }
}
}
Onus answered 21/5, 2016 at 3:30 Comment(0)
P
3
  1. I tested your program with PNG and the file has no artifacts
  2. These artifacts are a result of JPEG compression

Edit: The line

MediaStore.Images.Media.insertImage(context.getContentResolver(), file.getAbsolutePath(), file.getName(), file.getName());

was causing the conversion to jpeg.

The proper way to save the image is

ContentValues values = new ContentValues();
values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis());
values.put(Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath());
context.getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values);

Here is my simplified test program that sends the generated file directly

public class Test2Activity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    new saveBitmapToDisk().execute();
  }

  public Bitmap getBitmap() {
    final int bitmapHeight = 600, bitmapWidth = 600;
    Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
    Canvas bitmapCanvas = new Canvas(bitmap);

    Paint solidPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    solidPaint.setStyle(Paint.Style.FILL);
    solidPaint.setColor(Color.RED);
    bitmapCanvas.drawCircle(300, 300, 200, solidPaint);

    return bitmap;
  }

  private class saveBitmapToDisk extends AsyncTask<Void, Void, Uri> {
    Boolean toShare;

    @Override
    protected Uri doInBackground(Void... shareFile) {
      Context context = Test2Activity.this;
      try {
        File file = new File(context.getExternalFilesDir(null), "test.png");
        FileOutputStream fOut = new FileOutputStream(file);

        Bitmap saveBitmap = getBitmap();
        saveBitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
        fOut.flush();
        fOut.close();
        return Uri.fromFile(file);
      } catch (OutOfMemoryError e) {
        Log.e("MainActivity", "Out of Memory saving bitmap; bitmap is too large");
        return null;
      } catch (Exception e) {
        Log.e("MainActivity", e.getMessage());
        return null;
      }

    }

    @Override
    protected void onPostExecute(Uri uri) {
      Context context = Test2Activity.this;
      Toast.makeText(context, "Image saved", Toast.LENGTH_SHORT).show();

      final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
      intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
      intent.putExtra(Intent.EXTRA_STREAM, uri);
      intent.setType("image/png");
      Test2Activity.this.startActivity(intent);
    }
  }
}
Pentheus answered 2/6, 2016 at 16:0 Comment(5)
I just double checked on a fresh device, and I'm definitely still experiencing artifacts, and it's definitely not an old file. Nor is it a thumbnail; going to the "info" section indicates the correct dimensions. Is it possible this is something in the build environment, or some such?Onus
@Project I added to my reply a simplified activity you should test. It creates a circle image on transparent background, and opens the share intent to send this file, Run it on your device, and send the generated image by email to yourself. If the image has a transparent background, it was encoded as PNG, if opaque it was encoded as jpeg.Pentheus
Tested your example, it has a transparent background and no artifacts. Curious if it might be the directory I'm saving to or somesuch... will poke around at once coffee has happened.Onus
So it turns out it was the line MediaStore.Images.Media.insertImage(...) that was wrong. I changed it to the accepted answer for this question and it's finally working. Looks like the code I was using has something to do with the camera. Thanks for helping me hammer away at this; If you'd want to update your answer, I can mark it as accepted.Onus
@Project Done. Learned something new myselfPentheus
L
1

Artifacts like this are natural and unavoidable consequence of JPEG compression.

They should not crop up in PNG compression. If you are getting such artifacts when you create a PNG file, I'd wager that you are not creating a PNG stream at all, but rather a JPEG stream in a file with a PNG extension. No decent decoder relies on the file extension.

Lubumbashi answered 22/5, 2016 at 3:9 Comment(2)
This occurs with both saveBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fOut); and saveBitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);Onus
To be clearer; I had this as CompressFormat.PNG for a long time, wherein I was getting images like this. The only reason it is CompressFormat.JPEG here is that I was experimenting with changing things about the code to see if I got different results.Onus
P
0

I noticed two things in your code:

1) The filename you save to is String.format("%s.jpg", appName) or String.format("%s%03d.png", appName, suffix) independent of the actual encoding.

2) The bitmap you save has its density determined by prefs.saveImageDensity().get() so it may not be the same as the actual density of the bitmap you see on the screen.

Maybe you confused yourself with 1) or perhaps 2) causes the compression-artefacts you're seeing?

Pettis answered 31/5, 2016 at 7:24 Comment(5)
1) Is a function of my editing the post improperly 2) I should have been clearer; I've tried this in all sorts of densities, including the native density. I just went back and made sure that the file extension was PNG regardless, and commented out setting the DPI. Problem still occurring.Onus
The documentation even mentions that saving to PNG is a lossless operation, but maybe there is a bug in there. You should try writing a separate test program that loads and saves a bitmap and see if the problem occurs even there. That will show whether saving to PNG is really lossless.Pettis
I substituted using Android API for using this library to save as a BITMAP, and I'm getting the same artifacts. So, it seems to not have anything to do with the compression, but with the actual drawing to a bitmap. Which strikes me as pretty weird.Onus
Just as an update: I've been ruling out that I'm doing anything weird with my draw methods. Instead of calling canvasView.cluster.drawCached(bitmapCanvas); I've tried just drawing circles, squares, and paths directly to the bitmap. Still getting these weird artifacts. So not sure what's even left to check. Looking like I'll have to create a test project to figure this out.Onus
I've edited the question with an activity I made to test this in a fresh project. Artifacts still occurring when saving from this activity.Onus

© 2022 - 2024 — McMap. All rights reserved.