-
-
Save HikerBoricua/54aec42ee47f2ecb374ec3208b3a98ee to your computer and use it in GitHub Desktop.
#!/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') |
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!
Forked to add recursive folder search on the python level and (force) overwrite original file
https://gist.github.com/BavYeti/
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
Hello HikerBoricua,
I tried your script to edit my Samsung S8 panoramic picture, so they can be opened with usual jpeg picture editors like Lightroom.
When I run your script, it tells me, that the panos are not from a Samsung device.
Could you have a look into that?
I'd be very grateful, because your script helped me in the past very much.
I have attached a sample picture. Thank you in advance.