3

I have an image on sdcard and need to show on the image view

The problem is that after it has been decoded, it seems the quality is deteriorated. Are there any ways to keep the quality and at the same time preserve the memory?

Or, if I used an larger image, are there any ways to preserve the memory (avoid too large bitmap to load) with scaling? (I need to keep the size of the original image)

Thanks for helping.

public Bitmap decodeFile(String pubKey, int bookPageID, int type)
        throws IOException {
    Bitmap b = null;
    File f = null;
    String uri = null;
    FileInputStream fis = null;

    Log.d(TAG,"pageID to read: " + bookPageID);

    IRIssue issue = Broker.model.issueDataStore.getIRIssue(pubKey);

    String imageFolder = IRConstant.issueFolder(issue.year, issue.month, issue.day, issue.pubKey);

    // pageID - 1 since the page is an array (start at 0) , but page ID start at 1
    if (type == 2){
        uri = imageFolder + issue.vol[0].pages[bookPageID - 1].graphicUri;
    }else {
        uri = imageFolder + issue.vol[0].pages[bookPageID - 1].textUri;
    }

    f = new File(uri);

    Log.d(TAG,"is file: " + uri + " exist?" + f.exists());

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPurgeable = true;
    options.inInputShareable = true;
    options.inJustDecodeBounds = false;
    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
    fis = new FileInputStream(f);
    b = BitmapFactory.decodeStream(fis, null, options);
    fis.close();

    return b;
}
jonhopkins
  • 3,844
  • 3
  • 27
  • 39
user782104
  • 13,233
  • 55
  • 172
  • 312
  • try this options.inScaled = false; options.inDither = false; – Kaloyan Roussev Jan 02 '14 at 06:34
  • still an error " bitmap too large to uploaded into a texture" – user782104 Jan 02 '14 at 09:01
  • I found the problem is caused by the bitmap size > 2048 * 2048 , how to fix it , I tried scaling but the quality is bad? thanks – user782104 Jan 02 '14 at 10:30
  • How are you scaling the image ? options.inSampleSize of bitmap scales the image and the quality is not affected . – Lord Nick Jan 08 '14 at 06:13
  • My way will be 1. Get the image bounds by decoding bitmap with injustdecodebounds = true 2. than find out insamplesize by the method i provide below 3. than get the scaled bitmap using BitmapFactory.decodeStream(fis, null, options); with appropriate options. – varun bhardwaj Jan 08 '14 at 09:02

6 Answers6

3

The following code uses several concepts from Displaying Bitmaps Efficiently
First off the bitmap reading is done in a background thread, I'm using mark / reset on inputStream (wrapped with BufferedInputstream) to not read more than necessary from stream when we try to find out the size of the image to use when calculating scale factor. The example code below subsamples the image to match a size of 320x240 pixles. In a non example code one could have simple callback interface send the bitmap from onPostExecute to implementing class (callback interface implementer). Or provide the view as a member tot the AsyncTask directly and set the bitmap in onPostExecute.

Call the code with (example downloaded image on my device):

BitmapTask task = new BitmapTask(getContentResolver());
task.execute(Uri.parse("file:///storage/emulated/0/Download/download.jpg"));

The classes in question

private static class BitmapTask extends AsyncTask<Uri, Void, Bitmap> {

    // prevent mem leaks
    private WeakReference<ContentResolver> mWeakContentResolver;

    public BitmapTask(ContentResolver resolver) {
        mWeakContentResolver = new WeakReference<ContentResolver>(resolver);
    }

    @Override
    protected Bitmap doInBackground(Uri... params) {
        Bitmap bitmap = null;
        ContentResolver resolver = mWeakContentResolver.get();
        if (resolver != null) {
            BufferedInputStream stream = null;
            try {
                stream = new BufferedInputStream(
                        resolver.openInputStream(params[0]));
                stream.mark(1 * 1024);
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inJustDecodeBounds = true;
                // Find out size of image
                BitmapFactory.decodeStream(stream, null, options);
                try {
                    stream.reset();
                } catch (IOException e) {
                    Log.d(TAG, "reset failed");
                }
                int imageHeight = options.outHeight;
                int imageWidth = options.outWidth;
                String imageType = options.outMimeType;
                Log.d(TAG, "w, h, mime " + imageWidth + " , " + imageHeight
                        + " , " + imageType);
                options.inJustDecodeBounds = false;
                // Calculate down scale factor
                options.inSampleSize = calculateInSampleSize(options, 320,
                        240);
                return BitmapFactory.decodeStream(stream, null, options);
            } catch (FileNotFoundException e) {
                bitmap = null;
            } finally {
                IOUtils.closeStreamSilently(stream);
            }
        }
        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap result) {
        Log.d(TAG,
                "bitmap result: "
                        + ((result != null) ? "" + result.getByteCount()
                                : "0"));
        result.recycle();
    }
}

public static int calculateInSampleSize(BitmapFactory.Options options,
        int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and
        // keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

Edit: For large inputstreams there might be a problem with the mark/reset technique, SkImageDecoder::Factory returned null can be seen in logs sometimes, resulting in null bitmap, se other SO question on the matter: SkImageDecoder::Factory returned null. It can be fixed by reiniting the stream variable again stream = new resolver.openInputStream(params[0])); before return in doInBackground

Edit 2: If you have to preserve the image size but wan't to limit memory usage you could use the options.inPreferredConfig = Bitmap.Config.RGB_565; that halves the memory per pixel, but bare in mind that the images might not have great quality anymore (experiment!).

Community
  • 1
  • 1
Magnus
  • 1,483
  • 11
  • 14
1

I use a custom BitmapHandler class to solve this problem:

public class BitmapHandler {
    private static int IMAGE_MAX_SIZE = 540;  //This can be set to whatever you see fit
    private static String TAG = "BitmapHandler.java";

    public BitmapHandler(Context ctx){
            WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
            Display display = wm.getDefaultDisplay();
            Point size = new Point();
            display.getSize(size);
            int width = size.x;
            int height = size.y;
            Log.v(TAG, "Screen width: " + width + " height: " + height);
            IMAGE_MAX_SIZE = (Math.min(width, height))*4; //Try playing with this multiplier number to get different degrees of scaling
    }

    public Bitmap decodeFileAsPath(String uri) {
            // Create a file out of the uri
            File f = null;
            Log.v(TAG, "Incoming uri: " + uri);
            f = new File(uri);

            if (f.equals(null)){
                    Log.v(TAG, "File is null!");
            }
            return decodeFile(f);
    }

    private Bitmap decodeFile(File f) {
            Bitmap b = null;
            try {
                    // Decode image size
                    BitmapFactory.Options o = new BitmapFactory.Options();
                    o.inJustDecodeBounds = true;
                    o.inScaled = false;

                    FileInputStream fis = new FileInputStream(f);
                    BitmapFactory.decodeStream(fis, null, o);
                    fis.close();

                    int scale = 1;
                    Log.v(TAG, "Decode Image height: " + o.outHeight + " and width: " + o.outWidth);

                    if (o.outHeight > IMAGE_MAX_SIZE || o.outWidth > IMAGE_MAX_SIZE) {
                            scale = (int) Math.pow(
                                            2,
                                            (int) Math.round(Math.log(IMAGE_MAX_SIZE
                                                            / (double) Math.max(o.outHeight, o.outWidth))
                                                            / Math.log(0.5)));
                    }
                    Log.v(TAG, "Final scale: " + scale);
                    // Decode with inSampleSize
                    BitmapFactory.Options o2 = new BitmapFactory.Options();
                    o2.inScaled = false;
                    o2.inSampleSize = scale;
                    fis = new FileInputStream(f);
                    b = BitmapFactory.decodeStream(fis, null, o2);
                    fis.close();
            } catch (IOException e) {
                    Log.v(TAG, e.getMessage());
            }
            return b;
    }
}

This dynamically scales your image whilst trying to prevent an OutOfMemoryException

SalGad
  • 2,991
  • 2
  • 18
  • 25
  • I am working on an magazine image and allow the user to set the max zoom level e.g. 150%, 200%..etc... So the image size should be the same for every device . – user782104 Jan 02 '14 at 06:49
  • 1
    You can set that in the XML by setting the width and height attributes, and using android:scaleType:"fitCenter". This way the image will always be one size on the app – SalGad Jan 02 '14 at 06:55
  • @SalGad `decodeFileAsPath` is not needed, since the `if (f.equals(null))` won't' ever happen, equals compare this file object to other one, if `f` would've been null you had an NPE here, thus rendering the method unnecessary. You could wrap the call to `decodeFile` with, `decodeFile(new File(uri));` and make `decodeFile` public instead (and remove `decodeFileAsPath`). Just a friendly thought. – Magnus Jan 08 '14 at 20:57
1

BitmapFactory has an inSampleSize property which was designed to solve this problem. Refer to docs: http://developer.android.com/reference/android/graphics/BitmapFactory.Options.html#inSampleSize

This article writes about handling bitmaps efficiently: http://developer.android.com/training/displaying-bitmaps/index.html

Aron Lorincz
  • 1,677
  • 3
  • 19
  • 29
1

While decoding add options.injustdecodeBounds = true because that shows that you want only bounds not the whole bitmap. This will avoid the memory errors because you will only load the image size you actually require.

Second thing is to scale that bitmap according to your need and to do that without distortion you have to scale it in a way that will maintain the aspect ratio. While clicking picture either you set a fixed ration in which image is clicked and than scale that image in that ratio only. If it is not in your hand than you can use the following method to get the insample size and than will decode the image to a particular size without distortion.

private int calculateSampleSize(int width, int height, int targetWidth, int targetHeight) {
float bitmapWidth = width;
float bitmapHeight = height;

int bitmapResolution = (int) (bitmapWidth * bitmapHeight);
int targetResolution = targetWidth * targetHeight;

int sampleSize = 1;

if (targetResolution == 0) {
    return sampleSize;
}

for (int i = 1; (bitmapResolution / i) > targetResolution; i *= 2) {
    sampleSize = i;
}

return sampleSize;

}

Do provide any feedback if you find any improvements.

varun bhardwaj
  • 1,522
  • 13
  • 24
1

A fast approach that is also highly configurable is to use a WebView instead of an ImageView:

WebView mWebView = (WebView) findViewById(R.id.webview);
mWebView.getSettings().setAllowFileAccess(true);
mWebView.getSettings().setBuiltInZoomControls(true);
String base = Environment.getExternalStorageDirectory().getAbsolutePath().toString();
String imagePath = "file://" + base + "/myImage.png";//replace with the name of the image you are accessing
String html = "<html><head></head><body><img src=\"" + imagePath + "\"></body></html>";
mWebView.loadDataWithBaseURL("", html, "text/html","utf-8", "");
Phil
  • 35,852
  • 23
  • 123
  • 164
  • What do you solve by using webview? 90% of the code above is web specific, 10 % image specific. – Magnus Jan 08 '14 at 16:05
  • @Magnus, this first simplifies the code, since decoding the bitmap is handled by the `WebView`. Additionally, the OP can edit the `CSS` to ensure the correct fit. – Phil Jan 08 '14 at 16:18
  • setImageUri and setScaleType in ImageView does that too – Magnus Jan 08 '14 at 16:27
  • 3
    @Magnus, sounds like you should post an answer. – Phil Jan 08 '14 at 16:28
0

GridViewActivity.java

public class GridViewActivity extends Activity implements OnItemClickListener {
    private String[] filepathstring;
    private File[] listfile;
    GridView grid_sdcard;
    File file;
    ImageView image;
    GridViewAdapter adapter;
    int select;
    int sele;



    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.gridview_activity);

        image=(ImageView)convertView.findViewById(R.id.image_show);
        grid_sdcard=(GridView)findViewById(R.id.grid_sdcard);


        if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
        {
        Toast.makeText(this,"Error! No SDCARD Found!", Toast.LENGTH_LONG).show();   
        }
        else
        {
            file=new File(Environment.getExternalStorageDirectory() + File.separator +"eMENU Images/");
            file.mkdirs();
            Toast.makeText(GridViewActivity.this,"Past Image Here:", Toast.LENGTH_LONG).show();
        }
        if(file.isDirectory())
        {
            listfile=file.listFiles();
            for(int i=0;i<listfile.length;i++)
            {
                filepathstring[i]=listfile[i].getAbsolutePath();
            }
        }
        adapter=new GridViewAdapter(GridViewActivity.this,filepathstring);
        grid_sdcard.setAdapter(adapter);
        grid_sdcard.setOnItemClickListener(this);
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id1) {
        final String image=filepathstring[position];
        Bitmap bitmap=BitmapFactory.decodeFile(filepathlist[position]);
        imageshow.setImageBitmap(bitmap);
    }
}

GridViewAdapter.java

public class GridViewAdapter extends BaseAdapter {
    String[] filepathlist;
    Context context;


    public GridViewAdapter(Context con, String[] filepathstring) {
        context=con;
        filepathlist=filepathstring;
    }

    @Override
    public int getCount() {

        return filepathlist.length;
    }

    @Override
    public Object getItem(int position) {
        // TODO Auto-generated method stub
        return position;
    }

    @Override
    public long getItemId(int position) {
        // TODO Auto-generated method stub
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        if(convertView==null)
        {
            LayoutInflater inflater=(LayoutInflater)convertView.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView=inflater.inflate(R.layout.griadview_adapter,null);
        }
        ImageView imageshow=(ImageView)convertView.findViewById(R.id.image_show);
        Bitmap bitmap=BitmapFactory.decodeFile(filepathlist[position]);
        imageshow.setImageBitmap(bitmap);
        return convertView;
    }
}
Dharmendra
  • 571
  • 5
  • 12
  • @user782104 why is this marked as the correct answer? Nowhere in the OPs question there is a statement about a Grid / GridView / GridAdapter, so how can this be correct? – Magnus Jan 14 '14 at 21:30