返回 登录
2

PNG 故障的艺术

阅读7023

作者: UCNV,坐标东京的程序员、艺术家。开发不同程序以破坏图像和电影格式,由此创造视觉作品并让不同的效果分别发挥作用。
本文未经允许不得转载,更多精彩文章请订阅《程序员》
UCNV授权《程序员》整理翻译并本文。译/郭韵,暨南大学外国语学院讲师,同声传译员。

PNG是一种图像格式,始于1995年,作为一种颇受欢迎的图像格式,PNG至今仍十分盛行。一般而言,PNG以无损失压缩或处理透明像素的能力为人所知。然而,我并不想从泛泛的角度去看待不同的图像格式,而是尝着用不同的方式让它们发生故障,你有没有想过,PNG发生故障时是什么效果呢?

校验和(Checksum)

让我们首先来看看校验和(Checksum)系统中的CRC32算法。这一算法通常用于确认受损图像,当它侦测到图像文件受损时,不会在一般的图片阅读器上显现出来。因此,若想使PNG发生故障,用诸如文本编辑器或二进制编辑器重写部分二进制数据的简单方法是行不通的。换句话说,“黑掉”PNG之难,难于上青天。

因此,为了能顺利“黑掉”PNG,我们需要根据PNG的格式规范(PNG Specification)来做。这意味着,解码CRC32算法后要对数据进行重写和重新计算,并将其绑定到已经编辑的数据上。

状态(State)

接下来,我们看看PNG的转码过程。表1简要地显示了PNG编码流工作流程。包括四步:原始数据、过滤后数据、压缩后数据、格式化的PNG。

图片描述

表1 PNG编码流

理论上来说,上述四个步骤都可以成为“下手”的目标。然而,从“原始数据”下手跟从BMP下手一样,技术上而言,并不能把它称之为“PNG故障”。而以最后一步“格式化的PNG”为目标也行不通,原因在于上文提到的校验和系统的工作原理,因此无法产生故障。

现在,能供我们下手的就只有“过滤后数据”或“压缩后数据”两个步骤,只要方法正确,就可以使PNG故障形成。当我攻击“过滤后数据”时,产生的效果非常明显,故障元素好像花瓣一样弥漫了整张图像,过滤器之间的差异在“过滤后数据”受到攻击时变得尤为明显。另一方面,“压缩后数据”故障受到了它们自身的压缩算法的影响(Deflate压缩),因此显示出了一种与雪花噪点类似的图片效果。

当然,在转码过程之外,也有其他的过程能影响PNG故障的图案,例如透明像素和interlace。

五重过滤(Five filters)

决定PNG故障图案最重要的决定因素是过滤的过程,这一过程通过某种算法将每一层扫描线的为压缩像素数据进行转换,从而提升压缩效率。过滤包括四种算法和五种过滤类型,分别为Sub、Up、Average、Paeth和None(None为无过滤)。PNG图像通常在使用最适当的过滤类型对每一层扫描线进行过滤后才开始压缩,因此,在PNG图像生成之后,五种压缩会合为一体。

这五种过滤通常只是促进压缩效率的提升,因此无论使用哪一层过滤,输出结果都一样。然而,当过滤后的数据受到损坏后,输出结果会显示出明显的差异。当一个图像经过优化,并同时具备上述五种过滤效果后,就很难辨别出不同的过滤效果。然而当同一个单一的过滤应用在每一条扫描线上,图像出现故障时,差异就很明显了。

下面我会展示每一种过滤的效果差异,当我们仔细留意不同的效果时,会发觉哪一种过滤会让PNG故障的部分产生哪种美丽的效果。是的,这些图像的确很美丽。

故障: 实例

图1、图2展示了通过攻击过滤后数据得到的图像。原始PNG已经优化了添加到每一个扫描线的过滤层,五种过滤中所有类型都已叠加。故障反映出五种过滤之间在结合的时候是如何平衡的。

图片描述

图1 原始PNG图像

图片描述

图2 故障的PNG图像

不同过滤类型之间的差异

现我们来看看每一种过滤类型之间的差异。

图3使用了None过滤,即原始数据故障。在这个状态之下,每一个像素单独存在,像素间无任何关联,因此,单单重写字节不会对图片造成广泛的影响。

图片描述

图3 使用None过滤类型所产生的PNG故障图

图4使用了Sub过滤类型,添加到每一条扫面线上。当使用Sub算法时,目标像素通过参照右边的像素进行自我重写。这也是为什么这类故障会向右倾斜。

图片描述

图4 使用Sub过滤类型所产生的PNG故障图

图5中过滤名为Up,该过滤类型与Sub类似,但参照方向在顶部和底部。

图片描述

图5 使用Up过滤类型所产生的PNG故障图

如图6所示,Average这一过滤的表现为斜角角度,它所产生的柔化效果也是该类过滤的特点之一。使用Average filter时,PNG故障效果的“故障感”不强。

图片描述

图6 使用Average过滤类型所产生的PNG故障图

如图7所示,Paeth这一过滤的算法在众多过滤中最为复杂,因此也具备最复杂的故障效果。该故障会对图像的不同部位产生影响,即使在最少字节重写的情况下也是如此。PNG故障的主要效果正是由该过滤引起,原始图像中的元素得以保存,却遭到了很大的损坏。

图片描述

图7 使用Paeth过滤类型所产生的PNG故障图

压缩后的故障

图8就是我在前文描述为“压缩数据”的故障状态。出现了大量雪花噪点,很难在图像中辨认出原图像。这一故障偶尔能显示出不同的过滤效果,但图像通常都已经损坏。

图片描述

图8 压缩后的PNG故障

透明

我们看看当图像中包含透明像素时故障后的效果。

图片描述

图9 包含阿尔法像素(alpha pixels)的PNG原图

图片描述

图10 包含阿尔法像素的故障PNG图像

图片描述

图11 带有交错效果的PNG故障

这种透明效果很明显,尤其是Average这一过滤看起来将透明像素逐渐揉合。透明像素的完全聚合也是作为彩色部分用同样的手法来处理。其实Up这一过滤通常是应用在彩色部分上的。

还有一种可能,更新的一般性图像格式会根据该图像是否有彩色部分,或当图像为复杂图像时(如照片),转变它们的压缩模式。使用包括实体彩色部分的图像来测试故障不失为一种有用的方法,WebP就是其中一个例子。

交错(Interlace)

PNG交错层可以使用基于8*8像素的Adam 7算法,分为7种通路。当PNG发生交错故障时,我们能够直接看出来。我们还可以通过中间是否出现了类似“缝合”的效果(它的角度在朝向Average过滤率的方向逐步收窄)来判断这一故障是否属于交错故障。

总结

与JPEG或其他较新的图像格式相比,PNG是一种非常简单的格式。其中的过滤算法就像玩具,它的压缩方法跟传统的Zip压缩很类似。然而,这种简单的图像格式展现了多种多样不同的故障类型。也许我们本来只需要一个例子去解释JPEG故障,却需要许多不同类型的例子来解释何为PNG故障。最初,PNG是作为GIF之外的另一种图像格式而出现的。然而,当讲到故障时,GIF格式跟PNG相比就太苍白无力了。PNG已经具备了令人惊艳的不同效果,只是长期以来,为校验和这一障碍所掩盖。

PNGlitch库

我在2010年曾经发表过一段关于PNG故障的脚本。当时,这一脚本只是剔除了CRC32,并且在内部数据故障后又重新加进去。此后,为了能将其实际应用在自己的工作中,我一直在重写并且不断修改完善该脚本。2014年,我决定将自己的知识整理到一个库中。Ruby数据库PNGlitch由此而生。本篇文章中所有的故障图像也都是来源于此库。

下面我们就看看如何使用PNGlitch库,使用者必须对Ruby语言有特定的了解才能明白其中代码的涵义。

PNGlitch库如何上手

png = PNGlitch.open '/path/to/your/image.png'
png.glitch do |data|
  data.gsub /\d/, 'x'
end
png.save '/path/to/broken/image.png'
png.close

上述代码也能用不同的方法重写,如下:

PNGlitch.open('/path/to/your/image.png') do |png|
  png.glitch do |data|
    data.gsub /\d/, 'x'
  end
  png.save '/path/to/broken/image.png'
end

该故障将压缩和解压数据作为单一字符串来进行处理。虽然用起来简单,但对内存的占用也相当惊人。当内存占用过大时,用户可以使用IO而非如下图所示的字符串来写代码。

PNGlitch.open('/path/to/your/image.png') do |png|
  buf = 2 ** 18
  png.glitch_as_io do |io|
    until io.eof? do
      d = io.read(buf)
      io.pos -= d.size
      io.print(d.gsub(/\d/, 'x'))
    end
  end
  png.save '/path/to/broken/image.png'
end

PNGlitch也提供了方法去处理每一条扫描线。

PNGlitch.open('/path/to/your/image.png') do |png|
  png.each_scanline do |scanline|
    scanline.gsub! /\d/, 'x'
  end
  png.save '/path/to/broken/image.png'
end

第一个故障的例子有时候会破坏表达过滤类型的字节,所以会输出某些图片阅读器不能打开的文件。逐行扫描线(Each scanline)的方法则安全得多,内存使用也很低。这是一个比较彻底的方法,但比故障法要花费更多的时间。

PNGlitch库进阶

通过像素数据和过滤类型可以得出扫描线数据。用户可以使用Scanline#replace_data重写像素数据:

png.each_scanline do |scanline|
  data = scanline.data
  scanline.replace_data(data.gsub(/\d/, 'x'))
end

也可以用Scanline#gsub! 来进行String#gsub!等处理。

png.each_scanline do |scanline|
  scanline.gsub! /\d/, 'x'
end

用户可以输入以下指令,来验证PNG的类型。原理上,None, Sub, Average, 以及Paeth这几种过滤都以0~4之间的数值来表示。

puts png.filter_types

用户也可以输入each_scanline 来查看过滤类型。

png.each_scanline do |scanline|
  puts scanline.filter_type
  scanline.change_filter 3
end

上述例子每一个过滤类型都转向3,即Average。使用新过滤时,可以输入change_filter。这一处理办法不会引起故障,因为过滤在此重新被计算,所以PNG图片会正常显示。这也意味着由此产生的图像看起来没有什么特别的视觉效果。

然而,每一个过滤之间的差异会极大地影响到不同的故障效果。

PNGlitch.open(infile) do |png|
  png.each_scanline do |scanline|
    scanline.change_filter 3
  end
  png.glitch do |data|
    data.gsub /\d/, 'x'
  end
  png.save outfile1
end

PNGlitch.open(infile) do |png|
  png.each_scanline do |scanline|
    scanline.change_filter 4
  end
  png.glitch do |data|
    data.gsub /\d/, 'x'
  end
  png.save outfile2
end

上述两个例子的输出效果完全不一样,不同点就是在于过滤类型的差异。

上面的代码示例都是基于“过滤后的数据”状态的。当用户希望将“压缩数据”调整到PNGlitch,可以使用glitch_after_compress命令。

png.glitch_after_compress do |data|
  data[rand(data.size)] = 'x'
  data
end

PNGlitch库是个开源库,地址:https://github.com/ucnv/pnglitch

PNG 故障目录

这里我要介绍一系列的故障类型,并且展示PNG故障表达的广泛性及种类。我会介绍以下三种主要的破坏数据的方法:

  • Replace:随机重写数据串;
  • Transpose:将字节串分成大的区块,重新组织;
  • Defect:随机删除字节串。

图片描述

图12 故障方法:Replace/过滤:优化/交错:None/基于过滤后数据故障

图片描述
图13 故障方法:Transpose/过滤:优化/交错:None/基于过滤后数据故障

图片描述
图14 故障方法:Replace/过滤:Sub/交错:None/基于过滤后数据故障

图片描述
图15 故障方法:Defect/过滤:Sub/交错:None/基于过滤后数据故障

上文提到了五种类型的过滤,分别是Sub, Up, Average, Paeth, 以及这些过滤的优化或组合版本。也提到了120种不同的组合,下面选择其中一部分进行展示。

require 'pnglitch'
count = 0
infiles = %w(lena.png lena-alpha.png)
infiles.each do |file|
  alpha = /alpha/ =~ file
  [false, true].each do |compress|
    [false, true].each do |interlace|
      infile = file
      if interlace
        system("convert -interlace plane %s tmp.png" % infile)
        infile = 'tmp.png'
      end
      [:optimized, :sub, :up, :average, :paeth].each do |filter|
        [:replace, :transpose, :defect].each do |method|
          count += 1
          png = PNGlitch.open infile
          png.change_all_filters filter unless filter == :optimized
          options = [filter.to_s]
          options << 'alpha' if alpha
          options << 'interlace' if interlace
          options << 'compress' if compress
          options << method.to_s
          outfile = "lena-d-%s.png" % [count, options.join('-')]
          process = lambda do |data, range|
            case method
            when :replace
              range.times do
                data[rand(data.size)] = 'x'
              end
              data
            when :transpose
              x = data.size / 4
              data[0, x] + data[x * 2, x] + data[x * 1, x] + data[x * 3..-1]
            when :defect
              (range / 5).times do
                data[rand(data.size)] = ''
              end
              data
            end
          end
          unless compress
            png.glitch do |data|
              process.call data, 50
            end
          else
            png.glitch_after_compress do |data|
              process.call data, 10
            end
          end
          png.save outfile
          png.close
        end
      end
    end
  end
end
File.unlink 'tmp.png

错误的过滤

PNG扫面线含有过滤类型字节和过滤像素字节,有意地进行两者的错配组合是PNG故障的另一个技术。
图片描述

图16 使用了错配过滤类型的PNG故障图像

图16由以下代码生成。在PNGlitch中,用户可以通过graft命令将错误的过滤类型添加到扫描线。

require 'pnglitch'
PNGlitch.open('png.png') do |png|
  png.each_scanline do |line|
    line.graft rand(5)
  end
  png.save "png-glitch-graft.png"
end

这一方法即使用于检查每一过滤类型的故障效果之间的差异也十分方便。图17~图21展示了在不更改扫描线数据的情况下,将每一个特定的过滤类型字节与扫描线结合的结果。

require 'pnglitch'
(0..4).each do |filter|
  PNGlitch.open('png.png') do |png|
    png.each_scanline do |line|
      line.graft filter
    end
    png.save "png-glitch-graft-#{filter}.png"
  end
end

图片描述

图17 使用了错配过滤类型的PNG图像,将每种过滤类型设置为None

图片描述

图18 使用了错配过滤类型的PNG图像,将每种过滤类型设置为Sub

图片描述
图19 使用了错配过滤类型的PNG图像,将每种过滤类型设置为Up

图片描述
图20 使用了错配过滤类型的PNG图像,将每种过滤类型设置为Average

图片描述
图21 使用了错配过滤类型的PNG图像,将每种过滤类型设置为Paeth

错误过滤的执行

如果执行了错误的过滤会有什么结果呢?PNGlitch库允许用户随意改变过滤方法,所以用户可以自己动手看看会有什么结果。 使用了标准过滤方法的图片播放器能解码由某种特定编码编写的PNG图像,这能产生某种算法故障(我不确定是否该称其为“故障”)。以下的图像就是通过上述方法产生的。

图片描述

图22 使用了错配过滤类型1的PNG图像

图片描述
图23 使用了错配过滤类型2的PNG图像

图片描述
图24 使用了错配过滤类型3的PNG图像

图片描述
图25 使用了错配过滤类型4的PNG图像

require 'pnglitch'
PNGlitch.open('png.png') do |p|
  p.each_scanline do |l|
    l.register_filter_encoder do |data, prev|
      data.size.times.reverse_each do |i|
        x = data.getbyte(i)
        v = prev ? prev.getbyte(i - 1) : 0
        data.setbyte(i, (x - v) & 0xff)
      end
      data
    end
  end
 p.output 'png-incorrect-filter01.png'
end
require 'pnglitch'
PNGlitch.open('png.png') do |p|
  p.change_all_filters 4
  p.each_scanline do |l|
    l.register_filter_encoder do |data, prev|
      data.size.times.reverse_each do |i|
        x = data.getbyte(i)
v = prev ? prev.getbyte(i - 6) : 0
        data.setbyte(i, (x - v) & 0xff)
      end
      data
    end
  end
  p.output 'png-incorrect-filter02.png'
end
equire 'pnglitch'
PNGlitch.open('png.png') do |png|
  png.change_all_filters 4
  sample_size = png.sample_size
  png.each_scanline do |l|
    l.register_filter_encoder do |data, prev|
      data.size.times.reverse_each do |i|
        x = data.getbyte i
        is_a_exist = i >= sample_size
        is_b_exist = !prev.nil?
        a = is_a_exist ? data.getbyte(i - sample_size) : 0
        b = is_b_exist ? prev.getbyte(i) : 0
        c = is_a_exist && is_b_exist ? prev.getbyte(i - sample_size) : 0
        p =  a + b - c
        pa = (p - a).abs
pb = (p - b).abs
        pc = (p - c).abs
        pr = pa <= pb && pa <= pc ? a : pb <= pc ? b : c
        data.setbyte i, (x - pr) & 0xfe
      end
      data
    end
  end
  png.output 'png-incorrect-filter03.png'
end
require 'pnglitch'
PNGlitch.open('png.png') do |p|
  p.change_all_filters 2
  p.each_scanline do |l|
    l.register_filter_encoder do |data, prev|
      data.size.times.reverse_each do |i|
        x = data.getbyte(i)
        v = prev ? prev.getbyte(i) : 0
        data.setbyte(i, (x - v) & 0xfe)
      end
      data
    end
  end
  p.output 'png-incorrect-filter04.png'
end

订阅2017年程序员(含iOS、Android及印刷版)请访问 http://dingyue.programmer.com.cn
图片描述


8大热门主题,近60场干货分享,众多Spark、Docker、Kubernetes、Mesos、Rancher、Tensorflow 的 Commiter或PMC来到CCTC 2017现场,和超过2000技术人分享云计算、大数据、区块链、人工智能、微服务等最新的技术发展方向和最具实战价值的经典案例。大会报名渠道已经开启,两天通票最低只需599,第一天更是史无前例的开放免费报名。还等什么,点击报名获得快人一步的技术发展新理念、新实践。

评论