Как получить растровую информацию, а затем декодировать растровое изображение из интернет-inputStream?
фон
Предположим, у меня есть inputStream, который был создан из Интернета определенного файла изображения.
Я хочу получить информацию о файле изображения и только потом расшифровать его.
это полезно для нескольких целей, таких как понижающая дискретизация, а также предварительный просмотр информации перед показом изображения.
эта проблема
я попытался пометить и сбросить inputStream, обернув inputStream с BufferedInputStream, но это не сработало:
inputStream=new BufferedInputStream(inputStream);
inputStream.mark(Integer.MAX_VALUE);
final BitmapFactory.Options options=new BitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeStream(inputStream,null,options);
//this works fine. i get the options filled just right.
inputStream.reset();
final Bitmap bitmap=BitmapFactory.decodeStream(inputStream,null,options);
//this returns null
для получения inputStream из URL, я использую:
public static InputStream getInputStreamFromInternet(final String urlString)
{
try
{
final URL url=new URL(urlString);
final HttpURLConnection urlConnection=(HttpURLConnection)url.openConnection();
final InputStream in=urlConnection.getInputStream();
return in;
}
catch(final Exception e)
{
e.printStackTrace();
}
return null;
}
вопрос
Как я могу сделать код обрабатывать маркировку сброса?
он отлично работает с ресурсами (на самом деле мне даже не пришлось создавать новый BufferedInputStream для этого, чтобы работать), но не с inputStream из Интернета...
РЕДАКТИРОВАТЬ:
кажется, мой код просто отлично, вроде...
на некоторых веб-сайтах (таких как этот и этот) не удается декодировать файл изображения даже после сброса.
если вы декодируете растровое изображение (и используете inSampleSize), он может прекрасно его декодировать (это занимает много времени).
теперь вопрос в том, почему это происходит, и как я могу это исправить.
4 ответа
Я считаю, что проблема заключается в том, что вызов mark () с большим значением перезаписывается вызовом mark(1024). Как описано в документации:
До KITKAT, если is.markSupported() возвращает true, вызывается is.mark(1024). Что касается KITKAT, это уже не так.
Это может привести к ошибке reset (), если выполняется чтение больше, чем это значение.
(Вот решение той же проблемы, но при чтении с диска. Сначала я не осознавал, что ваш вопрос был задан именно из сетевого потока.)
Проблема с mark & reset в общем случае заключается в том, что BitmapFactory.decodeStream()
иногда сбрасывает ваши оценки. Таким образом, сброс для того, чтобы сделать фактическое чтение, нарушен.
Но есть и вторая проблема с BufferedInputStream: это может привести к тому, что все изображение будет помещено в буфер вместе с тем местом, где вы действительно его читаете. В зависимости от вашего варианта использования, это действительно может снизить производительность. (Много распределения означает много GC)
Здесь есть действительно отличное решение: /questions/43381717/vvod-java-fajla-s-vozmozhnostyu-peremotki-sbrosa/43381732#43381732
Я немного изменил его для этого конкретного случая использования, чтобы решить проблему с меткой и сбросом:
public class MarkableFileInputStream extends FilterInputStream
{
private static final String TAG = MarkableFileInputStream.class.getSimpleName();
private FileChannel m_fileChannel;
private long m_mark = -1;
public MarkableFileInputStream( FileInputStream fis )
{
super( fis );
m_fileChannel = fis.getChannel();
}
@Override
public boolean markSupported()
{
return true;
}
@Override
public synchronized void mark( int readlimit )
{
try
{
m_mark = m_fileChannel.position();
}
catch( IOException ex )
{
Log.d( TAG, "Mark failed" );
m_mark = -1;
}
}
@Override
public synchronized void reset() throws IOException
{
// Reset to beginning if mark has not been called or was reset
// This is a little bit of custom functionality to solve problems
// specific to Android's Bitmap decoding, and is slightly non-standard behavior
if( m_mark == -1 )
{
m_fileChannel.position( 0 );
}
else
{
m_fileChannel.position( m_mark );
m_mark = -1;
}
}
}
Это не выделит никакой дополнительной памяти во время чтения и может быть сброшено, даже если метки были очищены.
Вот простой метод, который всегда работает для меня:)
private Bitmap downloadBitmap(String url) {
// initilize the default HTTP client object
final DefaultHttpClient client = new DefaultHttpClient();
//forming a HttoGet request
final HttpGet getRequest = new HttpGet(url);
try {
HttpResponse response = client.execute(getRequest);
//check 200 OK for success
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
Log.w("ImageDownloader", "Error " + statusCode +
" while retrieving bitmap from " + url);
return null;
}
final HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream inputStream = null;
try {
// getting contents from the stream
inputStream = entity.getContent();
// decoding stream data back into image Bitmap that android understands
image = BitmapFactory.decodeStream(inputStream);
} finally {
if (inputStream != null) {
inputStream.close();
}
entity.consumeContent();
}
}
} catch (Exception e) {
// You Could provide a more explicit error message for IOException
getRequest.abort();
Log.e("ImageDownloader", "Something went wrong while" +
" retrieving bitmap from " + url + e.toString());
}
return image;
}
Можно ли пометить / сбросить поток, зависит от реализации потока. это необязательные операции, которые обычно не поддерживаются. Вы можете прочитать поток в буфер и затем прочитать его 2 раза, или просто установить сетевое соединение 2 раза.
проще всего, наверное, написать в ByteArrayOutputStream
,
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int count;
byte[] b = new byte[...];
while ((count = input.read(b) != -1) [
baos.write(b, 0, count);
}
Теперь либо использовать результат baos.toByteArray()
напрямую или создать ByteArrayInputStream
и использовать это неоднократно, вызывая reset()
после употребления его каждый раз.
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
это может звучать глупо, но в этом нет магии. Вы либо буферизуете данные в памяти, либо читаете их 2 раза из источника. если бы поток поддерживал mark / reset, он должен был сделать то же самое в своей реализации.