Skip to content

Instantly share code, notes, and snippets.

@HikerBoricua
Forked from bcyrill/fix_eoi.py
Last active July 25, 2021 21:25
Show Gist options
  • Save HikerBoricua/54aec42ee47f2ecb374ec3208b3a98ee to your computer and use it in GitHub Desktop.
Save HikerBoricua/54aec42ee47f2ecb374ec3208b3a98ee to your computer and use it in GitHub Desktop.
Adds peripheral code to make it easier to work with a folder/directory containing mixed content. Tested with S8+ (trailer versions 103, 105 & 106). Coded for Python 3.6.5 64 bit.
#!/usr/bin/python
# The Samsung trailer format is based on the implementation from ExifTool
# http://www.sno.phy.queensu.ca/~phil/exiftool/
# Forked off https://gist.github.com/bcyrill to fit a specific use case,
# bad pano files mixed with other files in a folder/dir
# The "dump" argument was not tested in this fork
# Meant to be run from a batch like the Windows CMD one in the block comment below
"""
echo off
for %%A in (*.jpg) do python fix_eoi.py "%%A"
echo If everything OK, press any key to delete all *_orig.jpg files
set /p answer="Otherwise close this window to prevent loss of originals ..."
del *_orig.jpg
echo on
"""
import mmap
import struct
import os
import sys
import shutil
if (len(sys.argv) < 2) or (len(sys.argv) > 3):
print("Usage: fix_eoi.py <filename> [dump]")
exit()
file_fullname = sys.argv[1]
if len(sys.argv) < 3:
dump = 0
elif sys.argv[2] == "dump":
dump = 1
else:
print("Invalid argument")
print("Usage: fix_eoi.py <filename> [dump]")
exit()
(file_name, file_ext) = os.path.splitext(file_fullname)
file_basename = os.path.basename(file_fullname)
if file_ext != '.jpg':
print(file_basename + ': Doesn\'t have Samsung panoramas standard .jpg extension')
exit()
with open(file_fullname, 'rb') as fh:
m = mmap.mmap(fh.fileno(), 0, access=mmap.ACCESS_READ)
b = bytearray(m)
trailer_tail = b[-4:]
if trailer_tail != bytearray(b'SEFT'):
print('{}: Expected Samsung (b\'SEFT\') trailer but found {}'.format(file_basename, trailer_tail))
exit()
# else:
# print("Found SEFT")
length = struct.unpack_from("<I",b[-8:-4])[0]
trailer = b[-(8+length):]
endPos = len(b)
dirPos = endPos-(8+length)
if trailer[0:4] != bytearray(b'SEFH'):
print('{}: Expected Samsung (b\'SEFH\') trailer but found {}'.format(file_basename, trailer[0:4]))
exit()
# else:
# print("Found SEFH")
version = struct.unpack_from("<I",trailer[4:8])[0]
if version not in set([101, 103, 105, 106]):
print('{}: Expected Samsung trailer version in [101,103,105,106] but found {}'.format(file_basename, version))
exit()
count = struct.unpack_from("<I",trailer[8:12])[0]
firstBlock = 0
is_pano = 0
for index in range(0, count):
entry = 12 + 12 * index;
type = struct.unpack_from("<H",trailer[entry+2:entry+4])[0]
noff = struct.unpack_from("<I",trailer[entry+4:entry+8])[0]
size = struct.unpack_from("<I",trailer[entry+8:entry+12])[0]
if firstBlock < noff:
firstBlock = noff
entryPos = dirPos - noff
entryLen = size
data = b[entryPos:entryPos+entryLen]
# Validate as the type has to match the SEFH/SEFT entry type
entry_type = struct.unpack_from("<H",data[2:4])[0]
if type != entry_type:
print(file_basename + ': Block type '+ type + ' doesn\'t match entry type ' + entry_type)
exit()
entry_offset = struct.unpack_from("<I",data[4:8])[0]
entry_name = data[8:8+entry_offset].decode("utf-8")
if entry_name == "Panorama_Shot_Info":
is_pano = 1
entry_data = data[8+entry_offset:]
if dump:
print("Dumping: %s" % entry_name)
with open(file_name + '_' + entry_name, 'wb') as f:
f.write(entry_data)
if (not is_pano) and (version < 106): #S8+ stopped adding this entry with trailer v106
print(file_basename + ': Not a Samsung panorama')
exit()
dataPos = dirPos - firstBlock
dirLen = endPos - dataPos
eoi = struct.unpack_from(">H",b[dataPos-2:dataPos])[0]
if eoi == 0xffd9:
print(file_basename + ': Already has EOI, not a defective Samsung panorama')
exit()
else:
print(file_basename + ': Inserting EOI to correct defective Samsung panorama')
with open(file_name + '_fix_eoi.jpg', 'wb') as f:
f.write(b[0:dataPos])
f.write(bytearray(b'\xff\xd9'))
f.write(b[dataPos:])
m.close()
fh.close()
shutil.move(file_fullname, file_name + '_orig.jpg')
@HikerBoricua
Copy link
Author

Beautiful spot!
Are you sure you're using the version above? It's relatively new (10 days) since the problem didn't hit until a recent Samsung "update." If you are and the problem is still there then let's try this again, but next time upload the picture with a dummy extension. GitHub may have modified the .jpg because it isn't missing the EOI.
BTW, this last move by Samsung was the last straw for me. I'm going to look for a different camera app that doesn't have this bug. I.e. don't expect new versions from here!

@BavYeti
Copy link

BavYeti commented Sep 20, 2020

Forked to add recursive folder search on the python level and (force) overwrite original file
https://gist.github.com/BavYeti/

@frapa
Copy link

frapa commented Jul 25, 2021

I made a rust version which can strip the files of the videos and reduce the size in about half. This version merely makes the file a valid JPEG file but the video remains in there. I also made some binaries if somebody is interested: https://github.com/frapa/sampan

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment