feat: support for external image references

This commit is contained in:
relikd
2025-06-09 00:20:14 +02:00
parent d151863706
commit 7804801297
3 changed files with 78 additions and 21 deletions

View File

@@ -35,9 +35,8 @@ python3 vcard2image.py AllContacts.vcf ./profile_pics/
### Limitations
The `image` field currently only supports JPG images.
I have honestly no idea where PNG images are stored.
For PNGs the database only stores a UUID instead of the file itself.
If you happen to know where I can find these, open an issue or pull request.
But as far as I see, Apple converts PNG to JPG before storing the image.
If you encounter a db which includes other image types, please let me know.
### Disclaimer

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python3
import os
import re
import sys
import sqlite3
@@ -57,7 +58,10 @@ def sanitize(cursor: sqlite3.Cursor, query: str) -> str:
all_cols = {x[1] for x in cursor.execute(f'PRAGMA table_info({table});')}
missing_cols = sel_cols.difference(all_cols)
for missing in missing_cols:
print(f'WARN: column "{missing}" not found in {table}. Ignoring.',
if missing == 'ZSERVICENAME':
pass # ignore irrelevant fields
else:
print(f'[WARN] Column "{missing}" not found in {table}. Ignoring.',
file=sys.stderr)
query = query.replace(missing, 'NULL')
return query
@@ -413,29 +417,55 @@ class Record:
optionalArray(self.service)
if self.image:
try:
data.append(self.imageAsBase64(self.image))
except NotImplementedError:
print('''Image format not supported.
Could not extract image for contact: {}
@: {!r}...
skipping.'''.format(self.fullname, self.image[:20]), file=sys.stderr)
data.append(self.imageAsBase64())
if self.iscompany:
data.append('X-ABShowAs:COMPANY')
data.append('END:VCARD')
return '\r\n'.join(data) + '\r\n'
def imageAsBase64(self, image: bytes) -> str:
img = image[1:] # why does Apple prepend \x01 to all images?!
def imageAsBase64(self) -> str:
if not self.image:
return '' # already checked before call, never happens
t = 'PHOTO;ENCODING=b;TYPE='
if img[6:10] == b'JFIF':
t += 'JPEG:' + b64encode(img).decode('ascii')
else:
raise NotImplementedError(
'Image types other than JPEG are not supported yet.')
if self.image[6:10] == b'JFIF':
t += 'JPEG:' + b64encode(self.image).decode('ascii')
# place 'P' manually for nice 75 char alignment
return t[0] + '\r\n '.join(t[i:i + 74] for i in range(1, len(t), 74))
def imagePreprocess(self, basePath: str) -> None:
# Assumption: Apple uses the first character to determine storage type
# \x01: embedded image
# \x02: external reference
if not self.image:
return # no image exists, nothing to do
if self.image[0] == 1: # loaded into memory
self.image = self.image[1:] # remove storage type indicator
elif self.image[0] == 2: # external referenced image
# for whatever reason this is null-terminated
imgName = self.image[1:].rstrip(b'\x00').decode('ascii')
imgPath = os.path.join(basePath, imgName)
if os.path.isfile(imgPath):
with open(imgPath, 'rb') as fp:
self.image = fp.read()
else:
self.image = None
raise FileNotFoundError(
f'Image reference not found: {imgPath}')
else:
raise NotImplementedError(
'Unexpected image data[{}]: {!r}'.format(
len(self.image), self.image[:20] + b'...'))
# by now loaded into memory either way
if self.image[6:10] != b'JFIF':
self.image = None
# We could convert to JPEG but I don't like to introduce a
# dependecy on Pillow solely for this use-case.
# Should never trigger because Apple converts to JPEG anyway.
raise NotImplementedError('Only JPEG images are supported.')
# ===============================
# Main Entry
@@ -454,7 +484,8 @@ class ABCDDB:
if not rec:
rec = Record.initEmpty(attr.parent)
records[attr.parent] = rec
print('Found unreferenced data field:', attr, file=sys.stderr)
print('[WARN] Found unreferenced data field:', attr,
file=sys.stderr)
return rec
# query once, then distribute
@@ -480,4 +511,31 @@ class ABCDDB:
_getOrMake(service).service.append(service)
db.close()
# support for externally referenced image files
# relative to abcddb file: ".AddressBook-v22_SUPPORT/_EXTERNAL_DATA"
dbBaseDir = os.path.dirname(os.path.abspath(db_path))
dbFilename = os.path.basename(db_path)
hiddenMediaDir = f'.{os.path.splitext(dbFilename)[0]}_SUPPORT'
extImgDir = os.path.join(dbBaseDir, hiddenMediaDir, '_EXTERNAL_DATA')
if not os.path.isfile(db_path + '-wal'):
print(f'[WARN] "{dbFilename}-wal" not found.',
'Both (-wal & -shm) may store recent changes.',
'Data could be incomplete.',
file=sys.stderr)
if not os.path.isdir(extImgDir):
print(f'[WARN] Hidden folder "{hiddenMediaDir}" is missing.',
'Some images may not be exported (warnings below).',
file=sys.stderr)
for rec in records.values():
try:
rec.imagePreprocess(extImgDir)
except Exception as e:
print('''Could not extract image for contact: {}
reason: {}
skipping.'''.format(rec.fullname, e), file=sys.stderr)
return list(records.values())

View File

@@ -2,6 +2,6 @@
'''
Convert AddressBook database (.abcddb) to Contacts VCards file (.vcf)
'''
__version__ = '1.1.1'
__version__ = '1.2.0'
from .ABCDDB import ABCDDB