如何在 Android 上旋转 JPEG 文件而不损失质量和增加文件大小?

Posted

技术标签:

【中文标题】如何在 Android 上旋转 JPEG 文件而不损失质量和增加文件大小?【英文标题】:How to rotate a JPEG file on Android without losing quality and gaining file size? 【发布时间】:2014-03-01 13:48:45 【问题描述】:

背景

我需要旋转相机拍摄的图像,以便它们始终具有正常方向。

为此,我使用下一个代码(使用this post 获取图像方向)

//<= get the angle of the image , and decode the image from the file
final Matrix matrix = new Matrix();
//<= prepare the matrix based on the EXIF data (based on https://gist.github.com/9re/1990019 )
final Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),matrix,false);
bitmap.recycle();
fileOutputStream = new FileOutputStream(tempFilePath);
rotatedBitmap.compress(CompressFormat.JPEG, 100, fileOutputStream);
rotatedBitmap.recycle();

这里的压缩率(又名“质量”参数)为 100。

问题

代码运行良好,但结果比原来的要大,要大得多。

原始文件约为 600-700 KB,而生成的文件约为 3MB ...

即使输入文件和输出文件的格式相同 (JPEG)。

相机设置为“超精细”质量。不知道是什么意思,但我认为它与压缩比有关。

我尝试过的

我尝试将“过滤器”参数设置为 false 或 true。两者都导致大文件。

即使没有旋转本身(只是解码和编码),我也会得到更大的文件大小......

仅当我将压缩比设置为 85 左右时,我才能获得相似的文件大小,但我想知道与原始文件相比质量如何受到影响。

问题

为什么会发生?

有没有办法获得与输入文件完全相同的大小和质量?

使用与原始文件相同的压缩率会实现吗?甚至可以获得原始文件的压缩率吗?

100% 压缩率是什么意思?


编辑:我发现 this link 谈论 JPEG 文件的旋转而不会丢失质量和文件大小,但在 android 上有解决方案吗?

Here's another link 说这是可能的,但我找不到任何允许旋转 jpeg 文件而不损失其质量的库

【问题讨论】:

设置方向标签是您最好的选择。我见过的唯一忽略方向标记的图像查看器是旧版本的“Windows Image Viewer”(内置应用程序)。我的 C 图像库允许无损旋转,但它不是免费的,需要您调用本机代码。 旧版本的“Windows 图像查看器”?所以 Windows 8 是旧的...... :) 无论如何,我希望避免假设人们如何显示图像。 我正在使用 PNG 图像,它的工作正常......对于 JPEG,请检查上面的代码。 @androidDev PNG 可以正常工作,因为它们是无损的。我询问了 JPEG 文件,这些文件是有损的。 嗨。你解决了吗?我尝试了 LLJTran,但它在读取图像文件时抛出异常。 【参考方案1】:

我尝试了两种方法,但我发现这些方法在我的情况下花费的时间太长。我仍然分享我用过的东西。

方法 1:适用于 Android 的 LLJTran

从这里获取 LLJTran: https://github.com/bkhall/AndroidMediaUtil

代码:

public static boolean rotateJpegFileBaseOnExifWithLLJTran(File imageFile, File outFile)
    try 

        int operation = 0;
        int degree = getExifRotateDegree(imageFile.getAbsolutePath());
        //int degree = 90;
        switch(degree)
            case 90:operation = LLJTran.ROT_90;break;
            case 180:operation = LLJTran.ROT_180;break;
            case 270:operation = LLJTran.ROT_270;break;
           
        if (operation == 0)
            Log.d(TAG, "Image orientation is already correct");
            return false;
        

        OutputStream output = null;
        LLJTran llj = null;
        try    
            // Transform image
            llj = new LLJTran(imageFile);
            llj.read(LLJTran.READ_ALL, false); //don't know why setting second param to true will throw exception...
            llj.transform(operation, LLJTran.OPT_DEFAULTS
                    | LLJTran.OPT_XFORM_ORIENTATION);

            // write out file
            output = new BufferedOutputStream(new FileOutputStream(outFile));
            llj.save(output, LLJTran.OPT_WRITE_ALL);
            return true;
         catch(Exception e)
            e.printStackTrace();
            return false;
        finally 
            if(output != null)output.close();
            if(llj != null)llj.freeMemory();
        
     catch (Exception e) 
        // Unable to rotate image based on EXIF data
        e.printStackTrace();
        return false;
    


public static int getExifRotateDegree(String imagePath)
    try 
        ExifInterface exif;
        exif = new ExifInterface(imagePath);
        String orientstring = exif.getAttribute(ExifInterface.TAG_ORIENTATION);
        int orientation = orientstring != null ? Integer.parseInt(orientstring) : ExifInterface.ORIENTATION_NORMAL;
        if(orientation == ExifInterface.ORIENTATION_ROTATE_90) 
            return 90;
        if(orientation == ExifInterface.ORIENTATION_ROTATE_180) 
            return 180;
        if(orientation == ExifInterface.ORIENTATION_ROTATE_270) 
            return 270;
     catch (IOException e) 
        e.printStackTrace();
           
    return 0;

方法二:使用 libjepg-turbo 的 jpegtran 可执行文件

1 按照此处描述的步骤操作: https://***.com/a/12296343/1099884

除了你不需要ndk-build 上的obj/local/armeabi/libjpeg.a,因为我只想要 jpegtran 可执行文件,但不想用libjepg.a 与 JNI 混淆。

2 将 jpegtran 可执行文件放在资产文件夹中。 代码:

public static boolean rotateJpegFileBaseOnExifWithJpegTran(Context context, File imageFile, File outFile)
    try 

        int operation = 0;
        int degree = getExifRotateDegree(imageFile.getAbsolutePath());
        //int degree = 90;
        String exe = prepareJpegTranExe(context);
        //chmod ,otherwise  premission denied
        boolean ret = runCommand("chmod 777 "+exe); 
        if(ret == false)
            Log.d(TAG, "chmod jpegTran failed");
            return false;
                   
        //rotate the jpeg with jpegtran
        ret = runCommand(exe+
                " -rotate "+degree+" -outfile "+outFile.getAbsolutePath()+" "+imageFile.getAbsolutePath());         

        return ret;         
     catch (Exception e) 
        // Unable to rotate image based on EXIF data
        e.printStackTrace();
        return false;
    


public static String prepareJpegTranExe(Context context)
    File exeDir = context.getDir("JpegTran", 0);
    File exe = new File(exeDir, "jpegtran");
    if(!exe.exists())
        try 
            InputStream is = context.getAssets().open("jpegtran");
            FileOutputStream os = new FileOutputStream(exe);
            int bufferSize = 16384;
            byte[] buffer = new byte[bufferSize];
            int count;
            while ((count=is.read(buffer, 0, bufferSize))!=-1) 
                os.write(buffer, 0, count);
                           
            is.close();
            os.close();
         catch (IOException e) 
            e.printStackTrace();
        
    
    return exe.getAbsolutePath();


public static boolean runCommand(String cmd)
    try
        Process process = Runtime.getRuntime().exec(cmd);

        BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()));
        int read;
        char[] buffer = new char[4096];
        StringBuffer output = new StringBuffer();
        while ((read = reader.read(buffer)) > 0) 
            output.append(buffer, 0, read);
        
        reader.close();

        // Waits for the command to finish.
        process.waitFor();

        return true;            
     catch (Exception e) 
        e.printStackTrace();
        return false;
    

很遗憾,两者都需要很长时间。在我的三星 Galaxy S1 上是 16 秒!!!!但我发现这个应用程序 (https://play.google.com/store/apps/details?id=com.lunohod.jpegtool) 只需要 3-4 秒。一定有办法。

【讨论】:

您检查输出位图是否与原始位图的旋转版本相同? @android 开发者,我保存了原始 jpeg 文件并将文件(使用 jepgtran)旋转 90 度 4 次到 PC 并使用 diffimg 检查。它没有显示不同的像素。 酷。顺便说一句,您不必使用 PC,您可以将位图存储在设备上并比较像素……这个答案看起来很有希望。我希望我有机会去看看。现在,这是一个赞成票 (+1) 。 :) 您链接的应用不存在。您是否成功地优化了代码和?可以分享到 Github 吗?另外,你能解决所有类型的方向吗?其中一些需要翻转图像...【参考方案2】:

设置完 bestPreviewSize 后,您现在必须设置 bestPictureSize,每部手机都支持不同的图片尺寸,因此要获得最佳图片质量,您必须检查支持的图片尺寸,然后将最佳尺寸设置为相机参数。您必须在表面更改中设置这些参数以获取宽度和高度。 surfaceChanged 将在开始时被调用,因此您的新参数将被设置。

 @Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) 


 Camera.Parameters myParameters = camera.getParameters();

        myPicSize = getBestPictureSize(width, height);

        if (myBestSize != null && myPicSize != null) 

            myParameters.setPictureSize(myPicSize.width, myPicSize.height);

            myParameters.setJpegQuality(100);
            camera.setParameters(myParameters);

            Toast.makeText(getApplicationContext(),
                    "CHANGED:Best PICTURE SIZE:\n" +
                            String.valueOf(myPicSize.width) + " ::: " + String.valueOf(myPicSize.height),
                    Toast.LENGTH_LONG).show();
      

现在是 getBestPictureSize ..

 private Camera.Size getBestPictureSize(int width, int height)
    
    Camera.Size result=null;
    Camera.Parameters p = camera.getParameters();
    for (Camera.Size size : p.getSupportedPictureSizes()) 
        if (size.width>width || size.height>height) 
            if (result==null) 
                result=size;
             else 
                int resultArea=result.width*result.height;
                int newArea=size.width*size.height;

                if (newArea>resultArea) 
                    result=size;
                
            
        
    
    return result;

 

【讨论】:

问题是关于在不损失质量的情况下旋转 JPEG 文件,但这也很好。 检查旋转图像@androiddeveloper link 再一次,了解相机是一件好事,但问题是关于图像文件(可以通过相机创建)。 如果您将此旋转设置为相机,您的图像文件将在保存时自动旋转,您不必为图像数据创建位图并将其他一些旋转方法应用于位图..跨度> 用户可以随时使用第三方相机应用。我想处理相机应用程序生成的图像文件,它有自己的元数据 (EXIF) 关于如何捕获图像(定位等...)。【参考方案3】:

对于旋转,试试这个..

final Matrix matrix = new Matrix(); 
matrix.setRotate(90): 
final Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),matrix,false);

我正在使用 PNG 图像,它工作正常....对于 JPEG 图像,请检查上面的代码。

【讨论】:

上面的代码是什么?另外,我的问题是关于 JPEG,因为它们是有损的(与原始图像相比,压缩时会丢失质量),以及如何在不丢失原始 JPEG 文件质量的情况下制作文件。您的代码与我提出的任何问题都没有任何关系。 @416E64726577 这不是一个真正的答案。它再次对文件进行解码和编码,这是我专门写的我不想要的东西,因为它会由于重新压缩而失去质量【参考方案4】:

100% 质量率可能是比最初保存文件的设置更高的质量设置。这会导致尺寸更大但(几乎)相同的图像。

我不确定如何获得完全相同的尺寸,也许只需将质量设置为 85% 就可以了(快速和肮脏)。

但是,如果您只想将图片旋转 90°,则可以只编辑 JPEG 元数据,而无需触及像素数据本身。

不确定它是如何在 android 中完成的,但这就是它的工作原理。

【讨论】:

jpeg 不是无损的。它总是有损的。关于编辑元数据,这无济于事,因为我想让所有图像查看器正确查看图像,并且相机应用程序有时会向元数据添加方向,因此如果您显示图像,它看起来不像用户已经完成了。例如,如果您以 90 度 CW 拍摄图像并且用户打开图像,则在不考虑方向的情况下拍摄图像数据,因此它将被旋转并且元数据将仅保存文件的方向。一个好的图像查看器会考虑它并自行旋转。 顺便说一句,这是一个关于 jpeg 的神话页面:graphicssoft.about.com/od/formatsjpeg/a/jpegmythsfacts.htm 它包括你写的关于 100% 质量率的内容。

以上是关于如何在 Android 上旋转 JPEG 文件而不损失质量和增加文件大小?的主要内容,如果未能解决你的问题,请参考以下文章

在 onPictureTaken 之后旋转 JPEG 的字节数组

如何根据方向元数据旋转 JPEG 图像?

如何在上传时仅旋转 jpg 和 jpeg?

如何在 Android 中从 JPEG 创建动画 GIF(开发)

在客户端的 JavaScript 中访问 JPEG EXIF 旋转数据

在 ghostscript 中旋转 jpeg 图像