Move animated_gif / animated_png autotagging to take place during uploading, instead of during tag editing. We can't generally assume the file will be present on the local filesystem after uploading.
113 lines
3.0 KiB
Ruby
113 lines
3.0 KiB
Ruby
class APNGInspector
|
|
attr_reader :frames
|
|
|
|
PNG_MAGIC_NUMBER = ["89504E470D0A1A0A"].pack('H*')
|
|
|
|
def initialize(file_path)
|
|
@file_path = file_path
|
|
@corrupted = false
|
|
@animated = false
|
|
end
|
|
|
|
#PNG file consists of 8-byte magic number, followed by arbitrary number of chunks
|
|
#Each chunk has the following structure:
|
|
#4-byte length (unsigned int, can be zero)
|
|
#4-byte name (ASCII string consisting of letters A-z)
|
|
#(length)-byte data
|
|
#4-byte CRC
|
|
#
|
|
#Any data after chunk named IEND is irrelevant
|
|
#APNG frame count is inside a chunk named acTL, in first 4 bytes of data.
|
|
|
|
|
|
#This function calls associated block for each PNG chunk
|
|
#parameters passed are |chunk_name, chunk_length, file_descriptor|
|
|
#returns true if file is read succesfully from start to IEND,
|
|
#or if 100 000 chunks are read; returns false otherwise.
|
|
def each_chunk
|
|
iend_reached = false
|
|
File.open(@file_path, 'rb') do |file|
|
|
#check if file is not PNG at all
|
|
return false if file.read(8) != PNG_MAGIC_NUMBER
|
|
|
|
chunks = 0
|
|
|
|
#We could be dealing with large number of chunks,
|
|
#so the code should be optimized to create as few objects as possible.
|
|
#All literal strings are frozen and read() function uses string buffer.
|
|
chunkheader = ''
|
|
while file.read(8, chunkheader)
|
|
#ensure that first 8 bytes from chunk were read properly
|
|
if chunkheader == nil || chunkheader.length < 8
|
|
return false
|
|
end
|
|
|
|
current_pos = file.tell
|
|
|
|
chunk_len, chunk_name = chunkheader.unpack("Na4".freeze)
|
|
return false if chunk_name =~ /[^A-Za-z]/
|
|
yield chunk_name, chunk_len, file
|
|
|
|
#no need to read further if IEND is reached
|
|
if chunk_name == "IEND".freeze
|
|
iend_reached = true
|
|
break
|
|
end
|
|
|
|
#check if we processed too many chunks already
|
|
#if we did, file is probably maliciously formed
|
|
#fail gracefully without marking the file as corrupt
|
|
chunks += 1
|
|
if chunks > 100000
|
|
iend_reached = true
|
|
break
|
|
end
|
|
|
|
#jump to the next chunk - go forward by chunk length + 4 bytes CRC
|
|
file.seek(current_pos+chunk_len+4, IO::SEEK_SET)
|
|
end
|
|
end
|
|
return iend_reached
|
|
end
|
|
|
|
def inspect!
|
|
actl_corrupted = false
|
|
|
|
read_success = each_chunk do |name, len, file|
|
|
if name == 'acTL'.freeze
|
|
framecount = parse_actl(len, file)
|
|
if framecount < 1
|
|
actl_corrupted = true
|
|
else
|
|
@animated = true
|
|
@frames = framecount
|
|
end
|
|
end
|
|
end
|
|
|
|
@corrupted = !read_success || actl_corrupted
|
|
self
|
|
end
|
|
|
|
def corrupted?
|
|
@corrupted
|
|
end
|
|
|
|
def animated?
|
|
!@corrupted && @animated
|
|
end
|
|
|
|
private
|
|
|
|
#return number of frames in acTL or -1 on failure
|
|
def parse_actl(len, file)
|
|
return -1 if len != 8
|
|
framedata = file.read(4)
|
|
if framedata == nil || framedata.length != 4
|
|
return -1
|
|
end
|
|
return framedata.unpack("N".freeze)[0]
|
|
end
|
|
|
|
end
|