Hello World
Spiga

您能看出这个生成缩略图的方法有什么问题吗?

2009-11-24 00:32 by 老赵, 9209 visits

昨天又使用了某个多年以前写的,或者说是“收集”而来的方法。这个方法的作用是根据一幅图片(一般是幅大图)生成它的缩略图。这个方法用了许多年了,一直没有去怀疑过它的正确性,但是昨天忽然发现它一直以来都存在一个问题,虽然可能不是那么明显,而且也不会造成太大问题(否则早就发现了)——但是,这的确是个不妥的地方。这个问题在我看来也有一定借鉴意义,因此我打算把它展示出来。那么,您能否看出它究竟是错在什么地方了呢?

生成缩略图的规则很简单,概括地说有三点:

  1. 包含图片完整内容,以及长宽比不变。
  2. 尺寸尽可能大,但如果图片本身很小,也不做拉伸。
  3. 不超过指定的width * height的范围内。

这个规则其实就是最传统的缩略图生成方式,使用如Windows照片浏览器等软件打开图片后,一般来说默认都会如此调整图片尺寸。而我们如果需要写一段代码来实现这一点也并不困难,以下便是我用了许多年的方法:

/// <summary>
/// Creates a thumbnail from an existing image. Sets the biggest dimension of the
/// thumbnail to either desiredWidth or Height and scales the other dimension down
/// to preserve the aspect ratio
/// </summary>
/// <param name="imageStream">stream to create thumbnail for</param>
/// <param name="desiredWidth">maximum desired width of thumbnail</param>
/// <param name="desiredHeight">maximum desired height of thumbnail</param>
/// <returns>Bitmap thumbnail</returns>
public Bitmap CreateThumbnail(Bitmap originalBmp, int desiredWidth, int desiredHeight)
{
    // If the image is smaller than a thumbnail just return it
    if (originalBmp.Width <= desiredWidth && originalBmp.Height <= desiredHeight)
    {
        return originalBmp;
    }

    int newWidth, newHeight;

    // scale down the smaller dimension
    if ((decimal)desiredWidth / originalBmp.Width < (decimal)desiredHeight / originalBmp.Height)
    {
        decimal desiredRatio = (decimal)desiredWidth / originalBmp.Width;
        newWidth = desiredWidth;
        newHeight = (int)(originalBmp.Height * desiredRatio);
    }
    else
    {
        decimal desiredRatio = (decimal)desiredHeight / originalBmp.Height;
        newHeight = desiredHeight;
        newWidth = (int)(originalBmp.Width * desiredRatio);
    }

    // This code creates cleaner (though bigger) thumbnails and properly
    // and handles GIF files better by generating a white background for
    // transparent images (as opposed to black)
    // This is preferred to calling Bitmap.GetThumbnailImage()
    Bitmap bmpOut = new Bitmap(newWidth, newHeight);

    using (Graphics graphics = Graphics.FromImage(bmpOut))
    {
        graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
        graphics.FillRectangle(Brushes.White, 0, 0, newWidth, newHeight);
        graphics.DrawImage(originalBmp, 0, 0, newWidth, newHeight);
    }

    return bmpOut;
}

它的具体来源我已经记不得了,不过从英文注释上来看这应该是个老外写的代码,那么我们现在就来解释一番。首先,这个方法会先判断源图片的大小是否已经可以放入目标区域(desiredWidth * desiredHeight)中了,如果是,则直接返回源图片。如果不满足第一个判断,则说明宽和高之中至少有一个超出了目标尺寸,而我们要对源图片进行等比例缩放。

那么缩放的“比例”又是多少呢?自然是“宽”或“高”中缩放“程度大”的那个。因为如果按照缩放程度小的那条边的比例来改变图片尺寸,那么另一条边势必会超出范围。因此,我们接下来便是比较desiredWidth与originalBmp.Width之比,以及desiredHeight与originalBmp.Height之比孰大孰小。哪个小,则意味着我们要把它作为缩放依据,因为它对图片尺寸的限制要比另一条边来的严格。于是乎,再第二个条件判断的任意一个分支中,我们都可以计算出缩放的比例(desiredRatio),然后把作为“依据”的那条边设为desiredWidth/Height,将另一条边根据缩放比例进行调整。在计算和比较过程中我们都使用了decimal数据类型,因为它是.NET中精度最高的浮点数类型,我们以此减少计算过程中所带来的误差。

至于得到了newWidth和newHeight之后,我们便只要根据这个尺寸生成目标图片即可,它便是源图片的缩略图,符合我们之前提出的三个要求。

听起来很简单,看上去也没有什么问题,不是吗?不过,其实这个实现中有一个不那么明显的问题,您发现了吗?(答案

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

104 条回复

  1. 老赵
    admin
    链接

    老赵 2009-11-24 00:36:00

    优库真烦,bug真多,上传工具都没法用了。
    不爽,吼一声,睡觉去了。

  2. cry
    *.*.*.*
    链接

    cry 2009-11-24 00:44:00

    O(∩_∩)O哈哈~,你姓赵,我也姓赵,我也吼一声!

  3. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-24 01:07:00

    这代码写的各种烂法 不知道老赵说的是哪一点
    1.return originalBmp;原图片存在被修改的可能性,返回值的行为不一致
    2.这段代码实在有够麻烦

        int newWidth, newHeight;
    
        // scale down the smaller dimension
        if ((decimal)desiredWidth / originalBmp.Width < (decimal)desiredHeight / originalBmp.Height)
        {
            decimal desiredRatio = (decimal)desiredWidth / originalBmp.Width;
            newWidth = desiredWidth;
            newHeight = (int)(originalBmp.Height * desiredRatio);
        }
        else
        {
            decimal desiredRatio = (decimal)desiredHeight / originalBmp.Height;
            newHeight = desiredHeight;
            newWidth = (int)(originalBmp.Width * desiredRatio);
        }
    

    明明这样就可以搞定:
        int newWidth, newHeight;
        decimal desiredRatio = Math.Min((decimal)desiredWidth / originalBmp.Width,(decimal)desiredHeight / originalBmp.Height)
        newWidth = Math.Floor(originalBmp.Width * desiredRatio);
        newHeight = Math.Floor(originalBmp.Height * desiredRatio);
    

    当然还有原来的程序里面没做舍入处理,可能出1像素的误差

  4. xiaotie
    *.*.*.*
    链接

    xiaotie 2009-11-24 01:10:00

    别的还没看出来,就看出来这句:originalBmp.Width < desiredWidth && originalBmp.Height < desiredHeight
    应该<=

  5. 老赵
    admin
    链接

    老赵 2009-11-24 01:13:00

    @xiaotie
    如果是相等的话,去哪个分支不都是一样的嘛。

  6. 老赵
    admin
    链接

    老赵 2009-11-24 01:17:00

    @winter-cn
    这个做法简单是简单,巧妙是巧妙了,但是问题依旧……
    不过你说的可能也是可以值得的地方吧,有一定道理,呵呵。

  7. xiaotie
    *.*.*.*
    链接

    xiaotie 2009-11-24 01:17:00

    @Jeffrey Zhao
    如果相等就不计算了。

  8. 老赵
    admin
    链接

    老赵 2009-11-24 01:21:00

    xiaotie:
    @Jeffrey Zhao
    如果相等就不计算了。


    啊,你说的没错,我改正。:)

  9. xiaotie
    *.*.*.*
    链接

    xiaotie 2009-11-24 01:39:00

    decimal 没必要用。不过这些都属于无关疼痒的。

    看到Brushes.White,想起前两天碰到的问题:我先New了一个Bitmap,然后在上面draw something(black),然后我把这个Bitmap转换成灰度图,成漆黑一片。折腾了一会才发现问题在于没有先用Brushes.White填充Bitmap。一个Bitmap初始化时每一个像素都是00000000,是透明度为100%的黑色图像,然后,向上面画黑色图案,画进去的是透明度为0%的像素,也即:FF000000,转换成灰度图没考虑到透明度,转换过后就漆黑一片了。

  10. hoodlum1980
    *.*.*.*
    链接

    hoodlum1980 2009-11-24 02:35:00

    decimal desiredRatio = (decimal)desiredWidth / originalBmp.Width;
    newWidth = desiredWidth;
    newHeight = (int)(originalBmp.Height * desiredRatio);


    (1)这里的计算不太对吧。可能会存在很小的误差。
    (2)或者newHeight没四舍五入。

  11. Nana's Lich
    *.*.*.*
    链接

    Nana's Lich 2009-11-24 05:57:00

    @xiaotie
    我看问题就出在这个Brushes.White上。
    不过其实际情况好像和你之前遇到的问题正好相反。
    这个略缩图函数要输出的只有缩小以后的原图,不应该包含任何不来自于原图的信息。
    在大部分的情况下,这个Brushes.White填充的背景应该起不到什么作用——因为最终都会被缩小以后的原图所覆盖。

    但是,万一原图不能覆盖呢?
    例如,原包含透明/半透明像素的话,最终结果会变成什么呢?

    你之前所遇到的问题是,在转换成灰度图的时候原本“黑色但透明”的像素变成了无法透明的黑色;而这个文中的代码,应该会有本来应该透明的部分被填上了不透明的白色的问题。

  12. MichaelPeng
    *.*.*.*
    链接

    MichaelPeng 2009-11-24 07:53:00

    [TestMethod]
    public void Thumbail_Test()
    {
    Bitmap bmp = new Bitmap(500, 10);
    Int32 desiredWidth = 20;
    Int32 desiredHeight = 20;
    CreateThumbnail(bmp, desiredWidth, desiredHeight);
    }

    这时就会抛出异常。只按缩放比例小的一条边去缩放,另外一条边可能得出长度为零。
    这个问题一是dev写的不严谨,另外test也没考虑边界情况。

    另外
    1 /// <param name="imageStream">stream to create thumbnail for</param>
    这里注释参数不对,如果选择在编译时生成xml文档,就会报警告。

    2 if ((decimal)desiredWidth / originalBmp.Width < (decimal)desiredHeight / originalBmp.Height)
    {
    decimal desiredRatio = (decimal)desiredWidth / originalBmp.Width;
    这里有重复计算,不知道编译器是否会优化

  13. Athrun
    *.*.*.*
    链接

    Athrun 2009-11-24 08:11:00

    原图的宽和高相等的时候程序执行下面这段
    else
    {

    decimal desiredRatio = (decimal)desiredHeight / originalBmp.Height;

    newHeight = desiredHeight;

    newWidth = (int)(originalBmp.Width * desiredRatio);
    }

    newHeight = desiredHeight;//此时新图片的高就没有进行处理还是原图片的高,而宽大却进行了等比例缩小.

    加一个相等时的计算,思路应该更清晰.

    最后说一句,我也是一个典型的拿来主义,以后还是得多思考了.

  14. Nana's Lich
    *.*.*.*
    链接

    Nana's Lich 2009-11-24 08:15:00

    @MichaelPeng
    好仔细。
    不过说到这种(1和0之间的)情况,短的一边缩成1之后显然是什么都看不清的……如果预料到这个的话,就可以另行对待这样的情况了。

  15. mysun
    *.*.*.*
    链接

    mysun 2009-11-24 09:10:00

    我觉得应该是缩放的精度不准确,可能是decimal与int类型之间造成的吧,最后还得请老赵说明,谢谢

  16. 老赵
    admin
    链接

    老赵 2009-11-24 09:20:00

    @Athrun
    这段逻辑没有问题吧,如果相等的话,随便进哪个分支都是没有问题的。

  17. 老赵
    admin
    链接

    老赵 2009-11-24 09:21:00

    Nana's Lich:
    @xiaotie
    我看问题就出在这个Brushes.White上。
    不过其实际情况好像和你之前遇到的问题正好相反。
    这个略缩图函数要输出的只有缩小以后的原图,不应该包含任何不来自于原图的信息。
    在大部分的情况下,这个Brushes.White填充的背景应该起不到什么作用——因为最终都会被缩小以后的原图所覆盖。

    但是,万一原图不能覆盖呢?
    例如,原包含透明/半透明像素的话,最终结果会变成什么呢?

    你之前所遇到的问题是,在转换成灰度图的时候原本“黑色但透明”的像素变成了无法透明的黑色;而这个文中的代码,应该会有本来应该透明的部分被填上了不透明的白色的问题。


    文中注释已经说明了这是by design的,呵呵。

  18. msikruby
    *.*.*.*
    链接

    msikruby 2009-11-24 09:26:00

    个人认为newWidth, newHeight需要和 desiredWidth, desiredHeight做个比较,选择合适的Width和Height再绘图

  19. 老赵
    admin
    链接

    老赵 2009-11-24 09:27:00

    @msikruby
    怎么说?现在不就是选择合适的再绘图吗?

  20. Jon.Hong
    *.*.*.*
    链接

    Jon.Hong 2009-11-24 09:32:00

    graphics.FillRectangle(Brushes.White, 0, 0, newWidth, newHeight);
    换成
    g.Clear(Color.White); ?

  21. hello,老赵[未注册用户]
    *.*.*.*
    链接

    hello,老赵[未注册用户] 2009-11-24 09:34:00

    这两行代码有问题,会造成1px的偏差。

    newHeight = (int)(originalBmp.Height * desiredRatio);

    newWidth = (int)(originalBmp.Width * desiredRatio);

    应该用Math.Round方法。

  22. rockshit
    *.*.*.*
    链接

    rockshit 2009-11-24 09:46:00

    太无聊了 ,大学里 玩玩这种还可以。简直是考验别人的智商。

  23. rockshit
    *.*.*.*
    链接

    rockshit 2009-11-24 09:48:00

    有bug就直接指出来 ,不需要大家玩这套东西。

  24. 老赵
    admin
    链接

    老赵 2009-11-24 09:48:00

    @rockshit
    你工作时编程不找bug?
    你工作时可以写出有bug的程序?
    大学里编程和工作时编程有什么区别?

  25. rockshit
    *.*.*.*
    链接

    rockshit 2009-11-24 09:49:00

    你会叫你的员工 ,开个会 ,然后聊这个?

  26. rockshit
    *.*.*.*
    链接

    rockshit 2009-11-24 09:50:00

    开博,最好是思想的沟通。不是你的 讲座。
    代码级别的沟通,有底低级了。

  27. 老赵
    admin
    链接

    老赵 2009-11-24 09:50:00

    @rockshit
    当然会,你们team是不是从来不做code review?

  28. rockshit
    *.*.*.*
    链接

    rockshit 2009-11-24 09:52:00

    老赵 , 你太牛逼了。
    王下定论的能力 高人一等。
    不能低调点嘛。

  29. 老赵
    admin
    链接

    老赵 2009-11-24 09:52:00

    @rockshit
    又见技术的三六九等,代码交流就低级了么。
    说到思想沟通,我觉得评论里从来不缺思想沟通阿,呵呵。

  30. 老赵
    admin
    链接

    老赵 2009-11-24 09:53:00

    @rockshit
    嗯嗯,说得对,低调低调。

  31. rockshit
    *.*.*.*
    链接

    rockshit 2009-11-24 09:54:00

    不过说实在 ,code review 实在乏味。

  32. 冰品羽扇
    *.*.*.*
    链接

    冰品羽扇 2009-11-24 09:55:00

    看不出来,多半情况正常,但也可能由于1px误差造成如下情况吧,老赵别卖关子了,公布标准答案吧

  33. Old
    *.*.*.*
    链接

    Old 2009-11-24 09:55:00

    Athrun:
    原图的宽和高相等的时候程序执行下面这段
    else
    {

    decimal desiredRatio = (decimal)desiredHeight / originalBmp.Height;

    newHeight = desiredHeight;

    newWidth = (int)(originalBmp.Width * desiredRatio);
    }

    newHeight = desiredHeight;//此时新图片的高就没有进行处理还是原图片的高,而宽大却进行了等比例缩小.

    加一个相等时的计算,思路应该更清晰.

    最后说一句,我也是一个典型的拿来主义,以后还是得多思考了.



    //////////////////////////////////////////////////////
    void Test2()
    {
    Bitmap bmp = new Bitmap(30, 30);
    Int32 desiredWidth = 10;
    Int32 desiredHeight = 10;
    Bitmap newBmp = CreateThumbnail(bmp, desiredWidth, desiredHeight); //height=10;width=9;
    }

    原图如果是(w=30,h=30),缩略图为(w=10,h=10)。
    而实际返回的是(w=9,h=10)

  34. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-24 09:56:00

    我第一眼的印象 可能是 Graphic dispose的时候连bmpout一起干掉了。
    就好象 streamwriter.close 的时候 stream也被close一样。

    仅仅是第一印象 我再看看

  35. rockshit
    *.*.*.*
    链接

    rockshit 2009-11-24 09:58:00

    老赵 ,你们team代码的编写指导和指导 有什么原则嘛,分享下。

  36. 老赵
    admin
    链接

    老赵 2009-11-24 09:58:00

    @冰品羽扇
    没有标准答案,只有我准备的答案,上面也有朋友指出其他问题了啊。
    再过一会儿吧。话说你这个图片是你自己画的,还是什么地方的示意图啊?

  37. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-24 09:59:00

    =-= 原来是用了很多年的代码 oh yeah 看来不是我说的第一印象阿

  38. aohan
    *.*.*.*
    链接

    aohan 2009-11-24 10:00:00

    各位都好仔细,这代码真是需要推敲,想想都有道理

    graphics.FillRectangle(Brushes.White, 0, 0, newWidth, newHeight);

    直接Brushes.White这样填,可能是会存在问题,是否

    graphics.Clear(Color.Transparent);

    以透明背景来代替

  39. Jon.Hong
    *.*.*.*
    链接

    Jon.Hong 2009-11-24 10:01:00

    @韦恩卑鄙 alias:v-zhewg
    graphic disposed掉,bmpout不会的

  40. mysun
    *.*.*.*
    链接

    mysun 2009-11-24 10:01:00

    你还别说,老赵,这要是以前我会直接copy过去用,不会管是不是有问题,但这几年回过头发现,当在用时似乎又很陌生,看来copy过来的即使老外写的也得仔细分析。

  41. Daniel Cai
    *.*.*.*
    链接

    Daniel Cai 2009-11-24 10:01:00

    此方法是不能用在有多帧的GIF图片上。

  42. 老赵
    admin
    链接

    老赵 2009-11-24 10:02:00

    rockshit:老赵 ,你们team代码的编写指导和指导 有什么原则嘛,分享下。


    没有什么特别的,一般不做硬性规定。
    最硬的规定就是根据Framework Design Guidelines定下的命名规则吧。
    其他设计啊,实现啊,都没有太死的原则,但是code review时都会讨论一下。

  43. fred chan
    *.*.*.*
    链接

    fred chan 2009-11-24 10:04:00

    略同。

  44. fred chan
    *.*.*.*
    链接

    fred chan 2009-11-24 10:05:00

    你还是 比较注重 思想级别的。

  45. 老赵
    admin
    链接

    老赵 2009-11-24 10:06:00

    Daniel Cai:此方法是不能用在有多帧的GIF图片上。


    是的,不过对于缩略图来说,怎么搞比较好?是不是就把一帧帧拆开缩小?

  46. slive
    *.*.*.*
    链接

    slive 2009-11-24 10:06:00

    快公布答案吧。。。

  47. 老赵
    admin
    链接

    老赵 2009-11-24 10:07:00

    mysun:你还别说,老赵,这要是以前我会直接copy过去用,不会管是不是有问题,但这几年回过头发现,当在用时似乎又很陌生,看来copy过来的即使老外写的也得仔细分析。


    这话说得,好像老外写的代码高人一等似的……

  48. xxxxxxxxxxxxxxxxxx[未注册用户…
    *.*.*.*
    链接

    xxxxxxxxxxxxxxxxxx[未注册用户] 2009-11-24 10:07:00

    aohan:
    各位都好仔细,这代码真是需要推敲,想想都有道理

    graphics.FillRectangle(Brushes.White, 0, 0, newWidth, newHeight);

    直接Brushes.White这样填,可能是会存在问题


    有啥问题?
    只不过可能出现变色线条而已

  49. mysun
    *.*.*.*
    链接

    mysun 2009-11-24 10:12:00

    @Jeffrey Zhao
    呵呵,那到没有这个意思
    不知具体答案是否可以明示了

  50. 老赵
    admin
    链接

    老赵 2009-11-24 10:13:00

    @mysun
    再过一段时间吧,我去做事了。
    其实已经有人说过了,只不过我会说地再简单一点详细一点。

  51. mysun
    *.*.*.*
    链接

    mysun 2009-11-24 10:24:00

    测了下,就是缩放后的宽度小了,问题出现在这吧
    else
    {
    decimal desiredRatio = (decimal)desiredHeight / originalBmp.Height;
    newHeight = desiredHeight;
    newWidth = (int)(originalBmp.Width * desiredRatio);
    }
    应该就是类型转换造成的

  52. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-11-24 10:31:00

    Jeffrey Zhao:

    mysun:你还别说,老赵,这要是以前我会直接copy过去用,不会管是不是有问题,但这几年回过头发现,当在用时似乎又很陌生,看来copy过来的即使老外写的也得仔细分析。


    这话说得,好像老外写的代码高人一等似的……


    很多人都这么认为,老外也是人,无非是母语,理解起来比我们容易而已.

  53. Daniel Cai
    *.*.*.*
    链接

    Daniel Cai 2009-11-24 10:32:00


    Jeffrey Zhao:

    Daniel Cai:此方法是不能用在有多帧的GIF图片上。


    是的,不过对于缩略图来说,怎么搞比较好?是不是就把一帧帧拆开缩小?


    http://www.codeproject.com/KB/GDI-plus/NGif.aspx

  54. Athrun
    *.*.*.*
    链接

    Athrun 2009-11-24 10:34:00

    Jeffrey Zhao:
    @Athrun
    这段逻辑没有问题吧,如果相等的话,随便进哪个分支都是没有问题的。


    的确是进那个分支都一样,我没有看清楚.:(

    到目前为止,还是不清楚bug在哪里.:(
    可至于那1px像素的问题,平时还真的很少考虑到.

  55. 品味从容
    *.*.*.*
    链接

    品味从容 2009-11-24 10:40:00

    看不出来,除非这一步
    if ((decimal)desiredWidth / originalBmp.Width < (decimal)desiredHeight / originalBmp.Height)
    如果相除结果相等的话,else里面的newWidth计算有点多余。

  56. JHT[未注册用户]
    *.*.*.*
    链接

    JHT[未注册用户] 2009-11-24 11:12:00

    (O.W>D.W && O.H<D.H)||(O.W<D.W && O.H>D.H)
    如:
    O:{W:100,H:30}=>D:{W:50,H:50}
    或者
    O:{W:30,H:100}=>D:{W:50,H:50}

  57. yuzusk[未注册用户]
    *.*.*.*
    链接

    yuzusk[未注册用户] 2009-11-24 11:27:00

    想到有本C++的书,就是以一个实现缩略图简单功能,但以工业级强度的要求来设计的软件作为topic的书。

  58. Justice
    *.*.*.*
    链接

    Justice 2009-11-24 11:41:00

    Nana's Lich:
    @xiaotie
    我看问题就出在这个Brushes.White上。
    不过其实际情况好像和你之前遇到的问题正好相反。
    这个略缩图函数要输出的只有缩小以后的原图,不应该包含任何不来自于原图的信息。
    在大部分的情况下,这个Brushes.White填充的背景应该起不到什么作用——因为最终都会被缩小以后的原图所覆盖。

    但是,万一原图不能覆盖呢?
    例如,原包含透明/半透明像素的话,最终结果会变成什么呢?

    你之前所遇到的问题是,在转换成灰度图的时候原本“黑色但透明”的像素变成了无法透明的黑色;而这个文中的代码,应该会有本来应该透明的部分被填上了不透明的白色的问题。


    我也认为问题在于这里,这种填充忽略了在图片中存在透明区域的问题,至于误差1px问题个人觉得完全可以忽略不计了...

  59. 卢春城
    *.*.*.*
    链接

    卢春城 2009-11-24 11:43:00

    (decimal)desiredWidth / originalBmp.Width < (decimal)desiredHeight / originalBmp.Height

    如果originalBmp.Width == 0 就出错了

    这里也可以用

    desiredWidth×originalBmp.Height < desiredHeight*originalBmp.Width

    来判断

  60. 老赵
    admin
    链接

    老赵 2009-11-24 11:46:00

    @卢春城
    bitmap的width不能为0的。

  61. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-24 12:02:00

    万恶的浮点运算器 我以为Math.Floor可以正确处理精度问题

  62. 老赵
    admin
    链接

    老赵 2009-11-24 12:04:00

    @winter-cn
    Math.Floor和精度没有关系啊,它只是简单的下取整。

  63. 老赵
    admin
    链接

    老赵 2009-11-24 12:04:00

    @MichaelPeng
    呵呵,用评论里的代码插入功能吧。:)

  64. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-24 12:17:00

    @Jeffrey Zhao
    注释里面写的equal or less than
    但是equal受到精度影响了

  65. 冷冷
    *.*.*.*
    链接

    冷冷 2009-11-24 12:25:00

    天,我等的花儿都谢了……

  66. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-24 12:29:00

    newWidth = (int)Math.Floor(Math.Min((decimal)originalWidth * desiredWidth / originalWidth, originalWidth * desiredHeight / originalHeight));
    newHeight = (int)Math.Floor(Math.Min((decimal)originalHeight * desiredWidth / originalWidth, originalHeight * desiredHeight / originalHeight)); 
    

    确实不是Math.Floor的问题 后来乘了个参数导致浮点运算精度误差被放大 Math.Floor得不到期望的结果了
    这个答案应该就没错了

  67. bnf[未注册用户]
    *.*.*.*
    链接

    bnf[未注册用户] 2009-11-24 12:31:00

    没想到老赵也装B阿。。。

  68. 老赵
    admin
    链接

    老赵 2009-11-24 12:41:00

    @bnf
    1、当然要装……
    2、现在这也叫装?

  69. xiaotie
    *.*.*.*
    链接

    xiaotie 2009-11-24 12:46:00

    @winter-cn
    这里decimal纯属没用,图像缩小Scale在0.01-1之间,float的精度都够用了。

  70. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-24 12:47:00

    Jeffrey Zhao:
    @bnf
    1、当然要装……
    2、现在这也叫装?


    老赵就像树上的茶叶 要浸泡在杯具当中是注定的事情

  71. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-24 12:48:00

    @xiaotie
    随便一写 见笑见笑
    主要是编译器要求匹配重载 顺手写了个decimal 我对C#也不是很熟悉

  72. Activenetwork
    *.*.*.*
    链接

    Activenetwork 2009-11-24 13:10:00

    极端情况下会发生整形溢出。

    Sorry, 换个马甲再上来。用公司的广告号评论影响不好。

  73. Joey Yin
    *.*.*.*
    链接

    Joey Yin 2009-11-24 13:18:00

    换好了。

    不关心具体逻辑,只要看到

    newHeight = (int)(originalBmp.Height * desiredRatio);
    

    潜意识就觉得这是个溢出点。结果仔细看看逻辑,真有可能出现用户构造极端输入造成溢出的情况。如果是C++代码的话就惨了。


    汗,再仔细看了看,不会溢出的。desiredRatio不会大于1.

  74. MichaelPeng
    *.*.*.*
    链接

    MichaelPeng 2009-11-24 13:20:00

    其实不用decimal,整型完全搞定。要是碰到整型溢出,估计是内存先耗光了。

    public Bitmap CreateThumbnail1(Bitmap originalBmp, int desiredWidth, int desiredHeight)
    {
        // If the image is smaller than a thumbnail just return it
        if (originalBmp.Width <= desiredWidth && originalBmp.Height <= desiredHeight)
        {
            return originalBmp;
        }
    
        int newWidth, newHeight;
        Int32 p1 = desiredWidth * originalBmp.Height;
        Int32 p2 = desiredHeight * originalBmp.Width;
    
        if (p1 < p2)
        {
            newWidth = desiredWidth;
            newHeight = Math.Max(p1 / originalBmp.Width, 1);
        }
        else
        {
            newWidth = Math.Max(p2 / originalBmp.Height, 1);
            newHeight = desiredHeight;
    
        }
    
    
    
        // This code creates cleaner (though bigger) thumbnails and properly
        // and handles GIF files better by generating a white background for
        // transparent images (as opposed to black)
        // This is preferred to calling Bitmap.GetThumbnailImage()
        Bitmap bmpOut = new Bitmap(newWidth, newHeight);
    
        using (Graphics graphics = Graphics.FromImage(bmpOut))
        {
            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphics.FillRectangle(Brushes.White, 0, 0, newWidth, newHeight);
            graphics.DrawImage(originalBmp, 0, 0, newWidth, newHeight);
        }
    
        return bmpOut;
    }
    [TestMethod]
    public void Thumbail_BigWidth_Test()
    {
        Bitmap bmp = new Bitmap(500, 10);
        Int32 desiredWidth = 20;
        Int32 desiredHeight = 20;
        Bitmap bmp1 = CreateThumbnail1(bmp, desiredWidth, desiredHeight);
        Assert.AreEqual(20, bmp1.Width);
        Assert.AreEqual(1, bmp1.Height);
    }
    
    [TestMethod]
    public void Thumbail_BigHeight_Test()
    {
        Bitmap bmp = new Bitmap(10, 500);
        Int32 desiredWidth = 20;
        Int32 desiredHeight = 20;
        Bitmap bmp1 = CreateThumbnail1(bmp, desiredWidth, desiredHeight);
        Assert.AreEqual(20, bmp1.Height);
        Assert.AreEqual(1, bmp1.Width);
    }
    
    [TestMethod]
    public void Thumbail_Normal_Test()
    {
        Bitmap bmp = new Bitmap(40, 30);
        Int32 desiredWidth = 20;
        Int32 desiredHeight = 20;
        Bitmap bmp1 = CreateThumbnail1(bmp, desiredWidth, desiredHeight);
        Assert.AreEqual(15, bmp1.Height);
        Assert.AreEqual(20, bmp1.Width);
    }
    
    
    

  75. taolaptop[未注册用户]
    *.*.*.*
    链接

    taolaptop[未注册用户] 2009-11-24 13:31:00

    0

  76. 钧梓昊逑
    *.*.*.*
    链接

    钧梓昊逑 2009-11-24 13:34:00

    第一眼的问题就是参数没有检查,originalBmp为null的检查,desiredWidth……的小于等于零检查。

  77. 钧梓昊逑
    *.*.*.*
    链接

    钧梓昊逑 2009-11-24 13:39:00

    另外,在原图尺寸小于等于目标尺寸和大于目标尺寸时返回的引用性质不同。
    对于这个方法的消费代码来说,无法事先知道返回值和参数是否是同一个引用,有可能引起错误的断言。
    那么依赖于引用相同或不同的代码当中,有可能会在不同的参数情境下产生不同的运行结果。

  78. Jon.Hong
    *.*.*.*
    链接

    Jon.Hong 2009-11-24 13:40:00

    @钧梓昊逑
    嗯,
    可能会进行两次dispose

  79. 钧梓昊逑
    *.*.*.*
    链接

    钧梓昊逑 2009-11-24 13:44:00

    一般的方法设计中,如果对参数对象进行修改返回修改后的对象,返回对象应该和参数对象引用是不同的,所以在原图尺寸小于等于目标尺寸时应进行复制后返回。

  80. 钧梓昊逑
    *.*.*.*
    链接

    钧梓昊逑 2009-11-24 13:44:00

    @Jon.Hong
    两次dispose往往是必要的

  81. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-24 13:47:00

    @MichaelPeng
    用不用整型不是溢出问题 而是精度问题

  82. hoodlum1980
    *.*.*.*
    链接

    hoodlum1980 2009-11-24 13:50:00

    钧梓昊逑:
    另外,在原图尺寸小于等于目标尺寸和大于目标尺寸时返回的引用性质不同。
    对于这个方法的消费代码来说,无法事选知道是否同一个引用,也有可能引起错误的断言。
    那么依赖于引用相同或不同的代码当中,有可能会在不同的参数情境下产生不同的运行结果。



    这倒的确是这个函数最大的“问题”。在两种情况下,一个是返回本身,一个是返回了新建对象。

  83. Joey Yin
    *.*.*.*
    链接

    Joey Yin 2009-11-24 13:52:00

    @钧梓昊逑

    钧梓昊逑:
    另外,在原图尺寸小于等于目标尺寸和大于目标尺寸时返回的引用性质不同。
    对于这个方法的消费代码来说,无法事选知道是否同一个引用,也有可能引起错误的断言。
    那么依赖于引用相同或不同的代码当中,有可能会在不同的参数情境下产生不同的运行结果。



    眼睛真毒。

  84. ykb[未注册用户]
    *.*.*.*
    链接

    ykb[未注册用户] 2009-11-24 14:07:00

    可能出现高等于0或者宽度等于0的情况,这样这段代码肯定抛出异常

  85. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-11-24 14:12:00

    也许这个也比较适合GPU处理?

  86. ykb[未注册用户]
    *.*.*.*
    链接

    ykb[未注册用户] 2009-11-24 14:15:00

    可能出现高等于0或者宽度等于0的情况,这样这段代码肯定抛出异常 如果输入的图片高度很小宽度很宽,或者高度很高,宽度很小,

  87. Jon.Hong
    *.*.*.*
    链接

    Jon.Hong 2009-11-24 14:21:00

    @装配脑袋
    脑袋就是脑袋

  88. Duron800[未注册用户]
    *.*.*.*
    链接

    Duron800[未注册用户] 2009-11-24 14:36:00

    return bmpOut.Clone()?

  89. 张蒙蒙
    *.*.*.*
    链接

    张蒙蒙 2009-11-24 17:21:00

    我支持codereview

  90. MichaelPeng
    *.*.*.*
    链接

    MichaelPeng 2009-11-24 19:31:00

    @winter-cn
    在这种问题下整型反而是解决精度问题的首选,没有舍入误差。

  91. 小城故事
    *.*.*.*
    链接

    小城故事 2009-11-24 21:52:00

    先生成一个缩略图画布bmpOut,缩略图的位置应该绘在画布的中央,而不是左上角,对于缩略图和原图长宽比例相差较大时,看上去不太舒服。

    另一种方案是从中间裁剪原图,将画布完全覆盖。

  92. Nana's Lich
    *.*.*.*
    链接

    Nana's Lich 2009-11-24 23:55:00

    Jeffrey Zhao:

    Nana's Lich:
    @xiaotie
    我看问题就出在这个Brushes.White上。
    不过其实际情况好像和你之前遇到的问题正好相反。
    这个略缩图函数要输出的只有缩小以后的原图,不应该包含任何不来自于原图的信息。
    在大部分的情况下,这个Brushes.White填充的背景应该起不到什么作用——因为最终都会被缩小以后的原图所覆盖。

    但是,万一原图不能覆盖呢?
    例如,原包含透明/半透明像素的话,最终结果会变成什么呢?

    你之前所遇到的问题是,在转换成灰度图的时候原本“黑色但透明”的像素变成了无法透明的黑色;而这个文中的代码,应该会有本来应该透明的部分被填上了不透明的白色的问题。


    文中注释已经说明了这是by design的,呵呵。



    居然把by design的功能误会成是缺陷……真是太丢人了……


    Jeffrey Zhao:
    @rockshit
    嗯嗯,说得对,低调低调。


    吵架的不要——老赵前辈也快点说答案吧,这把我给急的!

  93. 老赵
    admin
    链接

    老赵 2009-11-25 09:04:00

    小城故事:
    先生成一个缩略图画布bmpOut,缩略图的位置应该绘在画布的中央,而不是左上角,对于缩略图和原图长宽比例相差较大时,看上去不太舒服。

    另一种方案是从中间裁剪原图,将画布完全覆盖。


    这个方法和画布尺寸没有关系,生成的就是幅缩略图,它的大小是newWidth和newHeight而不是desiredWidth和desiredHeight。

  94. 小城故事
    *.*.*.*
    链接

    小城故事 2009-11-25 12:03:00

    @Jeffrey Zhao
    老赵没明白偶的意思,不是说大小,是绘制的位置,试想如果缩略图背景只有左边或上边是真正的缩略图,而其余一半是空白,这样就不符合要求。

  95. 老赵
    admin
    链接

    老赵 2009-11-25 12:07:00

    @小城故事
    我明白你的意思,我是说,我现在这个方法和背景无关,没有多余背景的,就生成一幅缩略图的。

  96. 小城故事
    *.*.*.*
    链接

    小城故事 2009-11-25 13:24:00

    @Jeffrey Zhao
    bmpOut,确定了长宽,背景是白色,这就是画布。如果这句

    graphics.DrawImage(originalBmp, 0, 0, newWidth, newHeight)

    绘缩略图没有将bmpOut填满的话,不就有多余空白背景了吗?我一般这样处理,把缩略图绘在bmpOut的中间:

    int left = (originalBmp.Width - newWidth)/2;
    int top = (originalBmp.Height - newHeight)/2;
    graphics.DrawImage(originalBmp, left, top, newWidth, newHeight);

  97. 老赵
    admin
    链接

    老赵 2009-11-25 13:25:00

    @小城故事
    画布尺寸是newWidth和newHeight,它就是计算后的缩略图尺寸。
    你把这段代码运行一下试试看吧。

  98. 小城故事
    *.*.*.*
    链接

    小城故事 2009-11-25 13:30:00

    @Jeffrey Zhao
    哦,对不起,没看清楚,原来你的缩略图尺寸会变的。呵呵,实际上基本不会这么用的。不过要是老赵本意就如此,那也没问题了。

  99. 老赵
    admin
    链接

    老赵 2009-11-25 13:33:00

    @小城故事
    实际上不都是我这样用的吗?互联网上大部分相册服务不都是我这种做法吗?
    我觉得你可能还是没理解这段代码的含义,运行一下吧。

  100. 小城故事
    *.*.*.*
    链接

    小城故事 2009-11-25 13:51:00

    @Jeffrey Zhao
    我明白,我也做过相册,包括查看和上传,如果bmpOut不固定大小,客户端要作大量工作才能达到一般相册的效果,不然会乱七八糟而难看。如果老赵相册是这么处理缩略图的,那你如何解决的缩略图大小不一的问题呢?比如desiredWidth和desiredHeight都是200,两个缩略图,一个是200×100,一个是100×200,客户端如何把这两个图一起显示?

  101. 老赵
    admin
    链接

    老赵 2009-11-25 14:01:00

    @小城故事
    客户端居中显示咯。

  102. binlib001[未注册用户]
    *.*.*.*
    链接

    binlib001[未注册用户] 2009-12-02 15:05:00

    应该是强制转化有问题吧。因该用Convert吧。

  103. qiushi[未注册用户]
    *.*.*.*
    链接

    qiushi[未注册用户] 2009-12-23 13:12:00

    我觉得应该是最后bmp没有被释放吧

  104. tbwind[未注册用户]
    *.*.*.*
    链接

    tbwind[未注册用户] 2010-03-12 14:52:00

    看得我信心大增啊..
    以前我也照网上东凑西拼做了个缩略图生成,其中碰到一些问题,都陆续解决了.
    [code]
    nTarWidth=240;
    nTarHeight=180;
    //----------------------生成缩略图尺寸----------------------
    if((float)nWidth/(float)nHeight>(float)nTarWidth/(float)nTarHeight)
    {
    nTarHeight=nHeight*nTarWidth/nWidth;
    if(nTarHeight==0)
    nTarHeight=2;
    }
    else
    {
    nTarWidth=nWidth*nTarHeight/nHeight;
    if(nTarWidth==0)
    nTarWidth=2;
    }

    [/code]
    英语不好,目标尺寸用了个tar(get),也不知道合不合适.
    那个背景我也填白了,因为要生成JPG缩略图,不支持透明.

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我