212 lines
6.9 KiB
CoffeeScript
212 lines
6.9 KiB
CoffeeScript
# Copyright 2021 Joe Drago. All rights reserved.
|
|
# SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
# READ THIS WHOLE COMMENT FIRST, BEFORE RUNNING THIS SCRIPT:
|
|
|
|
# The goal of this script is to detect AVIFs containing multiple adjacent iref boxes and merge them,
|
|
# filling leftover space with a free box (to avoid ruining file offsets elsewhere in the file). The
|
|
# syntax is simple:
|
|
|
|
# coffee irefmerge.coffee filename.avif
|
|
|
|
# This will look over the file's contents and if it detects multiple irefs, it will fix it in
|
|
# memory, make a adjacent backup of the file (filename.avif.irefmergeBackup), and then overwrite the
|
|
# original file with the fixed contents. Using -v on the commandline will enable Verbose mode, and
|
|
# using -n will disable the creation of backups (.irefmergeBackup files).
|
|
|
|
# This should be well-behaved on files created by old versions of avifenc, but **PLEASE** make
|
|
# backups of your images before running this script on them, **especially** if you plan to run with
|
|
# "-n". I do not advise running this script on AVIFs generated by anything other than avifenc.
|
|
|
|
# Possible responses for a file:
|
|
# * [NotAvif] This file isn't an AVIF.
|
|
# * [BadAvif] This file thinks it is an AVIF, but is missing important things.
|
|
# * [Skipped] This file is an AVIF, but didn't need any fixes.
|
|
# * [Success] This file is an AVIF, had to be fixed, and was fixed.
|
|
# * (the script crashes) I probably have a bug; let me know.
|
|
|
|
# Note on CoffeeScript:
|
|
# If you don't want to invoke coffeescript every time, you can compile it once with:
|
|
# coffee -c -b irefmerge.coffee
|
|
# ... and run the adjacent irefmerge.js with node instead. "It's just JavaScript."
|
|
|
|
# -------------------------------------------------------------------------------------------------
|
|
# Syntax
|
|
|
|
syntax = ->
|
|
console.log "Syntax: irefmerge [-v] [-n] file1 [file2 ...]"
|
|
console.log " -v : Verbose mode"
|
|
console.log " -n : No Backups (Don't generate adjacent .irefmergeBackup files when overwriting in-place)"
|
|
|
|
# -------------------------------------------------------------------------------------------------
|
|
# Constants and helpers
|
|
|
|
fs = require 'fs'
|
|
|
|
INDENT = " "
|
|
VERBOSE = false
|
|
|
|
verboseLog = ->
|
|
if VERBOSE
|
|
console.log.apply(null, arguments)
|
|
|
|
fatalError = (reason) ->
|
|
console.error "ERROR: #{reason}"
|
|
process.exit(1)
|
|
|
|
# -------------------------------------------------------------------------------------------------
|
|
# Box
|
|
|
|
class Box
|
|
constructor: (@filename, @type, @buffer, @start, @size) ->
|
|
@offset = @start
|
|
@bytesLeft = @size
|
|
@version = 0
|
|
@flags = 0
|
|
@boxes = {} # child boxes
|
|
|
|
nextBox: ->
|
|
if @bytesLeft < 8
|
|
return null
|
|
boxSize = @buffer.readUInt32BE(@offset)
|
|
boxType = @buffer.toString('utf8', @offset + 4, @offset + 8)
|
|
if boxSize > @bytesLeft
|
|
verboseLog("#{INDENT} * Truncated box of type #{boxType} (#{boxSize} bytes with only #{@bytesLeft} bytes left)")
|
|
return null
|
|
if boxSize < 8
|
|
verboseLog("#{INDENT} * Bad box size of type #{boxType} (#{boxSize} bytes")
|
|
return null
|
|
newBox = new Box(@filename, boxType, @buffer, @offset + 8, boxSize - 8)
|
|
@offset += boxSize
|
|
@bytesLeft -= boxSize
|
|
verboseLog "#{INDENT} * Discovered box type: #{newBox.type} offset: #{newBox.offset - 8} size: #{newBox.size + 8}"
|
|
return newBox
|
|
|
|
walkBoxes: ->
|
|
while box = @nextBox()
|
|
@boxes[box.type] = box
|
|
return
|
|
|
|
readFullBoxHeader: ->
|
|
if @bytesLeft < 4
|
|
fatalError("#{INDENT} * Truncated FullBox of type #{boxType} (only #{@bytesLeft} bytes left)")
|
|
versionAndFlags = @buffer.readUInt32BE(@offset)
|
|
@version = (versionAndFlags >> 24) & 0xFF
|
|
@flags = versionAndFlags & 0xFFFFFF
|
|
@offset += 4
|
|
@bytesLeft -= 4
|
|
return
|
|
|
|
ftypHasBrand: (brand) ->
|
|
if @type != 'ftyp'
|
|
fatalError("#{INDENT} * Calling Box.ftypHasBrand() on a non-ftyp box")
|
|
majorBrand = @buffer.toString('utf8', @offset, @offset + 4)
|
|
compatibleBrands = []
|
|
compatibleBrandCount = Math.floor((@size - 8) / 4)
|
|
for i in [0...compatibleBrandCount]
|
|
o = @offset + 8 + (i * 4)
|
|
compatibleBrand = @buffer.toString('utf8', o, o + 4)
|
|
compatibleBrands.push compatibleBrand
|
|
|
|
verboseLog "#{INDENT} * ftyp majorBrand: #{majorBrand} compatibleBrands: [#{compatibleBrands.join(', ')}]"
|
|
|
|
if majorBrand == brand
|
|
return true
|
|
for compatibleBrand in compatibleBrands
|
|
if compatibleBrand == brand
|
|
return true
|
|
return false
|
|
|
|
# -------------------------------------------------------------------------------------------------
|
|
# Main
|
|
|
|
irefMerge = (filename, makeBackups) ->
|
|
if not fs.existsSync(filename)
|
|
fatalError("File doesn't exist: #{filename}")
|
|
try
|
|
fileBuffer = fs.readFileSync(filename)
|
|
catch e
|
|
fatalError "Failed to read \"#{filename}\": #{e}"
|
|
|
|
fileBox = new Box(filename, "<file>", fileBuffer, 0, fileBuffer.length)
|
|
fileBox.walkBoxes()
|
|
|
|
ftypBox = fileBox.boxes.ftyp
|
|
if not ftypBox?
|
|
return "NotAvif"
|
|
if ftypBox.type != 'ftyp'
|
|
return "NotAvif"
|
|
if !ftypBox.ftypHasBrand('avif')
|
|
return "NotAvif"
|
|
|
|
metaBox = fileBox.boxes.meta
|
|
if not metaBox?
|
|
return "BadAvif"
|
|
metaBox.readFullBoxHeader()
|
|
|
|
merged = false
|
|
irefs = []
|
|
while box = metaBox.nextBox()
|
|
if box.type == 'iref'
|
|
irefs.push box
|
|
|
|
# console.log irefs
|
|
if irefs.length > 1
|
|
verboseLog "#{INDENT} * Discovered multiple (#{irefs.length}) iref boxes, merging..."
|
|
# merge irefs, and leave a free block in the dead space
|
|
newTotalSize = 8 + 4 # the new single iref header's size + fullbox
|
|
for iref in irefs
|
|
newTotalSize += iref.size - 4
|
|
fileBuffer.writeUInt32BE(newTotalSize, irefs[0].start - 8)
|
|
|
|
writeOffset = irefs[0].start + 4 # skip past the fullbox's version[1]+flags[3]
|
|
for iref in irefs
|
|
fileBuffer.copy(fileBuffer, writeOffset, iref.start + 4, iref.start + iref.size)
|
|
writeOffset += iref.size - 4
|
|
freeBoxSize = (irefs.length - 1) * 12
|
|
freeBox = Buffer.alloc(freeBoxSize)
|
|
freeBox.fill(0)
|
|
freeBox.writeUInt32BE(freeBoxSize)
|
|
freeBox.write("free", 4)
|
|
freeBox.copy(fileBuffer, writeOffset, 0, freeBoxSize)
|
|
verboseLog "#{INDENT} * Wrote a free chunk of size #{freeBoxSize} at offset #{writeOffset}"
|
|
merged = true
|
|
|
|
if merged
|
|
if makeBackups
|
|
backupFilename = filename + ".irefmergeBackup"
|
|
fs.writeFileSync(backupFilename, fs.readFileSync(filename))
|
|
fs.writeFileSync(filename, fileBuffer)
|
|
return "Success"
|
|
return "Skipped"
|
|
|
|
main = ->
|
|
showSyntax = false
|
|
makeBackups = true
|
|
files = []
|
|
|
|
for arg in process.argv.slice(2)
|
|
switch arg
|
|
when '-h', '--help'
|
|
showSyntax = true
|
|
break
|
|
when '-n', '--no-backups'
|
|
makeBackups = false
|
|
break
|
|
when '-v', '--verbose'
|
|
VERBOSE = true
|
|
break
|
|
else
|
|
files.push arg
|
|
|
|
if showSyntax or files.length == 0
|
|
return syntax()
|
|
|
|
for filename in files
|
|
verboseLog("[Reading] #{filename}")
|
|
result = irefMerge(filename, makeBackups)
|
|
console.log("[#{result}] #{filename}") # Always print this
|
|
|
|
return 0
|
|
|
|
main()
|