如何修改 python 中现有文件的压缩 itxt 记录?

How to modify a compressed itxt record of an existing file in python?

我知道这看起来太简单了,但我找不到直接的解决方案。

保存后,itxt 应该再次压缩。

没你想象的那么简单。如果是,您可能已经发现没有直接的解决方案。

让我们从基础开始。

PyPNG 可以读取所有块吗?

一个重要的问题,因为修改现有的 PNG 文件是一项艰巨的任务。阅读它的文档,开始时并不顺利:

PNG: Chunk by Chunk

Ancillary Chunks

.. iTXt
Ignored when reading. Not generated.

(https://pythonhosted.org/pypng/chunk.html)

但在那一页的下方,救恩!

Non-standard Chunks
Generally it is not possible to generate PNG images with any other chunk types. When reading a PNG image, processing it using the chunk interface, png.Reader.chunks, will allow any chunk to be processed (by user code).

所以我所要做的就是写这个'user code',PyPNG 可以完成剩下的工作。 (糟糕。)

iTXt 区块呢?

让我们来看看你感兴趣的内容。

4.2.3.3. iTXt International textual data

.. the textual data is in the UTF-8 encoding of the Unicode character set instead of Latin-1. This chunk contains:

Keyword:             1-79 bytes (character string)
Null separator:      1 byte
Compression flag:    1 byte
Compression method:  1 byte
Language tag:        0 or more bytes (character string)
Null separator:      1 byte
Translated keyword:  0 or more bytes
Null separator:      1 byte
Text:                0 or more bytes

(http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.iTXt)

我看起来很清楚。可选的压缩应该不是问题,因为

.. [t]he only value presently defined for the compression method byte is 0, meaning zlib ..

而且我非常有信心 Python 存在可以为我做到这一点的东西。

然后回到 PyPNG 的块处理。

我们能看到区块数据吗?

PyPNG 提供了一个 迭代器 ,因此检查 PNG 是否包含 iTXt 块确实很容易:

chunks()
Return an iterator that will yield each chunk as a (chunktype, content) pair.

(https://pythonhosted.org/pypng/png.html?#png.Reader.chunks)

所以让我们在交互模式下编写一些代码并检查一下。我从 http://pmt.sourceforge.net/itxt/ 那里得到了一张示例图片,为方便起见在这里重复了一遍。 (如果iTXt数据没有保存在这里,请下载并使用原始数据。)

>>> import png
>>> imageFile = png.Reader("itxt.png")
>>> print imageFile
<png.Reader instance at 0x10ae1cfc8>
>>> for c in imageFile.chunks():
...   print c[0],len(c[1])
... 
IHDR 13
gAMA 4
sBIT 4
pCAL 44
tIME 7
bKGD 6
pHYs 9
tEXt 9
iTXt 39
IDAT 4000
IDAT 831
zTXt 202
iTXt 111
IEND 0

成功!

回信呢?好吧,PyPNG 通常用于创建完整的图像,但幸运的是,它还提供了一种从自定义块显式创建图像的方法:

png.write_chunks(out, chunks)
Create a PNG file by writing out the chunks.

因此我们可以遍历块,更改所需的块,然后写回修改后的 PNG。

解包和打包iTXt数据

这本身就是一项任务。数据格式描述得很好,但不适合 Python 的原生 unpackpack 方法。所以我们必须自己发明一些东西。

文本字符串以 ASCIIZ 格式存储:以零字节结尾的字符串。我们需要一个小函数来拆分第一个 0:

def cutASCIIZ(str):
   end = str.find(chr(0))
   if end >= 0:
      result = str[:end]
      return [str[:end],str[end+1:]]
   return ['',str]

这个简单的函数 returns [beforeafter] 对的数组,并丢弃零本身。

为了尽可能透明地处理 iTXt 数据,我将其设为 class:

class Chunk_iTXt:
  def __init__(self, chunk_data):
    tmp = cutASCIIZ(chunk_data)
    self.keyword = tmp[0]
    if len(tmp[1]):
      self.compressed = ord(tmp[1][0])
    else:
      self.compressed = 0
    if len(tmp[1]) > 1:
      self.compressionMethod = ord(tmp[1][1])
    else:
      self.compressionMethod = 0
    tmp = tmp[1][2:]
    tmp = cutASCIIZ(tmp)
    self.languageTag = tmp[0]
    tmp = tmp[1]
    tmp = cutASCIIZ(tmp)
    self.languageTagTrans = tmp[0]
    if self.compressed:
      if self.compressionMethod != 0:
        raise TypeError("Unknown compression method")
      self.text = zlib.decompress(tmp[1])
    else:
      self.text = tmp[1]

  def pack (self):
    result = self.keyword+chr(0)
    result += chr(self.compressed)
    result += chr(self.compressionMethod)
    result += self.languageTag+chr(0)
    result += self.languageTagTrans+chr(0)
    if self.compressed:
      if self.compressionMethod != 0:
        raise TypeError("Unknown compression method")
      result += zlib.compress(self.text)
    else:
      result += self.text
    return result

  def show (self):
    print 'iTXt chunk contents:'
    print '  keyword: "'+self.keyword+'"'
    print '  compressed: '+str(self.compressed)
    print '  compression method: '+str(self.compressionMethod)
    print '  language: "'+self.languageTag+'"'
    print '  tag translation: "'+self.languageTagTrans+'"'
    print '  text: "'+self.text+'"'

因为这使用了 zlib,它需要在你的程序的顶部有一个 import zlib

class 构造函数接受 'too short' 字符串,在这种情况下它将对所有未定义的内容使用默认值。

show 方法列出用于调试目的的数据。

使用我的习惯 class

有了所有这些,现在检查、修改和添加 iTXt 块终于 直截了当:

import png
import zlib

# insert helper and class here

sourceImage = png.Reader("itxt.png")
chunkList = []
for chunk in sourceImage.chunks():
  if chunk[0] == 'iTXt':
    itxt = Chunk_iTXt(chunk[1])
    itxt.show()
    # modify existing data
    if itxt.keyword == 'Author':
      itxt.text = 'Rad Lexus'
      itxt.compressed = 1
    chunk = [chunk[0], itxt.pack()]
  chunkList.append (chunk)

# append new data
newData = Chunk_iTXt('')
newData.keyword = 'Custom'
newData.languageTag = 'nl'
newData.languageTagTrans = 'Aangepast'
newData.text = 'Dat was leuk.'
chunkList.insert (-1, ['iTXt', newData.pack()])

with open("foo.png", "wb") as file:
  png.write_chunks(file, chunkList)

添加一个全新的块时,注意不要append它,因为那样它会出现在之后 required last IEND 块,这是一个错误。我没有尝试,但你也不应该在所需的第一个 IHDR 块之前插入它,或者(正如 Glenn Randers-Pehrson 评论的那样)在连续的 IDAT 块之间。

请注意,根据规范,iTXt 中的所有文本都应采用 UTF8 编码。