物理の駅 Physics station by 現役研究者

テクノロジーは共有されてこそ栄える

Python: PNG形式の画像ファイルのDPIだけバイナリで書き換える

JPG形式の記事 と基本的には同じである。

違いは物理サイズ指定の単位が JPGではDPI (dots per inch) であるのに対して、PNGではPPM (pixels per meter) であること、チャンク(セグメント)ごとにCRC-32の巡回冗長検査があるのでそれを書き換える必要があることである。

以下、PNG形式の画像ファイルの物理サイズをA4の横幅にするコード。

def parse_png(file_path):
    with open(file_path, 'rb') as file:
        data = file.read()
    
    i = 0
    signature = data[i:i+8]
    assert("PNG" in str(signature))
    i+=8
    
    chunks = {}
    while True:
        length = int.from_bytes(data[i:i+4], 'big')
        chunk_type = data[i+4:i+8].decode('ascii')
        if chunk_type == 'IHDR':
            chunks[chunk_type] = (i,length)
        elif chunk_type == 'pHYs':
            chunks[chunk_type] = (i,length)
        if chunk_type == 'IEND':
            break
        i += (12+length)
        
    return chunks

import zlib

def modify_ppm(file_path, chunk_IHDR, chunk_pHYs):
    with open(file_path, 'rb') as f:
        data = f.read()
    
    width = int.from_bytes(data[chunk_IHDR[0]+8:chunk_IHDR[0]+12], 'big')
    height = int.from_bytes(data[chunk_IHDR[0]+12:chunk_IHDR[0]+16], 'big')
    precision = data[chunk_IHDR[0]+16]
    colors = data[chunk_IHDR[0]+17]
    
    x_density = int.from_bytes(data[chunk_pHYs[0]+8:chunk_pHYs[0]+12], 'big')
    y_density = int.from_bytes(data[chunk_pHYs[0]+12:chunk_pHYs[0]+16], 'big')
    units = data[chunk_pHYs[0]+16]
    
    #CRC-32検証
    # chunk_data = data[chunk_pHYs[0]+4:chunk_pHYs[0]+chunk_pHYs[1]+8]
    # crc_calculated = zlib.crc32(chunk_data) & 0xffffffff
    # crc_from_file = int.from_bytes(data[chunk_pHYs[0]+chunk_pHYs[1]+8:chunk_pHYs[0]+chunk_pHYs[1]+12],'big')
    # print(crc_calculated,"vs",crc_from_file)
    
    # print(f"PPM_Units : {units}")
    # print(f"X_PPM     : {x_density}")
    # print(f"Y_PPM     : {y_density}")
    # print(f"Precision : {precision} bits")
    # print(f"Height    : {height} pixels")
    # print(f"Width     : {width} pixels")
    # print(f"Colors    : {colors}")

    a4_width = 210
    a4_ppm = round(width/a4_width*1000) # PPMを計算
    
    if x_density == a4_ppm and units == 1:
        print(f"DPI: {x_density} == {a4_ppm}")
        return
    print(f"PPM: {x_density} -> {a4_ppm}")
    data = bytearray(data)
    data[chunk_pHYs[0]+8:chunk_pHYs[0]+12] = a4_ppm.to_bytes(4, 'big')
    data[chunk_pHYs[0]+12:chunk_pHYs[0]+16] = a4_ppm.to_bytes(4, 'big')
    data[chunk_pHYs[0]+16] = 1
    
    #新CRC-32書き込み
    chunk_data = data[chunk_pHYs[0]+4:chunk_pHYs[0]+chunk_pHYs[1]+8]
    crc_new = zlib.crc32(chunk_data) & 0xffffffff
    data[chunk_pHYs[0]+chunk_pHYs[1]+8:chunk_pHYs[0]+chunk_pHYs[1]+12] = crc_new.to_bytes(4, 'big')
    
    with open(file_path, 'wb') as f:
        f.write(data)

# ディレクトリ内のすべての png ファイルを変換
import glob
for file_path in glob.glob("*.png"):
    chunks = parse_png(file_path)
    modify_ppm(file_path,chunks["IHDR"],chunks["pHYs"])