IPTCInfo3 2.1.4__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ __pycache__
2
+ .cache
3
+ .coverage
4
+
5
+ # Packaging files
6
+ *.egg-info
7
+ dist/*
8
+ build/*
9
+ .cache
10
+ __pycache__
11
+
12
+ # Test files cleanup
13
+ .jpg
14
+ .jpeg
15
+
16
+ # ide stuff
17
+ .vscode
18
+ .vscode/*
19
+
20
+ # pipfile stuff
21
+ Pipfile.lock
@@ -0,0 +1,107 @@
1
+ ## 2.3.0 (2025-12-02)
2
+
3
+ ### Bug Fixes
4
+ - **Issue #50**: Fixed keywords and other list fields to prevent duplicate values (deduplicates on append and save)
5
+ - **Issue #45**: Removed outdated VERSION = 1.9 comment from code
6
+
7
+ ### New Features
8
+ - **Issue #44**: Added LICENSE file with dual Artistic-1.0 / GPL-1.0+ license
9
+ - Added `UniqueList` class for list fields that automatically prevents duplicates
10
+
11
+ ---
12
+
13
+ ## 2.2.0 (2025-10-02)
14
+
15
+ ### Bug Fixes
16
+ - **Issue #40**: Clarified license statements - now consistently states "Artistic-1.0 OR GPL-1.0-or-later"
17
+ - **Issue #24**: Changed "Marker scan hit start of image data" to INFO level when `force=True` is used
18
+ - **Issue #32**: Fixed charset recognition for ISO 2022 escape sequences (UTF-8 as `\x1b%G`)
19
+ - **Issue #26**: Added validation for float/NaN values in `packedIIMData()` to prevent TypeError
20
+
21
+ ### New Features
22
+ - **Issue #35**: Added 'credit line' field support per IPTC Core 1.1 (backward compatible with 'credit')
23
+ - **Issue #42**: Added 'destination' field as alias for 'original transmission reference'
24
+
25
+ ### Improvements
26
+ - **Issue #15**: Enhanced IPTC tag collection with better field mappings
27
+ - **Issue #38**: Verified backup file behavior (use `options={'overwrite': True}` to avoid ~ files)
28
+ - Better error handling and logging throughout
29
+
30
+ ### Build System Modernization
31
+ - Migrated from legacy `setup.py` to modern `pyproject.toml`
32
+ - Now uses `uv` and `hatchling` for building (PEP 517/518 compliant)
33
+ - Simplified setup.py to minimal backward compatibility shim
34
+ - Added comprehensive publishing guide (`PUBLISHING.md`)
35
+ - Supports Python 3.8 through 3.13
36
+
37
+ ### Notes
38
+ - **Issue #39, #41**: Ready for PyPI release with all fixes from master branch
39
+
40
+ ---
41
+
42
+ Updating builds to target 3.9.7
43
+
44
+ 2.1: Fixes merged to save modified IPTC info images
45
+
46
+ 1.9.5-8: https://bitbucket.org/gthomas/iptcinfo/issue/4/file-permissions-for-changed-files-are-not - copy original file's permission bits on save/saveAs
47
+
48
+ 1.9.5-7: https://bitbucket.org/gthomas/iptcinfo/issue/3/images-w-o-iptc-data-should-not-log-errors - have silencable parse errors.
49
+
50
+ 1.9.5-6: to have a nice new upload (seems easy_install grabs an old version).
51
+
52
+ 1.9.5-5: fix some issues with "super"
53
+
54
+ 1.9.5-3: use logging module.
55
+
56
+ 1.9.5-2: Emil Stenström pinpointed some bugs/misleading (un)comments
57
+ Also a new (mis)feature is implemented: if you don't specify inp_charset
58
+ (and the image misses such information, too) than no conversion is made
59
+ to unicode, everything stays bytestring!
60
+ This way you don't need to deal with charsets, BUT it is your risk to make
61
+ the modifications with the SAME charset as it is in the image!
62
+
63
+ 1.9.5-1: getting in sync with the Perl version 1.9.5
64
+
65
+ 1.9.2-rc8:
66
+ charset recognition loosened (failed with some image out of
67
+ Adobe Lightroom).
68
+
69
+ 1.9.2-rc7: NOT READY
70
+ IPTCInfo now accepts 'inp_charset' keyword for setting input charset.
71
+
72
+ 1.9.2-rc6: just PyLint-ed out some errors.
73
+
74
+ 1.9.2-rc5: Amos Latteier sent me a patch which releases the requirement of the
75
+ file objects to be file objects (he uses this on jpeg files stored in
76
+ databases as strings).
77
+ It modifies the module in order to look for a read method on the file
78
+ object. If one exists it assumes the argument is a file object, otherwise it
79
+ assumes it's a filename.
80
+
81
+ 1.9.2-rc4: on Windows systems, tmpfile may not work correctly - now I use
82
+ cStringIO on file save (to save the file without truncating it on Exception).
83
+
84
+ 1.9.2-rc3: some little bug fixes, some safety enhancements (now iptcinfo.py
85
+ will overwrite the original image file (info.save()) only if everything goes
86
+ fine (so if an exception is thrown at writing, it won't cut your original
87
+ file).
88
+
89
+ This is a pre-release version: needs some testing, and has an unfound bug
90
+ (yet): some pictures can be enhanced with iptc data, and iptcinfo.py is able
91
+ to read them, but some other iptc data readers will spit on it.
92
+
93
+ 1.9.1: a first release with some little encoding support
94
+
95
+ The class IPTCInfo now has an inp_charset and an out_charset attribute - the
96
+ first is the read image's charset (defaults to the system default charset),
97
+ the second is the charset the writer will use (defaults to inp_charset).
98
+
99
+ Reader will find the charset included in IPTC data (if any, defaults to the
100
+ system's default charset), and use it to read to unicode strings. Writer will
101
+ write using IPTCinfo.out_charset (if it is not set, will not write charset
102
+ IPTC record).
103
+
104
+ With this, it is possible to read and write i18n strings correctly.
105
+
106
+ I haven't tested this functionality thoroughly, and that little test was only
107
+ on my WinXP box only, with the only other IPTC reader: IrfanView.
@@ -0,0 +1,20 @@
1
+ IPTCInfo3 - IPTC metadata extraction and modification for Python
2
+
3
+ Copyright (C) 2000-2004 Josh Carter <josh@multipart-mixed.com>
4
+ Copyright (C) 2004-2008 Tamas Gulacsi <gthomas@gthomas.hu>
5
+ Copyright (C) 2008-2025 James Campbell
6
+
7
+ DUAL LICENSE
8
+
9
+ This software is dual-licensed. You may choose either:
10
+
11
+ 1. The Artistic License 1.0
12
+ https://opensource.org/licenses/Artistic-1.0
13
+
14
+ 2. The GNU General Public License v1.0 or later
15
+ https://www.gnu.org/licenses/old-licenses/gpl-1.0.en.html
16
+
17
+ You may use, modify, and distribute this software under the terms of
18
+ either license, at your option.
19
+
20
+ Full license texts available at the URLs above.
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: IPTCInfo3
3
+ Version: 2.3.0
4
+ Summary: Extract and modify IPTC metadata from JPEG images
5
+ Project-URL: Homepage, https://github.com/jamesacampbell/iptcinfo3
6
+ Project-URL: Repository, https://github.com/jamesacampbell/iptcinfo3
7
+ Project-URL: Issues, https://github.com/jamesacampbell/iptcinfo3/issues
8
+ Author-email: Tamas Gulacsi <gthomas@fw.hu>
9
+ Maintainer-email: James Campbell <jc@normail.co>
10
+ License: Artistic-1.0 OR GPL-1.0-or-later
11
+ License-File: LICENSE
12
+ Keywords: exif,iptc,jpeg,metadata,photography
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Artistic License
16
+ Classifier: License :: OSI Approved :: GNU General Public License (GPL)
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Multimedia :: Graphics
26
+ Classifier: Topic :: Utilities
27
+ Requires-Python: >=3.8
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
30
+ Requires-Dist: pytest-watch>=4.2; extra == 'dev'
31
+ Requires-Dist: pytest>=7.0; extra == 'dev'
32
+ Description-Content-Type: text/x-rst
33
+
34
+ ``IPTCINFO 3``
35
+ ==============
36
+
37
+ Like IPTCInfo but finally compatible for Python 3
38
+ -------------------------------------------------
39
+
40
+
41
+ Ported from Josh Carter's Perl IPTCInfo-1.9.pm by Tamas Gulacsi
42
+
43
+ Ever wish you add information to your photos like a caption, the place
44
+ you took it, the date, and perhaps even keywords and categories? You
45
+ already can. The International Press Telecommunications Council (IPTC)
46
+ defines a format for exchanging meta-information in news content, and
47
+ that includes photographs. You can embed all kinds of information in
48
+ your images. The trick is putting it to use.
49
+
50
+ That's where this IPTCInfo Python module comes into play. You can embed
51
+ information using many programs, including Adobe Photoshop, and
52
+ IPTCInfo will let your web server -- and other automated server
53
+ programs -- pull it back out. You can use the information directly in
54
+ Python programs, export it to XML, or even export SQL statements ready
55
+ to be fed into a database.
56
+
57
+ Installation
58
+ ------------
59
+
60
+ Install from PyPI::
61
+
62
+ pip install IPTCInfo3
63
+
64
+ Or with uv::
65
+
66
+ uv pip install IPTCInfo3
67
+
68
+ Requirements
69
+ ------------
70
+
71
+ - Python 3.8 or higher
72
+ - No external dependencies required
73
+
74
+ Usage
75
+ -----
76
+
77
+ ``from iptcinfo3 import IPTCInfo``
78
+
79
+
80
+ Create new info object
81
+ ``info = IPTCInfo('doge.jpg')``
82
+
83
+ Print list of keywords, supplemental categories, contacts
84
+ ``print(info['keywords'])``
85
+ ``print(info['supplementalCategories'])``
86
+ ``print(info['contacts'])``
87
+
88
+ Get specific attributes...
89
+ ``caption = info['caption/abstract']``
90
+
91
+ Create object for file that may not have IPTC data
92
+ ``info = IPTCInfo('such_iptc.jpg', force=True)``
93
+
94
+ Add/change an attribute
95
+ ``info['caption/abstract'] = 'Witty caption here'``
96
+ ``info['supplemental category'] = ['portrait']``
97
+
98
+ Lists for keywords, so you can just append!
99
+ ``info['keywords']).append('cool')``
100
+
101
+ Save new info to file
102
+ ``info.save()``
103
+ ``info.save_as('very_meta.jpg')``
@@ -1,8 +1,6 @@
1
1
  ``IPTCINFO 3``
2
2
  ==============
3
3
 
4
- `Build Status <https://travis-ci.org/crccheck/iptcinfo3.svg?branch=master>`_
5
-
6
4
  Like IPTCInfo but finally compatible for Python 3
7
5
  -------------------------------------------------
8
6
 
@@ -23,6 +21,23 @@ programs -- pull it back out. You can use the information directly in
23
21
  Python programs, export it to XML, or even export SQL statements ready
24
22
  to be fed into a database.
25
23
 
24
+ Installation
25
+ ------------
26
+
27
+ Install from PyPI::
28
+
29
+ pip install IPTCInfo3
30
+
31
+ Or with uv::
32
+
33
+ uv pip install IPTCInfo3
34
+
35
+ Requirements
36
+ ------------
37
+
38
+ - Python 3.8 or higher
39
+ - No external dependencies required
40
+
26
41
  Usage
27
42
  -----
28
43
 
@@ -47,6 +62,9 @@ Add/change an attribute
47
62
  ``info['caption/abstract'] = 'Witty caption here'``
48
63
  ``info['supplemental category'] = ['portrait']``
49
64
 
65
+ Lists for keywords, so you can just append!
66
+ ``info['keywords']).append('cool')``
67
+
50
68
  Save new info to file
51
69
  ``info.save()``
52
70
  ``info.save_as('very_meta.jpg')``
@@ -11,9 +11,9 @@
11
11
  # All rights reserved.
12
12
  #
13
13
  # This program is free software; you can redistribute it and/or modify
14
- # it under the same terms as Python itself.
14
+ # it under the terms of the Artistic License or the GNU General Public
15
+ # License (GPL). You may choose either license.
15
16
  #
16
- # VERSION = '1.9';
17
17
  """
18
18
  IPTCInfo - Python module for extracting and modifying IPTC image meta-data
19
19
  """
@@ -24,8 +24,9 @@ import shutil
24
24
  import sys
25
25
  import tempfile
26
26
  from struct import pack, unpack
27
+ import json
27
28
 
28
- __version__ = '2.1.4'
29
+ __version__ = '2.3.0'
29
30
  __author__ = 'Gulácsi, Tamás'
30
31
  __updated_by__ = 'Campbell, James'
31
32
 
@@ -177,7 +178,7 @@ def jpeg_get_variable_length(fh):
177
178
 
178
179
  # Length includes itself, so must be at least 2
179
180
  if length < 2:
180
- logger.warn("jpeg_get_variable_length: erroneous JPEG marker length")
181
+ logger.warning("jpeg_get_variable_length: erroneous JPEG marker length")
181
182
  return 0
182
183
  return length - 2
183
184
 
@@ -192,7 +193,7 @@ def jpeg_next_marker(fh):
192
193
  try:
193
194
  byte = read_exactly(fh, 1)
194
195
  while ord3(byte) != 0xff:
195
- # logger.warn("jpeg_next_marker: bogus stuff in Jpeg file at: ')
196
+ # logger.warning("jpeg_next_marker: bogus stuff in Jpeg file at: ')
196
197
  byte = read_exactly(fh, 1)
197
198
 
198
199
  # Now skip any extra 0xffs, which are valid padding.
@@ -360,7 +361,7 @@ def jpeg_debug_scan(filename): # pragma: no cover
360
361
  break
361
362
 
362
363
  if ord3(marker) == 0:
363
- logger.warn("Marker scan failed")
364
+ logger.warning("Marker scan failed")
364
365
  break
365
366
 
366
367
  elif ord3(marker) == 0xd9:
@@ -368,7 +369,7 @@ def jpeg_debug_scan(filename): # pragma: no cover
368
369
  break
369
370
 
370
371
  if not jpeg_skip_variable(fh):
371
- logger.warn("jpeg_skip_variable failed")
372
+ logger.warning("jpeg_skip_variable failed")
372
373
  return None
373
374
 
374
375
 
@@ -480,7 +481,7 @@ c_datasets = {
480
481
  101: 'country/primary location name',
481
482
  103: 'original transmission reference',
482
483
  105: 'headline',
483
- 110: 'credit',
484
+ 110: 'credit line', # Updated from 'credit' to 'credit line' per IPTC Core 1.1
484
485
  115: 'source',
485
486
  116: 'copyright notice',
486
487
  118: 'contact',
@@ -522,6 +523,17 @@ c_charset = {100: 'iso8859_1', 101: 'iso8859_2', 109: 'iso8859_3',
522
523
  c_charset_r = {v: k for k, v in c_charset.items()}
523
524
 
524
525
 
526
+ class UniqueList(list):
527
+ """A list that prevents duplicate values on append/extend."""
528
+ def append(self, value):
529
+ if value not in self:
530
+ super().append(value)
531
+
532
+ def extend(self, values):
533
+ for v in values:
534
+ self.append(v)
535
+
536
+
525
537
  class IPTCData(dict):
526
538
  """Dict with int/string keys from c_listdatanames"""
527
539
  def __init__(self, diction={}, *args, **kwds):
@@ -536,6 +548,12 @@ class IPTCData(dict):
536
548
  return key
537
549
  elif isinstance(key, str) and key.lower() in c_datasets_r:
538
550
  return c_datasets_r[key.lower()]
551
+ # Backward compatibility: 'credit' is now 'credit line' per IPTC Core 1.1
552
+ elif isinstance(key, str) and key.lower() == 'credit':
553
+ return 110
554
+ # Alias for compatibility with gThumb/exiftool
555
+ elif isinstance(key, str) and key.lower() == 'destination':
556
+ return 103 # Maps to 'original transmission reference'
539
557
  elif key.startswith(cls.c_cust_pre) and key[len(cls.c_cust_pre):].isdigit():
540
558
  # example: nonstandard_69 -> 69
541
559
  return int(key[len(cls.c_cust_pre):])
@@ -553,6 +571,13 @@ class IPTCData(dict):
553
571
  else:
554
572
  raise KeyError("Key %s is not in %s!" % (key, list(c_datasets.keys())))
555
573
 
574
+ def __contains__(self, name):
575
+ try:
576
+ key = self._key_as_int(name)
577
+ except KeyError:
578
+ return False
579
+ return super().__contains__(key)
580
+
556
581
  def __getitem__(self, name):
557
582
  return self.get(self._key_as_int(name), None)
558
583
 
@@ -593,11 +618,12 @@ class IPTCInfo:
593
618
 
594
619
  def __init__(self, fobj, force=False, inp_charset=None, out_charset=None):
595
620
  self._data = IPTCData({
596
- 'supplemental category': [],
597
- 'keywords': [],
598
- 'contact': [],
621
+ 'supplemental category': UniqueList(),
622
+ 'keywords': UniqueList(),
623
+ 'contact': UniqueList(),
599
624
  })
600
625
  self._fobj = fobj
626
+ self._force = force
601
627
  if duck_typed(fobj, 'read'): # DELETEME
602
628
  self._filename = None
603
629
  else:
@@ -613,7 +639,7 @@ class IPTCInfo:
613
639
  if datafound:
614
640
  self.collectIIMInfo(fh)
615
641
  else:
616
- logger.warn('No IPTC data found in %s', fobj)
642
+ logger.warning('No IPTC data found in %s', fobj)
617
643
 
618
644
  def _filepos(self, fh):
619
645
  """For debugging, return what position in the file we are."""
@@ -630,7 +656,7 @@ class IPTCInfo:
630
656
  """Saves Jpeg with IPTC data to a given file name."""
631
657
  with smart_open(self._fobj, 'rb') as fh:
632
658
  if not file_is_jpeg(fh):
633
- logger.error('Source file %s is not a Jpeg.' % self._fob)
659
+ logger.error('Source file %s is not a Jpeg.' % self._fobj)
634
660
  return None
635
661
 
636
662
  jpeg_parts = jpeg_collect_file_parts(fh)
@@ -686,8 +712,10 @@ class IPTCInfo:
686
712
  os.unlink(tmpfn)
687
713
  else:
688
714
  tmpfh.close()
689
- if os.path.exists(newfile):
690
- shutil.move(newfile, newfile + '~')
715
+ if os.path.exists(newfile) and options is not None and 'overwrite' in options:
716
+ os.unlink(newfile)
717
+ elif os.path.exists(newfile):
718
+ shutil.move(newfile, "{file}~".format(file=newfile))
691
719
  shutil.move(tmpfn, newfile)
692
720
  return True
693
721
 
@@ -699,6 +727,9 @@ class IPTCInfo:
699
727
  def __len__(self):
700
728
  return len(self._data)
701
729
 
730
+ def __contains__(self, key):
731
+ return key in self._data
732
+
702
733
  def __getitem__(self, key):
703
734
  return self._data[key]
704
735
 
@@ -716,7 +747,7 @@ class IPTCInfo:
716
747
  logger.info("File is JPEG, proceeding with JpegScan")
717
748
  return self.jpegScan(fh)
718
749
  else:
719
- logger.warn("File not a JPEG, trying blindScan")
750
+ logger.warning("File not a JPEG, trying blindScan")
720
751
  return self.blindScan(fh)
721
752
 
722
753
  c_marker_err = {0: "Marker scan failed",
@@ -752,14 +783,18 @@ class IPTCInfo:
752
783
  err = "jpeg_skip_variable failed"
753
784
  if err is not None:
754
785
  self.error = err
755
- logger.warn(err)
786
+ # When force=True, log as INFO instead of WARNING since we expect no IPTC data
787
+ if self._force:
788
+ logger.info(err)
789
+ else:
790
+ logger.warning(err)
756
791
  return None
757
792
 
758
793
  # If were's here, we must have found the right marker.
759
794
  # Now blindScan through the data.
760
795
  return self.blindScan(fh, MAX=jpeg_get_variable_length(fh))
761
796
 
762
- def blindScan(self, fh, MAX=8192):
797
+ def blindScan(self, fh, MAX=819200):
763
798
  """Scans blindly to first IIM Record 2 tag in the file. This
764
799
  method may or may not work on any arbitrary file type, but it
765
800
  doesn't hurt to check. We expect to see this tag within the first
@@ -767,7 +802,7 @@ class IPTCInfo:
767
802
  depending on how other programs choose to store IIM.)"""
768
803
 
769
804
  offset = 0
770
- # keep within first 8192 bytes
805
+ # keep within first 819200 bytes
771
806
  # NOTE: this may need to change
772
807
  logger.debug('blindScan: starting scan, max length %d', MAX)
773
808
 
@@ -776,7 +811,7 @@ class IPTCInfo:
776
811
  try:
777
812
  temp = read_exactly(fh, 1)
778
813
  except EOFException:
779
- logger.warn("BlindScan: hit EOF while scanning")
814
+ logger.warning("BlindScan: hit EOF while scanning")
780
815
  return None
781
816
  # look for tag identifier 0x1c
782
817
  if ord3(temp) == 0x1c:
@@ -787,15 +822,32 @@ class IPTCInfo:
787
822
  # found character set's record!
788
823
  try:
789
824
  temp = read_exactly(fh, jpeg_get_variable_length(fh))
790
- try:
791
- cs = unpack('!H', temp)[0]
792
- except Exception: # TODO better exception
793
- logger.warn('WARNING: problems with charset recognition (%r)', temp)
794
- cs = None
795
- if cs in c_charset:
796
- self.inp_charset = c_charset[cs]
797
- logger.info("BlindScan: found character set '%s' at offset %d",
798
- self.inp_charset, offset)
825
+ cs = None
826
+ # Check for ISO 2022 escape sequence (starts with ESC 0x1b)
827
+ if len(temp) >= 3 and ord3(temp[0]) == 0x1b:
828
+ # Parse ISO 2022 escape sequences
829
+ # ESC % G = UTF-8
830
+ if temp == b'\x1b%G':
831
+ self.inp_charset = 'utf_8'
832
+ # ESC % / @ = UTF-16 (not commonly used)
833
+ elif temp == b'\x1b%/@':
834
+ self.inp_charset = 'utf_16'
835
+ else:
836
+ logger.debug(
837
+ "BlindScan: unknown ISO 2022 charset escape sequence %r",
838
+ temp)
839
+ else:
840
+ # Try legacy numeric charset encoding
841
+ try:
842
+ cs = unpack('!H', temp)[0]
843
+ if cs in c_charset:
844
+ self.inp_charset = c_charset[cs]
845
+ except Exception:
846
+ logger.debug('BlindScan: could not parse charset from %r', temp)
847
+
848
+ if self.inp_charset:
849
+ logger.info("BlindScan: found character set '%s' at offset %d",
850
+ self.inp_charset, offset)
799
851
  except EOFException:
800
852
  pass
801
853
 
@@ -845,7 +897,7 @@ class IPTCInfo:
845
897
  try:
846
898
  value = str(value, encoding=self.inp_charset, errors='strict')
847
899
  except Exception: # TODO better exception
848
- logger.warn('Data "%r" is not in encoding %s!', value, self.inp_charset)
900
+ logger.warning('Data "%r" is not in encoding %s!', value, self.inp_charset)
849
901
  value = str(value, encoding=self.inp_charset, errors='replace')
850
902
 
851
903
  # try to extract first into _listdata (keywords, categories)
@@ -889,11 +941,22 @@ class IPTCInfo:
889
941
  LOGDBG.debug('out=%s', hex_dump(out))
890
942
  # Iterate over data sets
891
943
  for dataset, value in self._data.items():
892
- if len(value) == 0:
944
+ # Skip None, empty strings, empty lists, and NaN values
945
+ if value is None:
946
+ continue
947
+ # Handle float/int that might be NaN
948
+ if isinstance(value, (float, int)):
949
+ import math
950
+ if isinstance(value, float) and math.isnan(value):
951
+ continue
952
+ # Convert numeric values to strings
953
+ value = str(value)
954
+ # Check length for strings and lists
955
+ if hasattr(value, '__len__') and len(value) == 0:
893
956
  continue
894
957
 
895
958
  if not (isinstance(dataset, int) and dataset in c_datasets):
896
- logger.warn("packedIIMData: illegal dataname '%s' (%d)", dataset, dataset)
959
+ logger.warning("packedIIMData: illegal dataname '%s' (%d)", dataset, dataset)
897
960
  continue
898
961
 
899
962
  logger.debug('packedIIMData %02X: %r -> %r', dataset, value, self._enc(value))
@@ -903,10 +966,11 @@ class IPTCInfo:
903
966
  out.append(pack("!BBBH", tag, record, dataset, len(value)))
904
967
  out.append(value)
905
968
  else:
969
+ seen = set()
906
970
  for v in map(bytes, value):
907
- if v is None or len(v) == 0:
971
+ if v is None or len(v) == 0 or v in seen:
908
972
  continue
909
-
973
+ seen.add(v)
910
974
  out.append(pack("!BBBH", tag, record, dataset, len(v)))
911
975
  out.append(v)
912
976
 
@@ -944,7 +1008,16 @@ class IPTCInfo:
944
1008
 
945
1009
 
946
1010
  if __name__ == '__main__': # pragma: no cover
947
- logging.basicConfig(level=logging.DEBUG)
1011
+ logging.basicConfig(level=logging.ERROR)
948
1012
  if len(sys.argv) > 1:
949
1013
  info = IPTCInfo(sys.argv[1])
950
- print(info)
1014
+ if info.__dict__ != '':
1015
+ for k, v in info.__dict__.items():
1016
+ if k == '_data':
1017
+ print(k)
1018
+ for key, value in v.items():
1019
+ if type(value) == list:
1020
+ print(key, [x.decode() for x in value])
1021
+ [print(x.decode()) for x in value]
1022
+ print(key, value)
1023
+ print(k, v)
@@ -0,0 +1,80 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "IPTCInfo3"
7
+ version = "2.3.0"
8
+ description = "Extract and modify IPTC metadata from JPEG images"
9
+ readme = "README.rst"
10
+ license = { text = "Artistic-1.0 OR GPL-1.0-or-later" }
11
+ authors = [
12
+ { name = "Tamas Gulacsi", email = "gthomas@fw.hu" },
13
+ ]
14
+ maintainers = [
15
+ { name = "James Campbell", email = "jc@normail.co" },
16
+ ]
17
+ keywords = ["iptc", "metadata", "jpeg", "exif", "photography"]
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "License :: OSI Approved :: Artistic License",
21
+ "License :: OSI Approved :: GNU General Public License (GPL)",
22
+ "Intended Audience :: Developers",
23
+ "Programming Language :: Python",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.8",
26
+ "Programming Language :: Python :: 3.9",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Topic :: Multimedia :: Graphics",
32
+ "Topic :: Utilities",
33
+ ]
34
+ requires-python = ">=3.8"
35
+ dependencies = []
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/jamesacampbell/iptcinfo3"
39
+ Repository = "https://github.com/jamesacampbell/iptcinfo3"
40
+ Issues = "https://github.com/jamesacampbell/iptcinfo3/issues"
41
+
42
+ [project.optional-dependencies]
43
+ dev = [
44
+ "pytest>=7.0",
45
+ "pytest-cov>=4.0",
46
+ "pytest-watch>=4.2",
47
+ ]
48
+
49
+ [tool.hatch.build.targets.sdist]
50
+ include = [
51
+ "iptcinfo3.py",
52
+ "README.rst",
53
+ "CHANGELOG.md",
54
+ "LICENSE",
55
+ ]
56
+
57
+ [tool.hatch.build.targets.wheel]
58
+ packages = ["."]
59
+ only-include = ["iptcinfo3.py"]
60
+
61
+ [tool.pytest.ini_options]
62
+ testpaths = ["tests"]
63
+ python_files = ["test_*.py", "*_test.py"]
64
+
65
+ [tool.coverage.run]
66
+ branch = true
67
+ source = ["."]
68
+
69
+ [tool.coverage.report]
70
+ show_missing = true
71
+ skip_covered = true
72
+
73
+ [tool.ruff]
74
+ line-length = 100
75
+ target-version = "py38"
76
+
77
+ [tool.ruff.lint]
78
+ select = ["E", "F", "W", "I", "N", "UP"]
79
+ ignore = ["E501"] # Line length handled by formatter
80
+
@@ -1,72 +0,0 @@
1
- Metadata-Version: 1.2
2
- Name: IPTCInfo3
3
- Version: 2.1.4
4
- Summary: A great way to get IPTCInfo
5
- Home-page: https://github.com/jamesacampbell/iptcinfo3
6
- Author: Tamas Gulacsi
7
- Author-email: gthomas@fw.hu
8
- Maintainer: James Campbell
9
- Maintainer-email: jc@normail.co
10
- License: http://www.opensource.org/licenses/gpl-license.php
11
- Download-URL: https://github.com/jamesacampbell/iptcinfo3
12
- Description: ``IPTCINFO 3``
13
- ==============
14
-
15
- `Build Status <https://travis-ci.org/crccheck/iptcinfo3.svg?branch=master>`_
16
-
17
- Like IPTCInfo but finally compatible for Python 3
18
- -------------------------------------------------
19
-
20
-
21
- Ported from Josh Carter's Perl IPTCInfo-1.9.pm by Tamas Gulacsi
22
-
23
- Ever wish you add information to your photos like a caption, the place
24
- you took it, the date, and perhaps even keywords and categories? You
25
- already can. The International Press Telecommunications Council (IPTC)
26
- defines a format for exchanging meta-information in news content, and
27
- that includes photographs. You can embed all kinds of information in
28
- your images. The trick is putting it to use.
29
-
30
- That's where this IPTCInfo Python module comes into play. You can embed
31
- information using many programs, including Adobe Photoshop, and
32
- IPTCInfo will let your web server -- and other automated server
33
- programs -- pull it back out. You can use the information directly in
34
- Python programs, export it to XML, or even export SQL statements ready
35
- to be fed into a database.
36
-
37
- Usage
38
- -----
39
-
40
- ``from iptcinfo3 import IPTCInfo``
41
-
42
-
43
- Create new info object
44
- ``info = IPTCInfo('doge.jpg')``
45
-
46
- Print list of keywords, supplemental categories, contacts
47
- ``print(info['keywords'])``
48
- ``print(info['supplementalCategories'])``
49
- ``print(info['contacts'])``
50
-
51
- Get specific attributes...
52
- ``caption = info['caption/abstract']``
53
-
54
- Create object for file that may not have IPTC data
55
- ``info = IPTCInfo('such_iptc.jpg', force=True)``
56
-
57
- Add/change an attribute
58
- ``info['caption/abstract'] = 'Witty caption here'``
59
- ``info['supplemental category'] = ['portrait']``
60
-
61
- Save new info to file
62
- ``info.save()``
63
- ``info.save_as('very_meta.jpg')``
64
-
65
- Platform: any
66
- Classifier: Development Status :: 3 - Alpha
67
- Classifier: License :: OSI Approved :: Artistic License
68
- Classifier: License :: OSI Approved :: GNU General Public License (GPL)
69
- Classifier: Intended Audience :: Developers
70
- Classifier: Programming Language :: Python
71
- Classifier: Topic :: Multimedia :: Graphics
72
- Classifier: Topic :: Utilities
@@ -1,8 +0,0 @@
1
- README.rst
2
- iptcinfo3.py
3
- setup.cfg
4
- setup.py
5
- IPTCInfo3.egg-info/PKG-INFO
6
- IPTCInfo3.egg-info/SOURCES.txt
7
- IPTCInfo3.egg-info/dependency_links.txt
8
- IPTCInfo3.egg-info/top_level.txt
@@ -1 +0,0 @@
1
- iptcinfo3
IPTCInfo3-2.1.4/PKG-INFO DELETED
@@ -1,72 +0,0 @@
1
- Metadata-Version: 1.2
2
- Name: IPTCInfo3
3
- Version: 2.1.4
4
- Summary: A great way to get IPTCInfo
5
- Home-page: https://github.com/jamesacampbell/iptcinfo3
6
- Author: Tamas Gulacsi
7
- Author-email: gthomas@fw.hu
8
- Maintainer: James Campbell
9
- Maintainer-email: jc@normail.co
10
- License: http://www.opensource.org/licenses/gpl-license.php
11
- Download-URL: https://github.com/jamesacampbell/iptcinfo3
12
- Description: ``IPTCINFO 3``
13
- ==============
14
-
15
- `Build Status <https://travis-ci.org/crccheck/iptcinfo3.svg?branch=master>`_
16
-
17
- Like IPTCInfo but finally compatible for Python 3
18
- -------------------------------------------------
19
-
20
-
21
- Ported from Josh Carter's Perl IPTCInfo-1.9.pm by Tamas Gulacsi
22
-
23
- Ever wish you add information to your photos like a caption, the place
24
- you took it, the date, and perhaps even keywords and categories? You
25
- already can. The International Press Telecommunications Council (IPTC)
26
- defines a format for exchanging meta-information in news content, and
27
- that includes photographs. You can embed all kinds of information in
28
- your images. The trick is putting it to use.
29
-
30
- That's where this IPTCInfo Python module comes into play. You can embed
31
- information using many programs, including Adobe Photoshop, and
32
- IPTCInfo will let your web server -- and other automated server
33
- programs -- pull it back out. You can use the information directly in
34
- Python programs, export it to XML, or even export SQL statements ready
35
- to be fed into a database.
36
-
37
- Usage
38
- -----
39
-
40
- ``from iptcinfo3 import IPTCInfo``
41
-
42
-
43
- Create new info object
44
- ``info = IPTCInfo('doge.jpg')``
45
-
46
- Print list of keywords, supplemental categories, contacts
47
- ``print(info['keywords'])``
48
- ``print(info['supplementalCategories'])``
49
- ``print(info['contacts'])``
50
-
51
- Get specific attributes...
52
- ``caption = info['caption/abstract']``
53
-
54
- Create object for file that may not have IPTC data
55
- ``info = IPTCInfo('such_iptc.jpg', force=True)``
56
-
57
- Add/change an attribute
58
- ``info['caption/abstract'] = 'Witty caption here'``
59
- ``info['supplemental category'] = ['portrait']``
60
-
61
- Save new info to file
62
- ``info.save()``
63
- ``info.save_as('very_meta.jpg')``
64
-
65
- Platform: any
66
- Classifier: Development Status :: 3 - Alpha
67
- Classifier: License :: OSI Approved :: Artistic License
68
- Classifier: License :: OSI Approved :: GNU General Public License (GPL)
69
- Classifier: Intended Audience :: Developers
70
- Classifier: Programming Language :: Python
71
- Classifier: Topic :: Multimedia :: Graphics
72
- Classifier: Topic :: Utilities
IPTCInfo3-2.1.4/setup.cfg DELETED
@@ -1,15 +0,0 @@
1
- [flake8]
2
- max-line-length = 100
3
-
4
- [coverage:run]
5
- branch = True
6
- source = .
7
-
8
- [coverage:report]
9
- show_missing = True
10
- skip_covered = True
11
-
12
- [egg_info]
13
- tag_build =
14
- tag_date = 0
15
-
IPTCInfo3-2.1.4/setup.py DELETED
@@ -1,63 +0,0 @@
1
- """SETUP THIS STUFF."""
2
- import sys
3
- from distutils.command.sdist import sdist as _sdist
4
- from os import path
5
-
6
- from setuptools import setup
7
- classifiers = """
8
- Development Status :: 3 - Alpha
9
- License :: OSI Approved :: Artistic License
10
- License :: OSI Approved :: GNU General Public License (GPL)
11
- Intended Audience :: Developers
12
- Programming Language :: Python
13
- Topic :: Multimedia :: Graphics
14
- Topic :: Utilities
15
- """
16
-
17
- this_directory = path.abspath(path.dirname(__file__))
18
- with open(path.join(this_directory, 'README.rst')) as f:
19
- long_description = f.read()
20
-
21
- if sys.version_info < (2, 3):
22
- _setup = setup
23
-
24
- def setup(**kwargs):
25
- if "classifiers" in kwargs:
26
- del kwargs["classifiers"]
27
- _setup(**kwargs)
28
-
29
-
30
- class sdist(_sdist, object):
31
- def run(self):
32
- import os
33
- res = _sdist.run(self)
34
- print((self.get_archive_files()))
35
- for fn in self.get_archive_files():
36
- os.system('scp -p %s gtmainbox:/var/www/html/python/' % fn)
37
- return res
38
-
39
-
40
- def openfile(fname):
41
- import os
42
- return open(os.path.join(os.path.dirname(__file__), fname))
43
-
44
-
45
- version = next((row.split('=', 1)[-1].strip().strip("'").strip('"')
46
- for row in open('iptcinfo3.py', 'r')
47
- if row.startswith('__version__')))
48
- setup( # cmdclass={'sdist': sdist},
49
- name='IPTCInfo3',
50
- version=version,
51
- url='https://github.com/jamesacampbell/iptcinfo3',
52
- download_url='https://github.com/jamesacampbell/iptcinfo3',
53
- author='Tamas Gulacsi',
54
- author_email='gthomas@fw.hu',
55
- maintainer='James Campbell',
56
- maintainer_email='jc@normail.co',
57
- long_description=long_description,
58
- license='http://www.opensource.org/licenses/gpl-license.php',
59
- platforms=['any'],
60
- description="""A great way to get IPTCInfo""",
61
- classifiers=[_f for _f in classifiers.split('\n') if _f],
62
- py_modules=['iptcinfo3'],
63
- )