Read Olympus(r) image files (OIF and OIB).
Copyright (c) 2012-2020, Christoph Gohlke
Oiffile is a Python library to read image and metadata from Olympus Image
Format files. OIF is the native file format of the Olympus FluoView(tm)
software for confocal microscopy.
There are two variants of the format:
-
OIF (Olympus Image File) is a multi-file format that includes a main setting
file (.oif) and an associated directory with data and setting files (.tif,
.bmp, .txt, .pyt, .roi, and .lut).
-
OIB (Olympus Image Binary) is a compound document file, storing OIF and
associated files within a single file.
Author:
Christoph Gohlke <https://www.lfd.uci.edu/~gohlke/>
_
Organization:
Laboratory for Fluorescence Dynamics. University of California, Irvine
License: BSD 3-Clause
Version: 2020.1.18
Requirements
CPython >= 3.6 <https://www.python.org>
_
Numpy 1.14 <https://www.numpy.org>
_
Tifffile 2019.1.1 <https://pypi.org/project/tifffile/>
_
Revisions
2020.1.18
Fix indentation error.
2020.1.1
Support multiple image series.
Parse shape and dtype from settings file.
Remove support for Python 2.7 and 3.5.
Update copyright.
Notes
The API is not stable yet and might change between revisions.
No specification document is available.
Tested only with files produced on Olympus FV1000 hardware.
Read the image from an OIB file as numpy array:
>>> image = OibImread('test.oib')
>>> image.shape
(3, 256, 256)
>>> image[:, 95, 216]
array([820, 50, 436], dtype=uint16)
Read the image from a single TIFF file in an OIB file:
>>> with OifFile('test.oib') as oib:
... filename = natural_sorted(oib.glob('*.tif'))[0]
... image = oib.asarray(filename)
>>> filename
'Storage00001/s_C001.tif'
>>> image[95, 216]
820
Access metadata and the OIB main file:
>>> with OifFile('test.oib') as oib:
... oib.axes
... oib.shape
... oib.dtype
... dataname = oib.mainfile['File Info']['DataName']
'CYX'
(3, 256, 256)
dtype('uint16')
>>> dataname
'Cell 1 mitoEGFP.oib'
Extract the OIB file content to an OIF file and associated data directory:
>>> tempdir = tempfile.mkdtemp()
>>> oib2oif('test.oib', location=tempdir)
Saving ... done.
Read the image from the extracted OIF file:
>>> image = imread(f'{tempdir}/{dataname[:-4]}.oif')
>>> image[:, 95, 216]
array([820, 50, 436], dtype=uint16)
Read OLE compound file and access the 'OibInfo.txt' settings file:
>>> with CompoundFile('test.oib') as com:
... info = com.open_file('OibInfo.txt')
... len(com.files())
14
>>> info = SettingsFile(info, 'OibInfo.txt')
>>> info['OibSaveInfo']['Version']
'2.0.0.0'
CompoundFile
Compound Document File.
A partial implementation of the "[MS-CFB] - v20120705, Compound File
Binary File Format" specification by Microsoft Corporation.
This should be able to read Olympus OIB and Zeiss ZVI files.
Initialize instance from file.
Source code in src/domb/utils/oiffile.py
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682 | def __init__(self, filename):
"""Initialize instance from file."""
self._fh = open(filename, 'rb')
if self._fh.read(8) != b'\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1':
self.close()
raise ValueError('not a compound document file')
(
self.clsid,
self.version_minor,
self.version_major,
byteorder,
sector_shift,
mini_sector_shift,
_,
_,
self.dir_len,
self.fat_len,
self.dir_start,
_,
self.mini_stream_cutof_size,
self.minifat_start,
self.minifat_len,
self.difat_start,
self.difat_len,
) = struct.unpack('<16sHHHHHHIIIIIIIIII', self._fh.read(68))
self.byteorder = {0xFFFE: '<', 0xFEFF: '>'}[byteorder]
if self.byteorder != '<':
raise NotImplementedError('big-endian byte order not supported')
if self.clsid == b'\x00' * 16:
self.clsid = None
if self.clsid is not None:
raise OifFileError(f'cannot handle clsid {self.clsid}')
if self.version_minor != 0x3E:
raise OifFileError(
f'cannot handle version_minor {self.version_minor}')
if mini_sector_shift != 0x0006:
raise OifFileError(
f'cannot handle mini_sector_shift {mini_sector_shift}')
if not (
(self.version_major == 0x4 and sector_shift == 0x000C) or
(self.version_major == 0x3 and sector_shift == 0x0009 and
self.dir_len == 0)
):
raise OifFileError(
f'cannot handle version_major {self.version_major} '
f'and sector_shift {sector_shift}'
)
self.filename = filename
self.sec_size = 2**sector_shift
self.short_sec_size = 2**mini_sector_shift
secfmt = '<' + ('I' * (self.sec_size // 4))
# read DIFAT
self._difat = list(
struct.unpack('<' + ('I' * 109), self._fh.read(436))
)
nextsec = self.difat_start
for i in range(self.difat_len):
if nextsec >= CompoundFile.MAXREGSID:
raise OifFileError('nextsec >= CompoundFile.MAXREGSID')
sec = struct.unpack(secfmt, self._sec_read(nextsec))
self._difat.extend(sec[:-1])
nextsec = sec[-1]
# if nextsec != CompoundFile.ENDOFCHAIN: raise OifFileError()
self._difat = self._difat[:self.fat_len]
# read FAT
self._fat = []
for secid in self._difat:
self._fat.extend(struct.unpack(secfmt, self._sec_read(secid)))
# read mini FAT
self._minifat = []
for i, sector in enumerate(self._sec_chain(self.minifat_start)):
if i >= self.minifat_len:
break
self._minifat.extend(struct.unpack(secfmt, sector))
# read directories
self._dirs = []
for sector in self._sec_chain(self.dir_start):
for i in range(0, self.sec_size, 128):
direntry = DirectoryEntry(sector[i:i+128], self.version_major)
self._dirs.append(direntry)
# read root storage
if len(self._dirs) <= 0:
raise OifFileError('no directories found')
root = self._dirs[0]
if root.name != 'Root Entry':
raise OifFileError(
f"no root directory found, got {root.name}")
if root.create_time is not None: # and root.modify_time is None
raise OifFileError(f'invalid root.create_time {root.create_time}')
if root.stream_size % self.short_sec_size != 0:
raise OifFileError(
f'root.stream_size {root.stream_size} does not match '
f'short_sec_size {self.short_sec_size}'
)
# read mini stream
self._ministream = b''.join(self._sec_chain(root.sector_start))
self._ministream = self._ministream[:root.stream_size]
# map stream/file names to directory entries
nostream = CompoundFile.NOSTREAM
join = '/'.join # os.path.sep.join
dirs = self._dirs
visited = [False] * len(self._dirs)
def parse(dirid, path):
# return iterator over all file names and their directory entries
# TODO: replace with red-black tree parser
if dirid == nostream or visited[dirid]:
return
visited[dirid] = True
de = dirs[dirid]
if de.is_stream:
yield join(path + [de.name]), de
yield from parse(de.left_sibling_id, path)
yield from parse(de.right_sibling_id, path)
if de.is_storage:
yield from parse(de.child_id, path + [de.name])
self._files = dict(parse(self._dirs[0].child_id, []))
|
__str__
Return string with information about CompoundFile.
src/domb/utils/oiffile.py
742
743
744
745
746
747
748
749
750
751
752
753
754 | def __str__(self):
"""Return string with information about CompoundFile."""
s = [
self.__class__.__name__,
os.path.normpath(os.path.normcase(self.filename)),
]
for attr in (
'clsid', 'version_minor', 'version_major', 'byteorder',
'dir_len', 'fat_len', 'dir_start', 'mini_stream_cutof_size',
'minifat_start', 'minifat_len', 'difat_start', 'difat_len'
):
s.append(f'{attr}: {getattr(self, attr)}')
return '\n '.join(s)
|
close
Close file handle.
src/domb/utils/oiffile.py
| def close(self):
"""Close file handle."""
self._fh.close()
|
direntry
Return DirectoryEntry of filename.
src/domb/utils/oiffile.py
| def direntry(self, name):
"""Return DirectoryEntry of filename."""
return self._files[name]
|
files
Return sequence of file names.
src/domb/utils/oiffile.py
| def files(self):
"""Return sequence of file names."""
return self._files.keys()
|
Return formatted string with list of all files.
src/domb/utils/oiffile.py
| def format_tree(self):
"""Return formatted string with list of all files."""
return '\n'.join(natural_sorted(self.files()))
|
open_file
Return stream as file like object.
src/domb/utils/oiffile.py
| def open_file(self, filename):
"""Return stream as file like object."""
return BytesIO(self._read_stream(self._files[filename]))
|
DirectoryEntry
Compound Document Directory Entry.
Initialize directory entry from 128 bytes.
Source code in src/domb/utils/oiffile.py
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798 | def __init__(self, data, version_major):
"""Initialize directory entry from 128 bytes."""
(
name,
name_len,
self.entry_type,
self.color,
self.left_sibling_id,
self.right_sibling_id,
self.child_id,
self.clsid,
self.user_flags,
create_time,
modify_time,
self.sector_start,
self.stream_size,
) = struct.unpack('<64sHBBIII16sIQQIQ', data)
if version_major == 3:
self.stream_size = struct.unpack('<I', data[-8:-4])[0]
if self.clsid == b'\000' * 16:
self.clsid = None
if name_len % 2 != 0 or name_len > 64:
raise OifFileError(f'invalid name_len {name_len}')
if self.color not in (0, 1):
raise OifFileError(f'invalid color {self.color}')
self.name = name[:name_len-2].decode('utf-16')
self.create_time = filetime(create_time)
self.modify_time = filetime(modify_time)
self.is_stream = self.entry_type == 2
self.is_storage = self.entry_type == 1
|
__str__
Return string with information about DirectoryEntry.
src/domb/utils/oiffile.py
| def __str__(self):
"""Return string with information about DirectoryEntry."""
s = [self.__class__.__name__]
for attr in self.__slots__[1:]:
s.append(f'{attr}: {getattr(self, attr)}')
return '\n '.join(s)
|
OibFileSystem
Olympus Image Binary file system.
Open compound document and read OibInfo.txt settings.
Source code in src/domb/utils/oiffile.py
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400 | def __init__(self, filename):
"""Open compound document and read OibInfo.txt settings."""
self.filename = filename
self.com = CompoundFile(filename)
info = SettingsFile(self.com.open_file('OibInfo.txt'),
'OibInfo.txt')['OibSaveInfo']
self.name = info.get('Name', None)
self.version = info.get('Version', None)
self.compression = info.get('Compression', None)
self.mainfile = info[info['MainFileName']]
# map OIB file names to CompoundFile file names
oibfiles = {os.path.split(i)[-1]: i for i in self.com.files()}
self._files = {v: oibfiles[k] for k, v in info.items()
if k.startswith('Stream')}
# map storage names to directory names
self._folders = {i[0]: i[1] for i in info.items()
if i[0].startswith('Storage')}
# read main settings file
self.settings = SettingsFile(self.open_file(self.mainfile),
name=self.mainfile)
|
__str__
Return string with information about OibFileSystem.
src/domb/utils/oiffile.py
457
458
459
460
461
462
463
464
465
466 | def __str__(self):
"""Return string with information about OibFileSystem."""
return '\n '.join((
self.__class__.__name__,
os.path.normpath(os.path.normcase(self.filename)),
f'name: {self.name}',
f'version: {self.version}',
f'mainfile: {self.mainfile}',
f'compression: {self.compression}',
))
|
close
Close file handle.
src/domb/utils/oiffile.py
| def close(self):
"""Close file handle."""
self.com.close()
|
files
Return iterator over unsorted files in OIB.
src/domb/utils/oiffile.py
| def files(self):
"""Return iterator over unsorted files in OIB."""
return iter(self._files.keys())
|
open_file
Return file object from case sensitive path name.
src/domb/utils/oiffile.py
| def open_file(self, filename):
"""Return file object from case sensitive path name."""
try:
return self.com.open_file(self._files[filename])
except KeyError as exc:
raise FileNotFoundError(f'No such file: {filename}') from exc
|
saveas_oif
Save all streams in OIB file as separate files.
Raise OSError if target files or directories already exist.
The main .oif file name and storage names are determined from the
OibInfo.txt settings.
src/domb/utils/oiffile.py
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445 | def saveas_oif(self, location='', verbose=0):
"""Save all streams in OIB file as separate files.
Raise OSError if target files or directories already exist.
The main .oif file name and storage names are determined from the
OibInfo.txt settings.
"""
if location and not os.path.exists(location):
os.makedirs(location)
mainfile = os.path.join(location, self.mainfile)
if os.path.exists(mainfile):
raise FileExistsError(mainfile + ' already exists')
for folder in self._folders.keys():
folder = os.path.join(location, self._folders.get(folder, ''))
if os.path.exists(folder):
raise FileExistsError(folder + ' already exists')
os.makedirs(folder)
if verbose:
print('Saving', mainfile, end=' ')
for f in self._files.keys():
folder, name = os.path.split(f)
folder = os.path.join(location, self._folders.get(folder, ''))
path = os.path.join(folder, name)
if verbose == 1:
print(end='.')
elif verbose > 1:
print(path)
with open(path, 'w+b') as fh:
fh.write(self.open_file(f).read())
if verbose == 1:
print(' done.')
|
OifFile
Olympus Image File.
Attributes: |
-
mainfile
(SettingsFile )
–
-
filesystem
(OibFileSystem or OifFileSystem )
–
The underlying file system instance.
-
series
(tuple of tifffile.TiffSequence )
–
Sequence of TIFF files. Includes shape, dtype, and axes information.
-
is_oib
(bool )
–
|
Open OIF or OIB file and read main settings.
Source code in src/domb/utils/oiffile.py
202
203
204
205
206
207
208
209
210
211
212
213
214 | def __init__(self, filename):
"""Open OIF or OIB file and read main settings."""
self.filename = filename
if filename.lower().endswith('.oif'):
self.filesystem = OifFileSystem(filename)
self.is_oib = False
else:
self.filesystem = OibFileSystem(filename)
self.is_oib = True
self.mainfile = self.filesystem.settings
# map file names to storage names (flattened name space)
self._files_flat = {os.path.basename(f): f
for f in self.filesystem.files()}
|
axes
property
Return order of axes in image data from mainfile.
dtype
property
Return dtype of image data from mainfile.
shape
property
Return shape of image data from mainfile.
tiffs
property
Return first TiffSequence.
__str__
Return string with information about OifFile.
src/domb/utils/oiffile.py
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320 | def __str__(self):
"""Return string with information about OifFile."""
# info = self.mainfile['Version Info']
s = [
self.__class__.__name__,
os.path.normpath(os.path.normcase(self.filename)),
f'axes: {self.axes}',
'shape: {}'.format(', '.join(str(i) for i in self.shape)),
f'dtype: {self.dtype}',
# f'system name: {info.get("SystemName", "None")}',
# f'system version: {info.get("SystemVersion", "None")}',
# f'file version: {info.get("FileVersion", "None")}',
]
if len(self.series) > 1:
s.append(f'series: {len(self.series)}')
return '\n '.join(s)
|
asarray
Return image data from TIFF file(s) as numpy array.
By default the data from the TIFF files in the first image series
is returned.
The kwargs parameters are passed to the asarray functions of the
TiffFile or TiffSequence instances.
src/domb/utils/oiffile.py
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293 | def asarray(self, series=0, **kwargs):
"""Return image data from TIFF file(s) as numpy array.
By default the data from the TIFF files in the first image series
is returned.
The kwargs parameters are passed to the asarray functions of the
TiffFile or TiffSequence instances.
"""
if isinstance(series, int):
return self.series[series].asarray(**kwargs)
with TiffFile(self.open_file(series), name=series) as tif:
result = tif.asarray(**kwargs)
return result
|
close
Close file handle.
src/domb/utils/oiffile.py
| def close(self):
"""Close file handle."""
self.filesystem.close()
|
glob
Return iterator over unsorted file names matching pattern.
src/domb/utils/oiffile.py
224
225
226
227
228
229
230 | def glob(self, pattern='*'):
"""Return iterator over unsorted file names matching pattern."""
if pattern == '*':
return self.filesystem.files()
pattern = pattern.replace('.', r'\.').replace('*', '.*')
pattern = re.compile(pattern)
return (f for f in self.filesystem.files() if pattern.match(f))
|
open_file
Return open file object from path name.
src/domb/utils/oiffile.py
216
217
218
219
220
221
222 | def open_file(self, filename):
"""Return open file object from path name."""
try:
return self.filesystem.open_file(
self._files_flat.get(filename, filename))
except (KeyError, OSError) as exc:
raise FileNotFoundError(f'No such file: {filename}') from exc
|
series
Return tuple of TiffSequence of sorted TIFF files.
src/domb/utils/oiffile.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271 | @lazyattr
def series(self):
"""Return tuple of TiffSequence of sorted TIFF files."""
series = {}
for fname in self.glob('*.tif'):
key = ''.join(c for c in os.path.split(fname)[-1][:-4]
if c.isalpha())
if key in series:
series[key].append(fname)
else:
series[key] = [fname]
series = [TiffSequence(natural_sorted(files),
imread=self.asarray, pattern='axes')
for files in series.values()]
if len(series) > 1:
series = tuple(reversed(sorted(series,
key=lambda x: len(x.files))))
return series
|
OifFileError
Bases: Exception
Exception to raise issues with OIF or OIB structure.
OifFileSystem
Olympus Image File file system.
Open OIF file and read settings.
Source code in src/domb/utils/oiffile.py
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342 | def __init__(self, filename, storage_ext='.files'):
"""Open OIF file and read settings."""
self.filename = filename
self._path, self.mainfile = os.path.split(os.path.abspath(filename))
self.settings = SettingsFile(filename, name=self.mainfile)
self.name = self.settings['ProfileSaveInfo']['Name']
self.version = self.settings['ProfileSaveInfo']['Version']
# check that storage directory exists
storage = os.path.join(self._path, self.mainfile + storage_ext)
if not os.path.exists(storage) or not os.path.isdir(storage):
raise OSError(
f'OIF storage path not found: {self.mainfile}{storage_ext}')
# list all files
pathlen = len(self._path + os.path.sep)
self._files = [self.mainfile]
for f in glob(os.path.join(storage, '*')):
self._files.append(f[pathlen:])
|
__str__
Return string with information about OifFileSystem.
src/domb/utils/oiffile.py
367
368
369
370
371
372
373
374
375 | def __str__(self):
"""Return string with information about OifFileSystem."""
return '\n '.join((
self.__class__.__name__,
os.path.normpath(os.path.normcase(self.filename)),
f'name: {self.name}',
f'version: {self.version}',
f'mainfile: {self.mainfile}',
))
|
close
Close file handle.
src/domb/utils/oiffile.py
| def close(self):
"""Close file handle."""
|
files
Return iterator over unsorted files in OIF.
src/domb/utils/oiffile.py
| def files(self):
"""Return iterator over unsorted files in OIF."""
return iter(self._files)
|
glob
Return iterator of path names matching the specified pattern.
src/domb/utils/oiffile.py
| def glob(self, pattern):
"""Return iterator of path names matching the specified pattern."""
pattern = pattern.replace('.', r'\.').replace('*', '.*')
pattern = re.compile(pattern)
return (f for f in self.files() if pattern.match(f))
|
open_file
Return file object from path name.
src/domb/utils/oiffile.py
| def open_file(self, filename):
"""Return file object from path name."""
return open(os.path.join(self._path, filename), 'rb')
|
SettingsFile
Bases: dict
Olympus settings file (oif, txt, pyt, roi, lut).
Settings files contain little endian utf-16 encoded strings, except for
[ColorLUTData] sections, which contain uint8 binary arrays.
Settings can be accessed as a nested dictionary {section: {key: value}},
except for {'ColorLUTData': numpy array}.
Read settings file and parse into nested dictionaries.
Parameters: |
-
arg
(str or file object )
–
Name of file or open file containing little endian UTF-16 string.
File objects are closed by this function.
-
name
(str , default:
None
)
–
Human readable label of stream.
|
Source code in src/domb/utils/oiffile.py
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535 | def __init__(self, arg, name=None):
"""Read settings file and parse into nested dictionaries.
Parameters
----------
arg : str or file object
Name of file or open file containing little endian UTF-16 string.
File objects are closed by this function.
name : str
Human readable label of stream.
"""
dict.__init__(self)
if isinstance(arg, str):
self.name = arg
stream = open(arg, 'rb')
else:
self.name = str(name)
stream = arg
try:
content = stream.read()
finally:
stream.close()
if content[:4] == b'\xFF\xFE\x5B\x00':
# UTF16 BOM
content = content.rsplit(
b'[\x00C\x00o\x00l\x00o\x00r\x00L\x00U\x00T\x00'
b'D\x00a\x00t\x00a\x00]\x00\x0D\x00\x0A\x00', 1)
if len(content) > 1:
self['ColorLUTData'] = numpy.fromstring(
content[1], 'uint8').reshape(-1, 4)
content = content[0].decode('utf-16')
elif content[:1] == b'[':
# try UTF-8
content = content.rsplit(b'[ColorLUTData]\r\n', 1)
if len(content) > 1:
self['ColorLUTData'] = numpy.fromstring(
content[1], 'uint8').reshape(-1, 4)
try:
content = content[0].decode('utf-8')
except Exception as exc:
raise ValueError('not a valid settings file') from exc
else:
raise ValueError('not a valid settings file')
for line in content.splitlines():
line = line.strip()
if line.startswith(';'):
continue
if line.startswith('[') and line.endswith(']'):
self[line[1:-1]] = properties = {}
else:
key, value = line.split('=')
properties[key] = astype(value)
|
__str__
Return string with information about SettingsFile.
src/domb/utils/oiffile.py
| def __str__(self):
"""Return string with information about SettingsFile."""
return '\n '.join((self.__class__.__name__, format_dict(self)))
|
OibImread
Return image data from OIF or OIB file as numpy array.
'args' and 'kwargs' are arguments to OifFile.asarray().
src/domb/utils/oiffile.py
165
166
167
168
169
170
171
172
173 | def OibImread(filename, *args, **kwargs):
"""Return image data from OIF or OIB file as numpy array.
'args' and 'kwargs' are arguments to OifFile.asarray().
"""
with OifFile(filename) as oif:
result = oif.asarray(*args, **kwargs)
return result
|
astype
Return argument as one of types if possible.
src/domb/utils/oiffile.py
826
827
828
829
830
831
832
833
834
835
836
837 | def astype(value, types=None):
"""Return argument as one of types if possible."""
if value[0] in '\'"':
return value[1:-1]
if types is None:
types = int, float, str
for typ in types:
try:
return typ(value)
except (ValueError, TypeError, UnicodeEncodeError):
pass
return value
|
filetime
Return Python datetime from Microsoft FILETIME number.
src/domb/utils/oiffile.py
| def filetime(ft):
"""Return Python datetime from Microsoft FILETIME number."""
if not ft:
return None
sec, nsec = divmod(ft - 116444736000000000, 10000000)
return datetime.utcfromtimestamp(sec).replace(microsecond=(nsec // 10))
|
Return pretty-print of nested dictionary.
src/domb/utils/oiffile.py
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823 | def format_dict(adict, prefix=' ', indent=' ', bullets=('', ''),
excludes=('_', ), linelen=79, trim=1):
"""Return pretty-print of nested dictionary."""
result = []
for k, v in sorted(adict.items(), key=lambda x: str(x[0]).lower()):
if any(k.startswith(e) for e in excludes):
continue
if isinstance(v, dict):
v = '\n' + format_dict(v, prefix=prefix+indent, excludes=excludes,
trim=0)
result.append(f'{prefix}{bullets[1]}{k}: {v}')
else:
result.append((f'{prefix}{bullets[0]}{k}: {v}')[:linelen].rstrip())
if trim > 0:
result[0] = result[0][trim:]
return '\n'.join(result)
|
oib2oif
Convert OIB file to OIF.
src/domb/utils/oiffile.py
| def oib2oif(filename, location='', verbose=1):
"""Convert OIB file to OIF."""
with OibFileSystem(filename) as oib:
oib.saveas_oif(location=location, verbose=verbose)
|