探索qq發送png透明通道丟失的問題

我在qq發送圖片的時候注意到有一些圖片發出去能保持透明通道,就像左圖這樣,而右圖就慘不忍睹了,透明通道全成黑色了

那麼到底是什麼原因呢,我將第一幅圖用

from PIL import Image

img  = Image.open("1.png")#打開文件
img.save("2.png")

也就是另存爲生成了一個帶有Alpha通道的2.png,他們發送到qq的效果如上所示

 

他們相差0.01MB

於是我找他們的不同,先比較十行

first = open("1.png", "rb")#byte讀取數據
second = open("2.png", "rb")

line=first.read(10)#讀10byte
sline=second.read(10)
#方案二byte對齊
row=1
while(line):
    if line!=sline:#如果不一樣
        print(row)#顯示行號
        print(line)#顯示1.png的byte數據
        print(sline)#顯示2.png的byte數據,換行是爲了好看,方便對齊
    line=first.read(10)#讀下一行
    sline=second.read(10)
    row+=1#行號標記+1
    if row==10:#讀到9行的時候退出,不讀完,先比較一部分
        exit()
while(sline):
    print(row,sline)
    line=first.read(10)
    sline=second.read(10)
    row+=1
exit()
'''
#方案一行對齊
row=1
line=first.readline()
sline=second.readline()
while(line):
    if line!=sline:
        print(row)
        print(line)
        print(sline)
    line=first.readline()
    sline=second.readline()
    if row==10:
        exit()
    row+=1
while(sline):
    print(row,sline)
    line=first.readline()
    sline=second.readline()
    row+=1
'''

結果

發現4,5,6,7,8,9都不一樣,如果對比更多就是滿屏幕了,於是放棄這個想法

那麼我們可以通過讀png的編碼來了解他們的不同,於是去找png的規範寫了個讀取png格式的python程序

import binascii
import sys
first = open("1.png", "rb")#以二進制byte數據讀取png文件
#讀取文件類型
head=first.read(8)#讀取文件的開頭八個字節
print("格式",head[0:8],head[1:4])#0-7,8 輸出檢查文件類型
'''
#直接讀文件的頭
head=first.read(32)
print("head",head)
print("格式",head[0:8],head[1:4])#0-7,8
print("Chunk長度",head[8:12],int.from_bytes(head[8:12], byteorder='big'))#8-11,4
print("數據塊類型碼",head[12:16])#12-15,4
print("數據塊數據",head[16:16+int.from_bytes(head[8:12], byteorder='big')])#16-28,13
print("  寬",head[16:16+4],int.from_bytes(head[16:20], byteorder='big'))#16-19,4
print("  高",head[20:20+4],int.from_bytes(head[20:24], byteorder='big'))#20-23,4
print("  圖像深度",head[24],head[24])#24,1
print("  顏色類型",head[25],head[25])#25,1
print("  壓縮方法",head[26],head[26])#26,1
print("  濾波器方法",head[27],head[27])#27,1
print("  掃描方法",head[28],head[28])#28,1
print("CRC校驗碼",head[29:33],int.from_bytes(head[29:33], byteorder='big'),)#29-32,4
print()
'''
#用作重定向標準輸出流
class stroutput:
    
    def __init__(self,blend=1):#默認綁定輸出流
        self.buff=''
        self.__console__=sys.stdout
        if blend==1:
            self.blend()
            
    def blend(self):#綁定方法
        sys.stdout=self
        
    def write(self, output_stream):#構造write方法
        self.buff+=output_stream
        
    def to_console(self):#輸出到控制檯方法
        sys.stdout=self.__console__
        print(self.buff)
        sys.stdout=self
    
    def to_file(self, file_path):#輸出到文件方法
        f=open(file_path,'w')
        sys.stdout=f
        print(self.buff)
        f.close()
    
    def flush(self):#構造flush方法
        self.buff=''
        
    def close(self):#關閉還原輸出流方法
        sys.buff=""
        sys.stdout=self.__console__
        
    def getString(self):#返回當前流內容
        return self.buff
        
    def getSize(self):#返回當前流大小
        return len(self.buff)
        
    def printSize(self):#打印流大小
        sys.stdout=self.__console__
        print(len(self.buff))
        sys.stdout=self
    def print(self,string):#提供print方法
        sys.stdout=sys.__stdout__
        print(string)
        sys.stdout=self
        
#chunk解碼
def Chunkdecode(Chunktype,Chunklength,showtype,showdata,showdatadetail,readonly):
    showtypekey="".join(showtype)#將所有要輸出的類型合併成字符串
    if readonly==0:
        print(end="")
        #保留調試位置
    if Chunktype==b"IHDR":#如果是IHDR塊文件頭數據,按照語法讀
        data=first.read(Chunklength)#按照長度byte的數值讀取數據
        if showtypekey.find("IHDR")!=-1:#如果是IHDR數據解析IHDR數據
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  寬",data[0:4],int.from_bytes(data[0:4], byteorder='big'))
                print("  高",data[4:8],int.from_bytes(data[4:8], byteorder='big'))
                print("  圖像深度",data[8],data[8])
                print("  顏色類型",data[9],data[9])
                print("  壓縮方法",data[10],data[10])
                print("  濾波器方法",data[11],data[11])
                print("  掃描方法",data[12],data[12])
        return data#返回數據供生成校驗字符
    if Chunktype==b"IDAT":#圖像數據塊
        data=first.read(Chunklength)
        if showtypekey.find("IDAT")!=-1: 
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"IEND":#圖像結束數據
        data=first.read(Chunklength)
        if showtypekey.find("IEND")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"sBIT":#樣本有效位數據塊
        data=first.read(Chunklength)
        if showtypekey.find("sBIT")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"cHRM":#基色和白色點數據塊
        data=first.read(Chunklength)
        if showtypekey.find("cHRM")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"gAMA":#圖像Y數據塊
        data=first.read(Chunklength)
        if showtypekey.find("gAMA")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"PLTE":#調色板數據塊
        data=first.read(Chunklength)
        if showtypekey.find("PLTE")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"bKGD":#背景顏色數據塊
        data=first.read(Chunklength)
        if showtypekey.find("bKGD")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"hIST":#圖像直方圖數據塊
        data=first.read(Chunklength)
        if showtypekey.find("hIST")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"tRNS":#圖像透明數據塊
        data=first.read(Chunklength)
        if showtypekey.find("tRNS")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"oFFs":#(專用公共數據塊)
        data=first.read(Chunklength)
        if showtypekey.find("oFFs")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"pHYs":#物理像素尺寸數據塊
        data=first.read(Chunklength)
        if showtypekey.find("pHYs")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"sCAL":#(專用公共數據塊)
        data=first.read(Chunklength)
        if showtypekey.find("sCAL")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"tIME":#	圖像最後修改時間數據塊
        data=first.read(Chunklength)
        if showtypekey.find("tIME")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"tEXt":#文本信息數據塊
        data=first.read(Chunklength)
        if showtypekey.find("tEXt")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"zTXt":#壓縮文本數據塊
        data=first.read(Chunklength)
        if showtypekey.find("zTXt")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"fRAc":#(專用公共數據塊)
        data=first.read(Chunklength)
        if showtypekey.find("fRAc")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"gIFg":#(專用公共數據塊)
        data=first.read(Chunklength)
        if showtypekey.find("gIFg")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"gIFt":#(專用公共數據塊)
        data=first.read(Chunklength)
        if showtypekey.find("gIFt")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
    if Chunktype==b"gIFx":#(專用公共數據塊)
        data=first.read(Chunklength)
        if showtypekey.find("gIFx")!=-1:
            if showdata!=0: print("數據塊數據",data[0:0+Chunklength])
            if showdatadetail!=0:
                print("  大小",len(data))
        return data
        
#讀塊函數,默認不輸出任何東西,只有參數不爲0的輸出
def readChunk(readonly=0,showtype=["IHDR","IDAT","IEND","sBIT"],showChuncktype=0,showdata=0,Changeline=0,showdatadetail=0,checkCRC=0,showlength=0):
    output=stroutput()
    Chunklengthbyte=first.read(4)#讀取長度比特
    Chunklength=int.from_bytes(Chunklengthbyte, byteorder='big')#轉化長度比特爲int型
    if Chunklength==0:#檢查Chunk塊是不是讀取完畢
        print("png文件讀取完了")
        return False#結尾
    if showlength!=0:print("Chunk長度",Chunklengthbyte,Chunklength)#如果帶有showlength參數,且不爲0則打印
    Chunktype=first.read(4)#從文件讀取4byte的數據作爲Chunk類型
    if ((showChuncktype!=0) and ("".join(showtype).find(str(Chunktype,"utf-8"))!=-1)):print("數據塊類型碼",str(Chunktype))#判斷是否是要輸出的chunk塊
    CRCdata=Chunktype+Chunkdecode(Chunktype,Chunklength,showtype,showdata,showdatadetail,readonly)#執行分類解碼函數,並與chunk類型合成數據塊字符串
    CRC=first.read(4)#讀取CRC32的校驗碼
    if checkCRC!=0:#如果需要檢查校驗碼
        CRCcalc=binascii.crc32(CRCdata)#通過數據塊字符串算出CRC數值
        CRCint=int.from_bytes(CRC, byteorder='big')#轉化數據中的CRC
        print("CRC校驗碼",CRC,CRCint,CRCint==CRCcalc)#進行對比
    if Changeline!=0 and readonly==0 and output.getSize()!=0: output.to_console()#是否換行,如果輸出流裏沒輸出那麼拋棄
    output.close()
    return True#執行成功返回

def beginRead(showline=0,begin=1,end=0,**kwargs):#啓動函數(是否顯示行號,開始位置,結束位置)**kwargs是用來打通嵌套函數的,可以映射子函數的參數到主函數
    key=True#因爲python不支持,或者我不知道的空while,暫時用這個來代替
    line=1#行數統計
    if showline>0:#空間換時間,把while裏的if提出來進行分支,是否顯示行號
        while(key):#如果還有數據塊那麼接着讀取
            if line>=begin and (end==0 or line<=end):#判斷是否啓用區間參數,並檢查是不是在輸出區間begin和end之間
                output=stroutput()#劫持輸出流,目的是做一個攔截器,攔截多餘的空行,而不是用全局變量
                print(str(line)+".",end="")#打印行號,不換行
                key=readChunk(**kwargs)#傳遞參數
                if output.getSize()!=(len(str(line))+1):
                   output.to_console()
                output.close()
            else:
                key=readChunk(readonly=1)#僅僅按格式空讀取文件,什麼也不輸出,readonly是用來進行函數的區分和調試的
            line+=1
    else:#不顯示行號
        while(key):
            if line>=begin and (end==0 or line<=end):#判斷是否啓用區間參數,並檢查是不是在輸出區間begin和end之間
                key=readChunk(**kwargs)#傳遞參數
            else:
                key=readChunk(readonly=1)#僅僅按格式空讀取文件,什麼也不輸出,readonly是用來進行函數的區分和調試的
            line+=1
        
beginRead(showline=1,begin=1,end=50,Changeline=1,showChuncktype=1,showdatadetail=1,showtype=["IHDR","sBIT"],showdata=1)#啓動讀取,showChuncktype等參數定義在readChunk子函數中
#顯示行號,從1行開始,讀50行,塊與塊之間空行,顯示塊類型,顯示塊細節,顯示IHDR和sBIT類型

進行對比,程序我就不說了,我都有詳細註釋,使用參數

beginRead(showline=1,begin=1,end=2,Changeline=1,showChuncktype=1,showdatadetail=1),結果是

通過對比,知道了兩個文件間有什麼不同,編碼的數據塊不一樣。具體的png數據塊介紹給個傳送門點這裏,說的挺詳細的了。就是8字節的頭表示格式,也就是上圖的第一行,左邊是8字節的byte數據,右邊是抽取出來的文件類型。然後就是文件頭數據塊(數據塊有很多種,統稱chunk數據塊),這個肯定要在前面的,它包括了圖片的一些信息,具體的解碼已將在上圖了,標註b的是直接從文件裏讀出來的byte數據,沒有b標註的是已經轉換成int型的數據。

通過對比發現這兩個圖確實是不一樣的,但爲什麼他會失真呢,其實按道理來說帶有alpha通道的除非經過處理,不然是不會發生改變的。那麼爲什麼它的透明通道會丟失呢,原因肯定在騰訊對圖片進行了壓縮處理。我們通過qq點擊發送圖片的圖片右鍵另存爲

發現他已經嚴重變形了,甚至成了jpg

但是1.png爲什麼能保持原來的樣子,那肯定是因爲有緩存,我們去找找

我進入我的qq文檔下使用windows的查找什麼都沒找到,不知道什麼問題,還好裝了git,使用git bash

$ find . -name "*.png" -size +1M -size -5M

查找當前文件夾下的png後綴的在1M到3M之間的文件

找到了一個可惜不是它,好的野蠻搜索

$ find /c -name "*.png" -size +1M -size -5M

找到了

刪除掉temp後打開羣聊找到那張圖纔會真正下載,出現在D:\My Documents\Tencent Files\1806620741\Image\Group下就能找到圖片了,將其用其他照片替換掉我們在qq雙擊打開的就是我們替換的圖片,到此確定這個緩存

用對比程序運行看看

first = open("1.png", "rb")#byte讀取數據
second = open("LTKU7DO6N8WM8)Q1~S36{]D (2).png", "rb")
 
line=first.read(10)#讀10byte
sline=second.read(10)
#方案二byte對齊
row=1
while(line):
    if line!=sline:#如果不一樣
        print(row)#顯示行號
        print(line)#顯示1.png的byte數據
        print(sline)#顯示2.png的byte數據,換行是爲了好看,方便對齊
    line=first.read(10)#讀下一行
    sline=second.read(10)
    row+=1#行號標記+1
while(sline):
    print(row,sline)
    line=first.read(10)
    sline=second.read(10)
    row+=1

其實我已經完全能確定就是一張圖了,使用md5驗證也是可以的,只是md5不能找出不同,結果很正確

沒有任何不同

我們用2.png替換他,看看透明通道改變麼,注意1.png和2.png塊編碼方式是不一樣的,雙擊打開,如上圖所示,已經被替換了

透明通道完好,說明無關塊編碼方式,而是在騰訊服務器有1.png的原圖,而我上傳的2.png一直在被壓縮,電腦傳不了原圖,我們再拿手機試試,原圖發送

2.png完全沒問題,至此水落石出,只有手機qq才能傳高清大圖,電腦qq要傳就要傳文件或者壓縮包

細心的朋友可能就知道平時qq羣裏的圖片點開又是另一張圖,改動很大,就是因爲這個壓縮簡略圖的機制,上傳的png會被壓縮成jpg丟失透明通道,所以簡略圖.jpg一看過去是一張圖片但是打開後是帶透明通道的.png,很多信息就被隱藏起來了,所以纔有兩個不同的圖像疊在一幅圖上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章