Compare commits

..

23 Commits

Author SHA1 Message Date
Rhet Turnbull
2041789ff4 Updated README.md [skip ci] 2021-08-15 14:12:15 -07:00
Rhet Turnbull
aec86f93ea Added inspect() to repl, closes #501 2021-08-15 13:50:37 -07:00
Rhet Turnbull
57bfb03e05 Updated CHANGELOG.md [skip ci] 2021-08-02 05:55:19 -07:00
Rhet Turnbull
c2b2476e38 Updated docs for Text Detection [skip ci] 2021-08-02 05:52:48 -07:00
Rhet Turnbull
fa2027d453 Improved caching of detected_text results 2021-08-02 05:10:26 -07:00
Rhet Turnbull
9d980e4917 Updated CHANGELOG.md [skip ci] 2021-07-29 21:27:51 -07:00
Rhet Turnbull
673243c6cd Fix for #500, check for macOS version before loading Vision 2021-07-29 21:16:33 -07:00
Rhet Turnbull
7376223eb8 Updated text_detection to detect macOS version 2021-07-29 07:39:01 -07:00
Rhet Turnbull
ecd0b8e22f Updated detected_text docs to make it clear this only works on Catalina+ 2021-07-29 07:03:04 -07:00
Rhet Turnbull
c4a608b5bd Updated CHANGELOG.md [skip ci] 2021-07-29 07:02:38 -07:00
Rhet Turnbull
4d9e21ea16 Added error logging to PhotoInfo.detected_text 2021-07-29 06:32:07 -07:00
Rhet Turnbull
1ee3e035c4 Updated README.md [skip ci] 2021-07-29 06:25:59 -07:00
Rhet Turnbull
b1c0fb3e82 Added error logging to {detected_text} processing, #499 2021-07-29 06:23:02 -07:00
Rhet Turnbull
de715d2afd Updated CHANGELOG.md [skip ci] 2021-07-28 06:36:10 -07:00
Rhet Turnbull
607cf80dda Removed unneeded test file [skip ci] 2021-07-28 06:27:44 -07:00
Rhet Turnbull
0c8fbd69af Updated dependencies 2021-07-28 06:21:21 -07:00
Rhet Turnbull
c2335236be Added {detected_text} template 2021-07-27 06:08:49 -07:00
Rhet Turnbull
123340eada Added PhotoInfo.detected_text() 2021-07-25 18:34:59 -07:00
Rhet Turnbull
852a06f99b Updated docs 2021-07-24 21:30:52 -07:00
Rhet Turnbull
9f8da5c623 Updated CHANGELOG.md [skip ci] 2021-07-24 21:30:03 -07:00
Rhet Turnbull
077d577c98 Fixed {album_seq} and {folder_album_seq} help text 2021-07-24 20:53:59 -07:00
Rhet Turnbull
12f39dbaf5 Added {album_seq} and {folder_album_seq}, #496 2021-07-24 20:41:31 -07:00
Rhet Turnbull
6e9f709279 Updated CHANGELOG.md [skip ci] 2021-07-23 06:14:35 -07:00
30 changed files with 873 additions and 363 deletions

View File

@@ -4,6 +4,51 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.42.72](https://github.com/RhetTbull/osxphotos/compare/v0.42.71...v0.42.72)
> 2 August 2021
- Improved caching of detected_text results [`fa2027d`](https://github.com/RhetTbull/osxphotos/commit/fa2027d45308738d2335d4b5a72c3ef5c478491a)
#### [v0.42.71](https://github.com/RhetTbull/osxphotos/compare/v0.42.70...v0.42.71)
> 29 July 2021
- Updated text_detection to detect macOS version [`7376223`](https://github.com/RhetTbull/osxphotos/commit/7376223eb87a4919fd54cc685a3f263e83626879)
- Updated detected_text docs to make it clear this only works on Catalina+ [`ecd0b8e`](https://github.com/RhetTbull/osxphotos/commit/ecd0b8e22f8bf1f8d1e98d64834bebf0394dd903)
- Fix for #500, check for macOS version before loading Vision [`673243c`](https://github.com/RhetTbull/osxphotos/commit/673243c6cd1c267b6b741b5429cdb63c062648d1)
#### [v0.42.70](https://github.com/RhetTbull/osxphotos/compare/v0.42.69...v0.42.70)
> 29 July 2021
- Added error logging to {detected_text} processing, #499 [`b1c0fb3`](https://github.com/RhetTbull/osxphotos/commit/b1c0fb3e8284600394ddbfdd7dfa94916a843c81)
- Updated README.md [skip ci] [`1ee3e03`](https://github.com/RhetTbull/osxphotos/commit/1ee3e035c42d687158f7cf73382f0f263516dc37)
- Removed unneeded test file [skip ci] [`607cf80`](https://github.com/RhetTbull/osxphotos/commit/607cf80dda37ad529edd91fe92af3885b04b9a37)
#### [v0.42.69](https://github.com/RhetTbull/osxphotos/compare/v0.42.67...v0.42.69)
> 28 July 2021
- Added {detected_text} template [`c233523`](https://github.com/RhetTbull/osxphotos/commit/c2335236be7a1eecf4f25a9dcb844df4d6372b5c)
- Added PhotoInfo.detected_text() [`123340e`](https://github.com/RhetTbull/osxphotos/commit/123340eadabb0fb07209c4207ccad13a53de3619)
- Updated dependencies [`0c8fbd6`](https://github.com/RhetTbull/osxphotos/commit/0c8fbd69af7a0d696de5224bf3c302e0c240905f)
#### [v0.42.67](https://github.com/RhetTbull/osxphotos/compare/v0.42.66...v0.42.67)
> 24 July 2021
- Added {album_seq} and {folder_album_seq}, #496 [`12f39db`](https://github.com/RhetTbull/osxphotos/commit/12f39dbaf520ad767e3da667257ce00af60fdd7e)
- Fixed {album_seq} and {folder_album_seq} help text [`077d577`](https://github.com/RhetTbull/osxphotos/commit/077d577c9890c4840a60c3e450dcd4167aa669ea)
#### [v0.42.66](https://github.com/RhetTbull/osxphotos/compare/v0.42.65...v0.42.66)
> 23 July 2021
- Updated docs [`666b6ca`](https://github.com/RhetTbull/osxphotos/commit/666b6cac33fb8a2d0fc602609f11e190e11c538f)
- Added {id} sequence number template, #154 [`e95c096`](https://github.com/RhetTbull/osxphotos/commit/e95c0967846106f6da2adaa0b85520df8b351bb0)
- Updated example [skip ci] [`8216c33`](https://github.com/RhetTbull/osxphotos/commit/8216c33b596dba35007168cda4e8de34d9f4b2ea)
#### [v0.42.65](https://github.com/RhetTbull/osxphotos/compare/v0.42.64...v0.42.65)
> 20 July 2021

299
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
config: 37d92c35f55b2e7d711392f3f43dd1ef
config: 23e7c9cd300c96ffa7fce04034b83f61
tags: 645f666f9bcd5a90fca523b33c5a78b7

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Overview: module code &#8212; osxphotos 0.42.66 documentation</title>
<title>Overview: module code &#8212; osxphotos 0.42.69 documentation</title>
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../_static/alabaster.css" />
<script data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_export &#8212; osxphotos 0.42.66 documentation</title>
<title>osxphotos.photoinfo._photoinfo_export &#8212; osxphotos 0.42.69 documentation</title>
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.42.66 documentation</title>
<title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.42.69 documentation</title>
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
@@ -63,12 +63,14 @@
<span class="n">BURST_KEY</span><span class="p">,</span>
<span class="n">BURST_NOT_SELECTED</span><span class="p">,</span>
<span class="n">BURST_SELECTED</span><span class="p">,</span>
<span class="n">TEXT_DETECTION_CONFIDENCE_THRESHOLD</span><span class="p">,</span>
<span class="p">)</span>
<span class="kn">from</span> <span class="nn">..adjustmentsinfo</span> <span class="kn">import</span> <span class="n">AdjustmentsInfo</span>
<span class="kn">from</span> <span class="nn">..albuminfo</span> <span class="kn">import</span> <span class="n">AlbumInfo</span><span class="p">,</span> <span class="n">ImportInfo</span>
<span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">FaceInfo</span><span class="p">,</span> <span class="n">PersonInfo</span>
<span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">PhotoTemplate</span><span class="p">,</span> <span class="n">RenderOptions</span>
<span class="kn">from</span> <span class="nn">..placeinfo</span> <span class="kn">import</span> <span class="n">PlaceInfo4</span><span class="p">,</span> <span class="n">PlaceInfo5</span>
<span class="kn">from</span> <span class="nn">..text_detection</span> <span class="kn">import</span> <span class="n">detect_text</span>
<span class="kn">from</span> <span class="nn">..uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span><span class="p">,</span> <span class="n">get_uti_for_extension</span>
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">_debug</span><span class="p">,</span> <span class="n">_get_resource_loc</span><span class="p">,</span> <span class="n">findfiles</span>
@@ -1139,6 +1141,41 @@
<span class="n">template</span> <span class="o">=</span> <span class="n">PhotoTemplate</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">exiftool_path</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_exiftool_path</span><span class="p">)</span>
<span class="k">return</span> <span class="n">template</span><span class="o">.</span><span class="n">render</span><span class="p">(</span><span class="n">template_str</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span></div>
<div class="viewcode-block" id="PhotoInfo.detected_text"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotoInfo.detected_text">[docs]</a> <span class="k">def</span> <span class="nf">detected_text</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="o">=</span><span class="n">TEXT_DETECTION_CONFIDENCE_THRESHOLD</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Detects text in photo and returns lists of results as (detected text, confidence)</span>
<span class="sd"> confidence_threshold: float between 0.0 and 1.0. If text detection confidence is below this threshold,</span>
<span class="sd"> text will not be returned. Default is TEXT_DETECTION_CONFIDENCE_THRESHOLD</span>
<span class="sd"> If photo is edited, uses the edited photo, otherwise the original; falls back to the preview image if neither edited or original is available</span>
<span class="sd"> Returns: list of (detected text, confidence) tuples</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">path</span> <span class="o">=</span> <span class="p">(</span>
<span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span> <span class="k">else</span> <span class="bp">self</span><span class="o">.</span><span class="n">path</span>
<span class="p">)</span>
<span class="n">path</span> <span class="o">=</span> <span class="n">path</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_derivatives</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_derivatives</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">path</span><span class="p">:</span>
<span class="k">return</span> <span class="p">[]</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span>
<span class="k">except</span> <span class="p">(</span><span class="ne">AttributeError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">e</span><span class="p">,</span> <span class="ne">AttributeError</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">detect_text</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">confidence</span><span class="p">)</span>
<span class="k">for</span> <span class="n">text</span><span class="p">,</span> <span class="n">confidence</span> <span class="ow">in</span> <span class="n">detected_text</span>
<span class="k">if</span> <span class="n">confidence</span> <span class="o">&gt;=</span> <span class="n">confidence_threshold</span>
<span class="p">]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span></div>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">_longitude</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Returns longitude, in degrees&quot;&quot;&quot;</span>

View File

@@ -1,6 +1,6 @@
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
VERSION: '0.42.66',
VERSION: '0.42.69',
LANGUAGE: 'None',
COLLAPSE_INDEX: false,
BUILDER: 'html',

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos command line interface (CLI) &#8212; osxphotos 0.42.66 documentation</title>
<title>osxphotos command line interface (CLI) &#8212; osxphotos 0.42.69 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Index &#8212; osxphotos 0.42.66 documentation</title>
<title>Index &#8212; osxphotos 0.42.69 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
@@ -1308,10 +1308,10 @@
</li>
<li><a href="reference.html#osxphotos.PhotosDB.db_path">db_path (osxphotos.PhotosDB property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotosDB.db_version">db_version (osxphotos.PhotosDB property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.description">description (osxphotos.PhotoInfo property)</a>
</li>
<li>
@@ -1321,6 +1321,8 @@
<li><a href="cli.html#cmdoption-osxphotos-export-arg-DEST">osxphotos-export command line option</a>
</li>
</ul></li>
<li><a href="reference.html#osxphotos.PhotoInfo.detected_text">detected_text() (osxphotos.PhotoInfo method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.duplicates">duplicates (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.ExifInfo.duration">duration (osxphotos.PhotoInfo.ExifInfo attribute)</a>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.42.66 documentation</title>
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.42.69 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos &#8212; osxphotos 0.42.66 documentation</title>
<title>osxphotos &#8212; osxphotos 0.42.69 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

Binary file not shown.

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos package &#8212; osxphotos 0.42.66 documentation</title>
<title>osxphotos package &#8212; osxphotos 0.42.69 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
@@ -757,6 +757,16 @@ or None if no modification date set</p>
<dd><p>long / extended description of picture</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.detected_text">
<span class="sig-name descname"><span class="pre">detected_text</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">confidence_threshold</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">0.75</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photoinfo/photoinfo.html#PhotoInfo.detected_text"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotoInfo.detected_text" title="Permalink to this definition"></a></dt>
<dd><p>Detects text in photo and returns lists of results as (detected text, confidence)</p>
<p>confidence_threshold: float between 0.0 and 1.0. If text detection confidence is below this threshold,
text will not be returned. Default is TEXT_DETECTION_CONFIDENCE_THRESHOLD</p>
<p>If photo is edited, uses the edited photo, otherwise the original; falls back to the preview image if neither edited or original is available</p>
<p>Returns: list of (detected text, confidence) tuples</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.duplicates">
<em class="property"><span class="pre">property</span> </em><span class="sig-name descname"><span class="pre">duplicates</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.duplicates" title="Permalink to this definition"></a></dt>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Search &#8212; osxphotos 0.42.66 documentation</title>
<title>Search &#8212; osxphotos 0.42.69 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />

File diff suppressed because one or more lines are too long

View File

@@ -282,3 +282,5 @@ class AlbumSortOrder(Enum):
NEWEST_FIRST = 2
OLDEST_FIRST = 3
TITLE = 5
TEXT_DETECTION_CONFIDENCE_THRESHOLD = 0.75

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.42.66"
__version__ = "0.42.73"

View File

@@ -1796,6 +1796,7 @@ def export(
export_dir=dest,
dry_run=dry_run,
exiftool_path=exiftool_path,
export_db=export_db,
)
if album_export and export_results.exported:
@@ -1865,6 +1866,7 @@ def export(
finder_tag_template=finder_tag_template,
strip=strip,
export_dir=dest,
export_db=export_db,
)
results.xattr_written.extend(tags_written)
results.xattr_skipped.extend(tags_skipped)
@@ -1876,6 +1878,7 @@ def export(
xattr_template,
strip=strip,
export_dir=dest,
export_db=export_db,
)
results.xattr_written.extend(xattr_written)
results.xattr_skipped.extend(xattr_skipped)
@@ -2538,10 +2541,22 @@ def export_photo(
sidecar_flags |= SIDECAR_EXIFTOOL
rendered_suffix = _render_suffix_template(
original_suffix, "original_suffix", "--original-suffix", strip, dest, photo
original_suffix,
"original_suffix",
"--original-suffix",
strip,
dest,
photo,
export_db,
)
rendered_preview_suffix = _render_suffix_template(
preview_suffix, "preview_suffix", "--preview-suffix", strip, dest, photo
preview_suffix,
"preview_suffix",
"--preview-suffix",
strip,
dest,
photo,
export_db,
)
# if download_missing and the photo is missing or path doesn't exist,
@@ -2557,11 +2572,24 @@ def export_photo(
results = ExportResults()
dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=strip, edited=False
photo,
directory,
export_by_date,
dest,
dry_run,
strip=strip,
edited=False,
export_db=export_db,
)
for dest_path in dest_paths:
filenames = get_filenames_from_template(
photo, filename_template, dest, dest_path, original_name, strip=strip
photo,
filename_template,
dest,
dest_path,
original_name,
strip=strip,
export_db=export_db,
)
for filename in filenames:
@@ -2632,12 +2660,26 @@ def export_photo(
if export_edited and photo.hasadjustments:
dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=strip, edited=True
photo,
directory,
export_by_date,
dest,
dry_run,
strip=strip,
edited=True,
export_db=export_db,
)
for dest_path in dest_paths:
# if export-edited, also export the edited version
edited_filenames = get_filenames_from_template(
photo, filename_template, dest, dest_path, original_name, strip=strip, edited=True
photo,
filename_template,
dest,
dest_path,
original_name,
strip=strip,
edited=True,
export_db=export_db,
)
for edited_filename in edited_filenames:
edited_filename = pathlib.Path(edited_filename)
@@ -2674,6 +2716,7 @@ def export_photo(
strip,
dest,
photo,
export_db,
)
edited_filename = (
f"{edited_filename.stem}{rendered_edited_suffix}{edited_ext}"
@@ -2729,7 +2772,9 @@ def export_photo(
return results
def _render_suffix_template(suffix_template, var_name, option_name, strip, dest, photo):
def _render_suffix_template(
suffix_template, var_name, option_name, strip, dest, photo, export_db
):
"""render suffix template
Returns:
@@ -2739,7 +2784,9 @@ def _render_suffix_template(suffix_template, var_name, option_name, strip, dest,
return ""
try:
options = RenderOptions(filename=True, strip=strip, export_dir=dest)
options = RenderOptions(
filename=True, strip=strip, export_dir=dest, exportdb=export_db
)
rendered_suffix, unmatched = photo.render_template(suffix_template, options)
except ValueError as e:
raise click.BadOptionUsage(
@@ -2848,7 +2895,9 @@ def export_photo_to_directory(
results.missing.append(str(pathlib.Path(dest_path) / filename))
return results
render_options = RenderOptions(export_dir=export_dir, dest_path=dest_path)
render_options = RenderOptions(
export_dir=export_dir, dest_path=dest_path, exportdb=export_db
)
tries = 0
while tries <= retry:
@@ -2960,6 +3009,7 @@ def get_filenames_from_template(
original_name,
strip=False,
edited=False,
export_db=None,
):
"""get list of export filenames for a photo
@@ -2987,6 +3037,7 @@ def get_filenames_from_template(
edited_version=edited,
export_dir=export_dir,
dest_path=dest_path,
exportdb=export_db,
)
filenames, unmatched = photo.render_template(filename_template, options)
except ValueError as e:
@@ -3011,7 +3062,14 @@ def get_filenames_from_template(
def get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=False, edited=False
photo,
directory,
export_by_date,
dest,
dry_run,
strip=False,
edited=False,
export_db=None,
):
"""get list of directories to export a photo into, creates directories if they don't exist
@@ -3042,7 +3100,9 @@ def get_dirnames_from_template(
elif directory:
# got a directory template, render it and check results are valid
try:
options = RenderOptions(dirname=True, strip=strip, edited_version=edited)
options = RenderOptions(
dirname=True, strip=strip, edited_version=edited, exportdb=export_db
)
dirnames, unmatched = photo.render_template(directory, options)
except ValueError as e:
raise click.BadOptionUsage(
@@ -3325,6 +3385,7 @@ def write_finder_tags(
finder_tag_template=None,
strip=False,
export_dir=None,
export_db=None,
):
"""Write Finder tags (extended attributes) to files; only writes attributes if attributes on file differ from what would be written
@@ -3338,6 +3399,7 @@ def write_finder_tags(
exiftool_merge_keywords: if True, include any keywords in the exif data of the source image as keywords
finder_tag_template: list of templates to evaluate for determining Finder tags
export_dir: value to use for {export_dir} template
export_db: an ExportDB object
Returns:
(list of file paths that were updated with new Finder tags, list of file paths skipped because Finder tags didn't need updating)
@@ -3369,6 +3431,7 @@ def write_finder_tags(
path_sep="/",
strip=strip,
export_dir=export_dir,
exportdb=export_db,
)
rendered, unmatched = photo.render_template(template_str, options)
except ValueError as e:
@@ -3408,7 +3471,12 @@ def write_finder_tags(
def write_extended_attributes(
photo, files, xattr_template, strip=False, export_dir=None
photo,
files,
xattr_template,
strip=False,
export_dir=None,
export_db=None,
):
"""Writes extended attributes to exported files
@@ -3416,6 +3484,7 @@ def write_extended_attributes(
photo: a PhotoInfo object
strip: xattr_template: list of tuples: (attribute name, attribute template)
export_dir: value to use for {export_dir} template
exportdb: an ExportDB object
Returns:
tuple(list of file paths that were updated with new attributes, list of file paths skipped because attributes didn't need updating)
@@ -3429,6 +3498,7 @@ def write_extended_attributes(
path_sep="/",
strip=strip,
export_dir=export_dir,
exportdb=export_db,
)
rendered, unmatched = photo.render_template(template_str, options)
except ValueError as e:
@@ -3480,7 +3550,7 @@ def write_extended_attributes(
def run_post_command(
photo, post_command, export_results, export_dir, dry_run, exiftool_path
photo, post_command, export_results, export_dir, dry_run, exiftool_path, export_db
):
# todo: pass in RenderOptions from export? (e.g. so it contains strip, etc?)
# todo: need a shell_quote template type:
@@ -3492,7 +3562,9 @@ def run_post_command(
# some categories, like error, return a tuple of (file, error str)
if isinstance(f, tuple):
f = f[0]
render_options = RenderOptions(export_dir=export_dir, filepath=f)
render_options = RenderOptions(
export_dir=export_dir, filepath=f, exportdb=export_db
)
template = PhotoTemplate(photo, exiftool_path=exiftool_path)
command, _ = template.render(command_template, options=render_options)
command = command[0] if command else None
@@ -4002,6 +4074,10 @@ def _get_selected(photosdb):
@click.pass_context
def repl(ctx, cli_obj, db):
"""Run interactive osxphotos shell"""
from osxphotos import PhotosDB, PhotoInfo, ExifTool
from rich import inspect as _inspect
pretty.install()
print(f"python version: {sys.version}")
print(f"osxphotos version: {osxphotos._version.__version__}")
@@ -4018,6 +4094,10 @@ def repl(ctx, cli_obj, db):
show = _show_photo
get_selected = _get_selected(photosdb)
def inspect(obj):
"""inspect object"""
return _inspect(obj, methods=True)
print(f"Found {len(photos)} photos in {tictoc:0.2f} seconds")
print("The following variables are defined:")
print(f"- photosdb: PhotosDB() instance for {photosdb.library_path}")
@@ -4033,5 +4113,8 @@ def repl(ctx, cli_obj, db):
print(
f"- help(object): print help text including list of methods for object; for example, help(PhotosDB)"
)
print(
f"- inspect(object): print information about an object; for example inspect(photosdb)"
)
print(f"- quit(): exit this interactive shell\n")
code.interact(banner="", local=locals())

View File

@@ -1,6 +1,4 @@
""" Helper class for managing a database used by
PhotoInfo.export for tracking state of exports and updates
"""
""" Helper class for managing a database used by PhotoInfo.export for tracking state of exports and updates """
import datetime
import logging
@@ -12,13 +10,15 @@ from abc import ABC, abstractmethod
from io import StringIO
from sqlite3 import Error
from ._constants import OSXPHOTOS_EXPORT_DB
from ._version import __version__
OSXPHOTOS_EXPORTDB_VERSION = "3.2"
OSXPHOTOS_EXPORTDB_VERSION = "4.0"
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {str(datetime.datetime.now())}"
class ExportDB_ABC(ABC):
""" abstract base class for ExportDB """
"""abstract base class for ExportDB"""
@abstractmethod
def get_uuid_for_file(self, filename):
@@ -88,6 +88,14 @@ class ExportDB_ABC(ABC):
def get_previous_uuids(self):
pass
@abstractmethod
def get_detected_text_for_uuid(self, uuid):
pass
@abstractmethod
def set_detected_text_for_uuid(self, uuid, json_text):
pass
@abstractmethod
def set_data(
self,
@@ -104,7 +112,7 @@ class ExportDB_ABC(ABC):
class ExportDBNoOp(ExportDB_ABC):
""" An ExportDB with NoOp methods """
"""An ExportDB with NoOp methods"""
def __init__(self):
self.was_created = True
@@ -162,6 +170,12 @@ class ExportDBNoOp(ExportDB_ABC):
def get_previous_uuids(self):
return []
def get_detected_text_for_uuid(self, uuid):
return None
def set_detected_text_for_uuid(self, uuid, json_text):
pass
def set_data(
self,
filename,
@@ -177,23 +191,23 @@ class ExportDBNoOp(ExportDB_ABC):
class ExportDB(ExportDB_ABC):
""" Interface to sqlite3 database used to store state information for osxphotos export command """
"""Interface to sqlite3 database used to store state information for osxphotos export command"""
def __init__(self, dbfile):
""" dbfile: path to osxphotos export database file """
"""dbfile: path to osxphotos export database file"""
self._dbfile = dbfile
# _path is parent of the database
# all files referenced by get_/set_uuid_for_file will be converted to
# relative paths to this parent _path
# this allows the entire export tree to be moved to a new disk/location
# whilst preserving the UUID to filename mappping
# whilst preserving the UUID to filename mapping
self._path = pathlib.Path(dbfile).parent
self._conn = self._open_export_db(dbfile)
self._insert_run_info()
def get_uuid_for_file(self, filename):
""" query database for filename and return UUID
returns None if filename not found in database
"""query database for filename and return UUID
returns None if filename not found in database
"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
@@ -211,7 +225,7 @@ class ExportDB(ExportDB_ABC):
return uuid
def set_uuid_for_file(self, filename, uuid):
""" set UUID of filename to uuid in the database """
"""set UUID of filename to uuid in the database"""
filename = str(pathlib.Path(filename).relative_to(self._path))
filename_normalized = filename.lower()
conn = self._conn
@@ -226,9 +240,9 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def set_stat_orig_for_file(self, filename, stats):
""" set stat info for filename
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""set stat info for filename
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
if len(stats) != 3:
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
@@ -247,8 +261,8 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_stat_orig_for_file(self, filename):
""" get stat info for filename
returns: tuple of (mode, size, mtime)
"""get stat info for filename
returns: tuple of (mode, size, mtime)
"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
@@ -272,21 +286,21 @@ class ExportDB(ExportDB_ABC):
return stats
def set_stat_edited_for_file(self, filename, stats):
""" set stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""set stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
return self._set_stat_for_file("edited", filename, stats)
def get_stat_edited_for_file(self, filename):
""" get stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""get stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
return self._get_stat_for_file("edited", filename)
def set_stat_exif_for_file(self, filename, stats):
""" set stat info for filename (after exiftool has updated it)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""set stat info for filename (after exiftool has updated it)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
if len(stats) != 3:
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
@@ -305,8 +319,8 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_stat_exif_for_file(self, filename):
""" get stat info for filename (after exiftool has updated it)
returns: tuple of (mode, size, mtime)
"""get stat info for filename (after exiftool has updated it)
returns: tuple of (mode, size, mtime)
"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
@@ -330,19 +344,19 @@ class ExportDB(ExportDB_ABC):
return stats
def set_stat_converted_for_file(self, filename, stats):
""" set stat info for filename (after image converted to jpeg)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""set stat info for filename (after image converted to jpeg)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
return self._set_stat_for_file("converted", filename, stats)
def get_stat_converted_for_file(self, filename):
""" get stat info for filename (after jpeg conversion)
returns: tuple of (mode, size, mtime)
"""get stat info for filename (after jpeg conversion)
returns: tuple of (mode, size, mtime)
"""
return self._get_stat_for_file("converted", filename)
def get_info_for_uuid(self, uuid):
""" returns the info JSON struct for a UUID """
"""returns the info JSON struct for a UUID"""
conn = self._conn
try:
c = conn.cursor()
@@ -356,7 +370,7 @@ class ExportDB(ExportDB_ABC):
return info
def set_info_for_uuid(self, uuid, info):
""" sets the info JSON struct for a UUID """
"""sets the info JSON struct for a UUID"""
conn = self._conn
try:
c = conn.cursor()
@@ -369,7 +383,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_exifdata_for_file(self, filename):
""" returns the exifdata JSON struct for a file """
"""returns the exifdata JSON struct for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
@@ -387,7 +401,7 @@ class ExportDB(ExportDB_ABC):
return exifdata
def set_exifdata_for_file(self, filename, exifdata):
""" sets the exifdata JSON struct for a file """
"""sets the exifdata JSON struct for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
@@ -401,7 +415,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_sidecar_for_file(self, filename):
""" returns the sidecar data and signature for a file """
"""returns the sidecar data and signature for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
@@ -429,7 +443,7 @@ class ExportDB(ExportDB_ABC):
return sidecar_data, sidecar_sig
def set_sidecar_for_file(self, filename, sidecar_data, sidecar_sig):
""" sets the sidecar data and signature for a file """
"""sets the sidecar data and signature for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
@@ -443,7 +457,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_previous_uuids(self):
"""returns list of UUIDs of previously exported photos found in export database """
"""returns list of UUIDs of previously exported photos found in export database"""
conn = self._conn
previous_uuids = []
try:
@@ -455,6 +469,36 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
return previous_uuids
def get_detected_text_for_uuid(self, uuid):
"""Get the detected_text for a uuid"""
conn = self._conn
try:
c = conn.cursor()
c.execute(
"SELECT text_data FROM detected_text WHERE uuid = ?",
(uuid,),
)
results = c.fetchone()
detected_text = results[0] if results else None
except Error as e:
logging.warning(e)
detected_text = None
return detected_text
def set_detected_text_for_uuid(self, uuid, text_json):
"""Set the detected text for uuid"""
conn = self._conn
try:
c = conn.cursor()
c.execute(
"INSERT OR REPLACE INTO detected_text(uuid, text_data) VALUES (?, ?);",
(uuid, text_json,),
)
conn.commit()
except Error as e:
logging.warning(e)
def set_data(
self,
filename,
@@ -466,8 +510,7 @@ class ExportDB(ExportDB_ABC):
info_json,
exif_json,
):
""" sets all the data for file and uuid at once
"""
"""sets all the data for file and uuid at once"""
filename = str(pathlib.Path(filename).relative_to(self._path))
filename_normalized = filename.lower()
conn = self._conn
@@ -510,7 +553,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def close(self):
""" close the database connection """
"""close the database connection"""
try:
self._conn.close()
except Error as e:
@@ -548,9 +591,9 @@ class ExportDB(ExportDB_ABC):
return stats
def _open_export_db(self, dbfile):
""" open export database and return a db connection
if dbfile does not exist, will create and initialize the database
returns: connection to the database
"""open export database and return a db connection
if dbfile does not exist, will create and initialize the database
returns: connection to the database
"""
if not os.path.isfile(dbfile):
@@ -573,7 +616,7 @@ class ExportDB(ExportDB_ABC):
return conn
def _get_db_connection(self, dbfile):
""" return db connection to dbname """
"""return db connection to dbname"""
try:
conn = sqlite3.connect(dbfile)
except Error as e:
@@ -583,15 +626,15 @@ class ExportDB(ExportDB_ABC):
return conn
def _get_database_version(self, conn):
""" return tuple of (osxphotos, exportdb) versions for database connection conn """
"""return tuple of (osxphotos, exportdb) versions for database connection conn"""
version_info = conn.execute(
"SELECT osxphotos, exportdb, max(id) FROM version"
).fetchone()
return (version_info[0], version_info[1])
def _create_db_tables(self, conn):
""" create (if not already created) the necessary db tables for the export database
conn: sqlite3 db connection
"""create (if not already created) the necessary db tables for the export database
conn: sqlite3 db connection
"""
sql_commands = {
"sql_version_table": """ CREATE TABLE IF NOT EXISTS version (
@@ -599,6 +642,10 @@ class ExportDB(ExportDB_ABC):
osxphotos TEXT,
exportdb TEXT
); """,
"sql_about_table": """ CREATE TABLE IF NOT EXISTS about (
id INTEGER PRIMARY KEY,
about TEXT
);""",
"sql_files_table": """ CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY,
filepath TEXT NOT NULL,
@@ -651,12 +698,18 @@ class ExportDB(ExportDB_ABC):
size INTEGER,
mtime REAL
); """,
"sql_detected_text_table": """ CREATE TABLE IF NOT EXISTS detected_text (
id INTEGER PRIMARY KEY,
uuid TEXT NOT NULL,
text_data JSON
); """,
"sql_files_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_files_filepath_normalized on files (filepath_normalized); """,
"sql_info_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_info_uuid on info (uuid); """,
"sql_exifdata_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_exifdata_filename on exifdata (filepath_normalized); """,
"sql_edited_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_edited_filename on edited (filepath_normalized);""",
"sql_converted_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_converted_filename on converted (filepath_normalized);""",
"sql_sidecar_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_sidecar_filename on sidecar (filepath_normalized);""",
"sql_detected_text_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_detected_text on detected_text (uuid);""",
}
try:
c = conn.cursor()
@@ -666,12 +719,13 @@ class ExportDB(ExportDB_ABC):
"INSERT INTO version(osxphotos, exportdb) VALUES (?, ?);",
(__version__, OSXPHOTOS_EXPORTDB_VERSION),
)
c.execute("INSERT INTO about(about) VALUES (?);", (OSXPHOTOS_ABOUT_STRING,))
conn.commit()
except Error as e:
logging.warning(e)
def __del__(self):
""" ensure the database connection is closed """
"""ensure the database connection is closed"""
try:
self._conn.close()
except:
@@ -696,35 +750,33 @@ class ExportDB(ExportDB_ABC):
class ExportDBInMemory(ExportDB):
""" In memory version of ExportDB
Copies the on-disk database into memory so it may be operated on without
modifying the on-disk verison
"""In memory version of ExportDB
Copies the on-disk database into memory so it may be operated on without
modifying the on-disk version
"""
def init(self, dbfile):
self._dbfile = dbfile
def __init__(self, dbfile):
self._dbfile = dbfile or f"./{OSXPHOTOS_EXPORT_DB}"
# _path is parent of the database
# all files referenced by get_/set_uuid_for_file will be converted to
# relative paths to this parent _path
# this allows the entire export tree to be moved to a new disk/location
# whilst preserving the UUID to filename mappping
self._path = pathlib.Path(dbfile).parent
self._conn = self._open_export_db(dbfile)
# whilst preserving the UUID to filename mapping
self._path = pathlib.Path(self._dbfile).parent
self._conn = self._open_export_db(self._dbfile)
self._insert_run_info()
def _open_export_db(self, dbfile):
""" open export database and return a db connection
returns: connection to the database
"""open export database and return a db connection
returns: connection to the database
"""
if not os.path.isfile(dbfile):
conn = self._get_db_connection()
if conn:
self._create_db_tables(conn)
self.was_created = True
self.was_upgraded = ()
self.version = OSXPHOTOS_EXPORTDB_VERSION
else:
if not conn:
raise Exception("Error getting connection to in-memory database")
self._create_db_tables(conn)
self.was_created = True
self.was_upgraded = ()
else:
try:
conn = sqlite3.connect(dbfile)
@@ -749,12 +801,11 @@ class ExportDBInMemory(ExportDB):
self.was_upgraded = (exportdb_ver, OSXPHOTOS_EXPORTDB_VERSION)
else:
self.was_upgraded = ()
self.version = OSXPHOTOS_EXPORTDB_VERSION
self.version = OSXPHOTOS_EXPORTDB_VERSION
return conn
def _get_db_connection(self):
""" return db connection to in memory database """
"""return db connection to in memory database"""
try:
conn = sqlite3.connect(":memory:")
except Error as e:

View File

@@ -14,6 +14,7 @@ from datetime import timedelta, timezone
from typing import Optional
import yaml
from osxmetadata import OSXMetaData
from .._constants import (
_MOVIE_TYPE,
@@ -30,12 +31,14 @@ from .._constants import (
BURST_KEY,
BURST_NOT_SELECTED,
BURST_SELECTED,
TEXT_DETECTION_CONFIDENCE_THRESHOLD,
)
from ..adjustmentsinfo import AdjustmentsInfo
from ..albuminfo import AlbumInfo, ImportInfo
from ..personinfo import FaceInfo, PersonInfo
from ..phototemplate import PhotoTemplate, RenderOptions
from ..placeinfo import PlaceInfo4, PlaceInfo5
from ..text_detection import detect_text
from ..uti import get_preferred_uti_extension, get_uti_for_extension
from ..utils import _debug, _get_resource_loc, findfiles
@@ -1106,6 +1109,52 @@ class PhotoInfo:
template = PhotoTemplate(self, exiftool_path=self._db._exiftool_path)
return template.render(template_str, options)
def detected_text(self, confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
"""Detects text in photo and returns lists of results as (detected text, confidence)
confidence_threshold: float between 0.0 and 1.0. If text detection confidence is below this threshold,
text will not be returned. Default is TEXT_DETECTION_CONFIDENCE_THRESHOLD
If photo is edited, uses the edited photo, otherwise the original; falls back to the preview image if neither edited or original is available
Returns: list of (detected text, confidence) tuples
"""
try:
return self._detected_text_cache[confidence_threshold]
except (AttributeError, KeyError) as e:
if isinstance(e, AttributeError):
self._detected_text_cache = {}
try:
detected_text = self._detected_text()
except Exception as e:
logging.warning(f"Error detecting text in photo {self.uuid}: {e}")
detected_text = []
self._detected_text_cache[confidence_threshold] = [
(text, confidence)
for text, confidence in detected_text
if confidence >= confidence_threshold
]
return self._detected_text_cache[confidence_threshold]
def _detected_text(self):
"""detect text in photo, either from cached extended attribute or by attempting text detection"""
path = (
self.path_edited if self.hasadjustments and self.path_edited else self.path
)
path = path or self.path_derivatives[0] if self.path_derivatives else None
if not path:
return []
md = OSXMetaData(path)
detected_text = md.get_attribute("osxphotos_detected_text")
if detected_text is None:
detected_text = detect_text(path)
md.set_attribute("osxphotos_detected_text", detected_text)
return detected_text
@property
def _longitude(self):
"""Returns longitude, in degrees"""

View File

@@ -1,7 +1,9 @@
""" Custom template system for osxphotos, implements osxphotos template language (OTL) """
import datetime
import json
import locale
import logging
import os
import pathlib
import shlex
@@ -11,11 +13,13 @@ from typing import Optional
from textx import TextXSyntaxError, metamodel_from_file
from ._constants import _UNKNOWN_PERSON
from ._constants import _UNKNOWN_PERSON, TEXT_DETECTION_CONFIDENCE_THRESHOLD
from ._version import __version__
from .datetime_formatter import DateTimeFormatter
from .exiftool import ExifToolCaching
from .export_db import ExportDB_ABC, ExportDBInMemory
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
from .text_detection import detect_text
from .utils import expand_and_validate_filepath, load_function
# TODO: a lot of values are passed from function to function like path_sep--make these all class properties
@@ -128,9 +132,28 @@ TEMPLATE_SUBSTITUTIONS = {
"{exif.lens_model}": "Lens model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'",
"{uuid}": "Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'",
"{id}": "A unique number for the photo based on its primary key in the Photos database. "
+ "A sequential integer, e.g. 1, 2, 3...etc. May be formatted using a python string format code. "
+ "A sequential integer, e.g. 1, 2, 3...etc. Each asset associated with a photo (e.g. an image and Live Photo preview) will share the same id. "
+ "May be formatted using a python string format code. "
+ "For example, to format as a 5-digit integer and pad with zeros, use '{id:05d}' which results in "
+ "00001, 00002, 00003...etc. ",
"{album_seq}": "An integer, starting at 0, indicating the photo's index (sequence) in the containing album. "
+ "Only valid when used in a '--filename' template and only when '{album}' or '{folder_album}' is used in the '--directory' template. "
+ 'For example \'--directory "{folder_album}" --filename "{album_seq}_{original_name}"\'. '
+ "To start counting at a value other than 0, append append a period and the starting value to the field name. "
+ "For example, to start counting at 1 instead of 0: '{album_seq.1}'. "
+ "May be formatted using a python string format code. "
+ "For example, to format as a 5-digit integer and pad with zeros, use '{album_seq:05d}' which results in "
+ "00000, 00001, 00002...etc. "
+ "This may result in incorrect sequences if you have duplicate albums with the same name; see also '{folder_album_seq}'.",
"{folder_album_seq}": "An integer, starting at 0, indicating the photo's index (sequence) in the containing album and folder path. "
+ "Only valid when used in a '--filename' template and only when '{folder_album}' is used in the '--directory' template. "
+ 'For example \'--directory "{folder_album}" --filename "{folder_album_seq}_{original_name}"\'. '
+ "To start counting at a value other than 0, append append a period and the starting value to the field name. "
+ "For example, to start counting at 1 instead of 0: '{folder_album_seq.1}' "
+ "May be formatted using a python string format code. "
+ "For example, to format as a 5-digit integer and pad with zeros, use '{folder_album_seq:05d}' which results in "
+ "00000, 00001, 00002...etc. "
+ "This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '{album_seq}'.",
"{comma}": "A comma: ','",
"{semicolon}": "A semicolon: ';'",
"{questionmark}": "A question mark: '?'",
@@ -178,6 +201,13 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
+ "For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. "
+ "'{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of "
+ "the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
"{detected_text}": "List of text strings found in the image after performing text detection. "
+ "Using '{detected_text}' will cause osxphotos to perform text detection on your photos using the built-in macOS text detection algorithms which will slow down your export. "
+ "The results for each photo will be cached in the export database so that future exports with '--update' do not need to reprocess each photo. "
+ "You may pass a confidence threshold value between 0.0 and 1.0 after a colon as in '{detected_text:0.5}'; "
+ f"The default confidence threshold is {TEXT_DETECTION_CONFIDENCE_THRESHOLD}. "
+ "'{detected_text}' works only on macOS Catalina (10.15) or later. "
+ "Note: this feature is not the same thing as Live Text in macOS Monterey, which osxphotos does not yet support.",
"{shell_quote}": "Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.",
"{function}": "Execute a python function from an external file and use return value as template substitution. "
+ "Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. "
@@ -257,6 +287,7 @@ class RenderOptions:
dest_path: set to the destination path of the photo (for use by {function} template), only valid with --filename
filepath: set to value for filepath of the exported photo if you want to evaluate {filepath} template
quote: quote path templates for execution in the shell
exportdb: ExportDB object
"""
none_str: str = "_"
@@ -271,6 +302,7 @@ class RenderOptions:
dest_path: Optional[str] = None
filepath: Optional[str] = None
quote: bool = False
exportdb: Optional[ExportDB_ABC] = None
class PhotoTemplateParser:
@@ -297,13 +329,10 @@ class PhotoTemplateParser:
"""Parse a template_statement string"""
return self.metamodel.model_from_str(template_statement)
def format_id_str(value, format_str):
"""Format value based on format code in field in format id:02d"""
if not format_str:
return str(value)
format_str = "{0:" + f"{format_str}" + "}"
return format_str.format(value)
def fields(self, template_statement):
"""Return list of fields found in a template statement; does not verify that fields are valid"""
model = self.parse(template_statement)
return [ts.template.field for ts in model.template_strings if ts.template]
class PhotoTemplate:
@@ -329,6 +358,7 @@ class PhotoTemplate:
# initialize render options
# this will be done in render() but for testing, some of the lookup functions are called directly
options = RenderOptions()
self.options = options
self.path_sep = options.path_sep
self.inplace_sep = options.inplace_sep
self.edited_version = options.edited_version
@@ -340,6 +370,8 @@ class PhotoTemplate:
self.export_dir = options.export_dir
self.filepath = options.filepath
self.quote = options.quote
self.dest_path = options.dest_path
self.exportdb = options.exportdb or ExportDBInMemory(None)
def render(
self,
@@ -359,6 +391,7 @@ class PhotoTemplate:
if type(template) is not str:
raise TypeError(f"template must be type str, not {type(template)}")
self.options = options
self.path_sep = options.path_sep
self.inplace_sep = options.inplace_sep
self.edited_version = options.edited_version
@@ -371,7 +404,8 @@ class PhotoTemplate:
self.dest_path = options.dest_path
self.filepath = options.filepath
self.quote = options.quote
self.options = options
self.dest_path = options.dest_path
self.exportdb = options.exportdb or self.exportdb
try:
model = self.parser.parse(template)
@@ -499,7 +533,10 @@ class PhotoTemplate:
conditional_value = []
vals = []
if field in SINGLE_VALUE_SUBSTITUTIONS:
if (
field in SINGLE_VALUE_SUBSTITUTIONS
or field.split(".")[0] in SINGLE_VALUE_SUBSTITUTIONS
):
vals = self.get_template_value(
field,
default=default,
@@ -525,7 +562,7 @@ class PhotoTemplate:
)
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
vals = self.get_template_value_multi(
field, path_sep=path_sep, default=default
field, subfield, path_sep=path_sep, default=default
)
elif field.split(".")[0] in PATHLIB_SUBSTITUTIONS:
vals = self.get_template_value_pathlib(field)
@@ -578,12 +615,8 @@ class PhotoTemplate:
f"comparison operators may only be used with a single value: {vals} {conditional_value}"
)
try:
match = (
True
if test_function(
float(vals[0]), float(conditional_value[0])
)
else False
match = bool(
test_function(float(vals[0]), float(conditional_value[0]))
)
if (match and not negation) or (negation and not match):
return ["True"]
@@ -683,9 +716,6 @@ class PhotoTemplate:
if self.photo.uuid is None:
return []
if field not in FIELD_NAMES:
raise ValueError(f"SyntaxError: Unknown field: {field}")
# initialize today with current date/time if needed
if self.today is None:
self.today = datetime.datetime.now()
@@ -938,8 +968,26 @@ class PhotoTemplate:
value = self.photo.exif_info.lens_model if self.photo.exif_info else None
elif field == "uuid":
value = self.photo.uuid
elif field.startswith("id"):
value = format_id_str(self.photo._info["pk"], subfield)
elif field == "id":
value = format_str_value(self.photo._info["pk"], subfield)
elif field.startswith("album_seq") or field.startswith("folder_album_seq"):
dest_path = self.dest_path
if not dest_path:
value = None
else:
if field.startswith("album_seq"):
album = pathlib.Path(dest_path).name
album_info = _get_album_by_name(self.photo, album)
else:
album_info = _get_album_by_path(self.photo, dest_path)
value = album_info.photo_index(self.photo) if album_info else None
if value is not None:
try:
start_id = field.split(".", 1)
value = int(value) + int(start_id[1])
except IndexError:
pass
value = format_str_value(value, subfield)
elif field in PUNCTUATION:
value = PUNCTUATION[field]
elif field == "osxphotos_version":
@@ -1040,11 +1088,12 @@ class PhotoTemplate:
value = []
return value
def get_template_value_multi(self, field, path_sep, default):
def get_template_value_multi(self, field, subfield, path_sep, default):
"""lookup value for template field (multi-value template substitutions)
Args:
field: template field to find value for.
subfield: the template subfield value
path_sep: path separator to use for folder_album field
default: value of default field
@@ -1093,12 +1142,10 @@ class PhotoTemplate:
folder = path_sep.join(album.folder_names)
folder += path_sep + album.title
values.append(folder)
elif self.dirname:
values.append(sanitize_dirname(album.title))
else:
# album not in folder
if self.dirname:
values.append(sanitize_dirname(album.title))
else:
values.append(album.title)
values.append(album.title)
elif field == "comment":
values = [
f"{comment.user}: {comment.text}" for comment in self.photo.comments
@@ -1141,6 +1188,8 @@ class PhotoTemplate:
values = [str(obj)]
else:
values = [val for val in obj]
elif field == "detected_text":
values = _get_detected_text(self.photo, self.exportdb, confidence=subfield)
else:
raise ValueError(f"Unhandled template value: {field}")
@@ -1353,3 +1402,51 @@ def _get_pathlib_value(field, value, quote):
return val_str
except AttributeError:
raise ValueError("Illegal value for path template: {attribute}")
def format_str_value(value, format_str):
"""Format value based on format code in field in format id:02d"""
if not format_str:
return str(value)
format_str = "{0:" + f"{format_str}" + "}"
return format_str.format(value)
def _get_album_by_name(photo, album):
"""Finds first album named album that photo is in and returns the AlbumInfo object, otherwise returns None"""
for album_info in photo.album_info:
if album_info.title == album:
return album_info
return None
def _get_album_by_path(photo, folder_album_path):
"""finds the first album whose folder_album path matches and folder_album_path and returns the AlbumInfo object, otherwise, returns None"""
for album_info in photo.album_info:
# following code is how {folder_album} builds the folder path
folder = "/".join(sanitize_dirname(f) for f in album_info.folder_names)
folder += "/" + sanitize_dirname(album_info.title)
if folder_album_path.endswith(folder):
return album_info
return None
def _get_detected_text(photo, exportdb, confidence=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
"""Returns the detected text for a photo
{detected_text} uses this instead of PhotoInfo.detected_text() to cache the text for all confidence values
"""
if not photo.isphoto:
return []
confidence = (
float(confidence)
if confidence is not None
else TEXT_DETECTION_CONFIDENCE_THRESHOLD
)
# _detected_text caches the text detection results in an extended attribute
# so the first time this gets called is slow but repeated accesses are fast
detected_text = photo._detected_text()
exportdb.set_detected_text_for_uuid(photo.uuid, json.dumps(detected_text))
return [text for text, conf in detected_text if conf >= confidence]

View File

@@ -0,0 +1,75 @@
""" Use Apple's Vision Framework via PyObjC to perform text detection on images (macOS 10.15+ only) """
import logging
from typing import List
import objc
import Quartz
from Cocoa import NSURL
from Foundation import NSDictionary
# needed to capture system-level stderr
from wurlitzer import pipes
from .utils import _get_os_version
ver, major, minor = _get_os_version()
if ver == "10" and int(major) < 15:
vision = False
else:
import Vision
vision = True
def detect_text(img_path: str) -> List:
"""process image at img_path with VNRecognizeTextRequest and return list of results"""
if not vision:
logging.warning(f"detect_text not implemented for this version of macOS")
return []
with objc.autorelease_pool():
input_url = NSURL.fileURLWithPath_(img_path)
with pipes() as (out, err):
# capture stdout and stderr from system calls
# otherwise, Quartz.CIImage.imageWithContentsOfURL_
# prints to stderr something like:
# 2020-09-20 20:55:25.538 python[73042:5650492] Creating client/daemon connection: B8FE995E-3F27-47F4-9FA8-559C615FD774
# 2020-09-20 20:55:25.652 python[73042:5650492] Got the query meta data reply for: com.apple.MobileAsset.RawCamera.Camera, response: 0
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
vision_options = NSDictionary.dictionaryWithDictionary_({})
vision_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
input_image, vision_options
)
results = []
handler = make_request_handler(results)
vision_request = (
Vision.VNRecognizeTextRequest.alloc().initWithCompletionHandler_(handler)
)
error = vision_handler.performRequests_error_([vision_request], None)
vision_request.dealloc()
vision_handler.dealloc()
for result in results:
result[0] = str(result[0])
return results
def make_request_handler(results):
"""results: list to store results"""
if not isinstance(results, list):
raise ValueError("results must be a list")
def handler(request, error):
if error:
print(f"Error! {error}")
else:
observations = request.results()
for text_observation in observations:
recognized_text = text_observation.topCandidates_(1)[0]
results.append([recognized_text.string(), recognized_text.confidence()])
return handler

View File

@@ -1,11 +1,12 @@
pyobjc-core==7.2
pyobjc-framework-AppleScriptKit==7.2
pyobjc-framework-AppleScriptObjC==7.2
pyobjc-framework-Photos==7.2
pyobjc-framework-Quartz==7.2
pyobjc-framework-AVFoundation==7.2
pyobjc-framework-CoreServices==7.2
pyobjc-framework-Metal==7.2
pyobjc-core>=7.2
pyobjc-framework-AppleScriptKit>=7.2
pyobjc-framework-AppleScriptObjC>=7.2
pyobjc-framework-Photos>=7.2
pyobjc-framework-Quartz>=7.2
pyobjc-framework-AVFoundation>=7.2
pyobjc-framework-CoreServices>=7.2
pyobjc-framework-Metal>=7.2
pyobjc-framework-Vision>=7.2
Click==8.0.1
PyYAML==5.4.1
Mako==1.1.4
@@ -13,10 +14,10 @@ bpylist2==3.0.2
pathvalidate==2.4.1
dataclasses==0.7;python_version<'3.7'
wurlitzer==2.1.0
photoscript==0.1.3
photoscript==0.1.4
toml==0.10.2
osxmetadata==0.99.25
osxmetadata==0.99.31
textx==2.3.0
rich==10.2.2
rich==10.6.0
bitmath==1.3.3.1
more-itertools==8.8.0
more-itertools==8.8.0

View File

@@ -2,4 +2,5 @@ sphinx_click
pytest==6.2.4
pytest-mock
m2r2
pyinstaller==4.4

View File

@@ -73,14 +73,15 @@ setup(
"Topic :: Software Development :: Libraries :: Python Modules",
],
install_requires=[
"pyobjc-core==7.2",
"pyobjc-framework-AppleScriptKit==7.2",
"pyobjc-framework-AppleScriptObjC==7.2",
"pyobjc-framework-Photos==7.2",
"pyobjc-framework-Quartz==7.2",
"pyobjc-framework-AVFoundation==7.2",
"pyobjc-framework-CoreServices==7.2",
"pyobjc-framework-Metal==7.2",
"pyobjc-core",
"pyobjc-framework-AppleScriptKit",
"pyobjc-framework-AppleScriptObjC",
"pyobjc-framework-Photos",
"pyobjc-framework-Quartz",
"pyobjc-framework-AVFoundation",
"pyobjc-framework-CoreServices",
"pyobjc-framework-Metal",
"pyobjc-framework-Vision",
"Click==8.0.1",
"PyYAML==5.4.1",
"Mako==1.1.4",
@@ -88,11 +89,11 @@ setup(
"pathvalidate==2.4.1",
"dataclasses==0.7;python_version<'3.7'",
"wurlitzer==2.1.0",
"photoscript==0.1.3",
"photoscript==0.1.4",
"toml==0.10.2",
"osxmetadata==0.99.25",
"osxmetadata==0.99.31",
"textx==2.3.0",
"rich==10.2.2",
"rich==10.6.0",
"bitmath==1.3.3.1",
"more-itertools==8.8.0",
],

View File

@@ -234,6 +234,11 @@ UUID_NOT_REFERENCE = "F12384F6-CD17-4151-ACBA-AE0E3688539E"
UUID_DUPLICATE = ""
UUID_DETECTED_TEXT = {
"E2078879-A29C-4D6F-BACB-E3BBE6C3EB91": "osxphotos",
"A92D9C26-3A50-4197-9388-CB5F7DB9FA91": None,
}
@pytest.fixture(scope="module")
def photosdb():
@@ -1423,3 +1428,14 @@ def test_multi_uuid(photosdb):
photos = photosdb.photos(uuid=[UUID_DICT["favorite"], UUID_DICT["not_favorite"]])
assert len(photos) == 2
def test_detected_text(photosdb):
"""test PhotoInfo.detected_text"""
for uuid, expected_text in UUID_DETECTED_TEXT.items():
photo = photosdb.get_photo(uuid=uuid)
detected_text = " ".join(text for text, conf in photo.detected_text())
if expected_text is not None:
assert expected_text in detected_text
else:
assert not detected_text

View File

@@ -778,6 +778,21 @@ UUID_DICT_MISSING = {
"D79B8D77-BFFC-460B-9312-034F2877D35B": "Pumkins2.jpg", # not missing
}
UUID_DICT_FOLDER_ALBUM_SEQ = {
"7783E8E6-9CAC-40F3-BE22-81FB7051C266": {
"directory": "{folder_album}",
"album": "Sorted Oldest First",
"filename": "{album?{folder_album_seq.1}_,}{original_name}",
"result": "3_IMG_3092.heic",
},
"3DD2C897-F19E-4CA6-8C22-B027D5A71907": {
"directory": "{album}",
"album": "Sorted Oldest First",
"filename": "{album?{album_seq}_,}{original_name}",
"result": "0_IMG_4547.jpg",
},
}
def modify_file(filename):
"""appends data to a file to modify it"""
@@ -7070,3 +7085,39 @@ def test_export_query_function():
)
assert result.exit_code == 0
assert "exported: 1" in result.output
def test_export_album_seq():
"""Test {album_seq} template"""
import glob
from osxphotos.cli import cli
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
for uuid in UUID_DICT_FOLDER_ALBUM_SEQ:
result = runner.invoke(
cli,
[
"export",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"-V",
"--album",
UUID_DICT_FOLDER_ALBUM_SEQ[uuid]["album"],
"--directory",
UUID_DICT_FOLDER_ALBUM_SEQ[uuid]["directory"],
"--filename",
UUID_DICT_FOLDER_ALBUM_SEQ[uuid]["filename"],
"--uuid",
uuid,
],
)
assert result.exit_code == 0
files = glob.glob(f"{UUID_DICT_FOLDER_ALBUM_SEQ[uuid]['album']}/*")
assert (
f"{UUID_DICT_FOLDER_ALBUM_SEQ[uuid]['album']}/{UUID_DICT_FOLDER_ALBUM_SEQ[uuid]['result']}"
in files
)

View File

@@ -1,5 +1,7 @@
""" Test ExportDB """
import json
import pytest
EXIF_DATA = """[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos", "EXIF:ImageDescription": "\u2068Elder Park\u2069, \u2068Adelaide\u2069, \u2068Australia\u2069", "XMP:Description": "\u2068Elder Park\u2069, \u2068Adelaide\u2069, \u2068Australia\u2069", "XMP:Title": "Elder Park", "EXIF:GPSLatitude": "34 deg 55' 8.01\" S", "EXIF:GPSLongitude": "138 deg 35' 48.70\" E", "Composite:GPSPosition": "34 deg 55' 8.01\" S, 138 deg 35' 48.70\" E", "EXIF:GPSLatitudeRef": "South", "EXIF:GPSLongitudeRef": "East", "EXIF:DateTimeOriginal": "2017:06:20 17:18:56", "EXIF:OffsetTimeOriginal": "+09:30", "EXIF:ModifyDate": "2020:05:18 14:42:04"}]"""
@@ -12,10 +14,11 @@ DATABASE_VERSION1 = "tests/export_db_version1.db"
def test_export_db():
""" test ExportDB """
"""test ExportDB"""
import os
import tempfile
from osxphotos.export_db import ExportDB, OSXPHOTOS_EXPORTDB_VERSION
from osxphotos.export_db import OSXPHOTOS_EXPORTDB_VERSION, ExportDB
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
dbname = os.path.join(tempdir.name, ".osxphotos_export.db")
@@ -47,6 +50,9 @@ def test_export_db():
assert db.get_sidecar_for_file(filepath) == (SIDECAR_DATA, (13, 14, 15))
assert db.get_previous_uuids() == ["FOO-BAR"]
db.set_detected_text_for_uuid("FOO-BAR", json.dumps([["foo", 0.5]]))
assert json.loads(db.get_detected_text_for_uuid("FOO-BAR")) == [["foo", 0.5]]
# test set_data which sets all at the same time
filepath2 = os.path.join(tempdir.name, "test2.jpg")
db.set_data(
@@ -80,6 +86,7 @@ def test_export_db():
assert db.get_stat_converted_for_file(filepath2) == (7, 8, 9)
assert db.get_stat_edited_for_file(filepath2) == (10, 11, 12)
assert sorted(db.get_previous_uuids()) == (["BAR-FOO", "FOO-BAR"])
assert json.loads(db.get_detected_text_for_uuid("FOO-BAR")) == [["foo", 0.5]]
# update data
db.set_uuid_for_file(filepath, "FUBAR")
@@ -88,10 +95,11 @@ def test_export_db():
def test_export_db_no_op():
""" test ExportDBNoOp """
"""test ExportDBNoOp"""
import os
import tempfile
from osxphotos.export_db import ExportDBNoOp, OSXPHOTOS_EXPORTDB_VERSION
from osxphotos.export_db import OSXPHOTOS_EXPORTDB_VERSION, ExportDBNoOp
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
db = ExportDBNoOp()
@@ -121,6 +129,9 @@ def test_export_db_no_op():
assert db.get_sidecar_for_file(filepath) == (None, (None, None, None))
assert db.get_previous_uuids() == []
db.set_detected_text_for_uuid("FOO-BAR", json.dumps([["foo", 0.5]]))
assert db.get_detected_text_for_uuid("FOO-BAR") is None
# test set_data which sets all at the same time
filepath2 = os.path.join(tempdir.name, "test2.jpg")
db.set_data(
@@ -148,13 +159,14 @@ def test_export_db_no_op():
def test_export_db_in_memory():
""" test ExportDBInMemory """
"""test ExportDBInMemory"""
import os
import tempfile
from osxphotos.export_db import (
OSXPHOTOS_EXPORTDB_VERSION,
ExportDB,
ExportDBInMemory,
OSXPHOTOS_EXPORTDB_VERSION,
)
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
@@ -174,6 +186,7 @@ def test_export_db_in_memory():
db.set_stat_edited_for_file(filepath, (10, 11, 12))
db.set_sidecar_for_file(filepath, SIDECAR_DATA, (13, 14, 15))
assert db.get_previous_uuids() == ["FOO-BAR"]
db.set_detected_text_for_uuid("FOO-BAR", json.dumps([["foo", 0.5]]))
db.close()
@@ -192,6 +205,7 @@ def test_export_db_in_memory():
assert dbram.get_stat_edited_for_file(filepath) == (10, 11, 12)
assert dbram.get_sidecar_for_file(filepath) == (SIDECAR_DATA, (13, 14, 15))
assert dbram.get_previous_uuids() == ["FOO-BAR"]
assert json.loads(dbram.get_detected_text_for_uuid("FOO-BAR")) == [["foo", 0.5]]
# change a value
dbram.set_uuid_for_file(filepath, "FUBAR")
@@ -202,6 +216,7 @@ def test_export_db_in_memory():
dbram.set_stat_converted_for_file(filepath, (1, 2, 3))
dbram.set_stat_edited_for_file(filepath, (4, 5, 6))
dbram.set_sidecar_for_file(filepath, "FUBAR", (20, 21, 22))
dbram.set_detected_text_for_uuid("FUBAR", json.dumps([["bar", 0.5]]))
assert dbram.get_uuid_for_file(filepath_lower) == "FUBAR"
assert dbram.get_info_for_uuid("FUBAR") == INFO_DATA2
@@ -212,6 +227,7 @@ def test_export_db_in_memory():
assert dbram.get_stat_edited_for_file(filepath) == (4, 5, 6)
assert dbram.get_sidecar_for_file(filepath) == ("FUBAR", (20, 21, 22))
assert dbram.get_previous_uuids() == ["FUBAR"]
assert json.loads(dbram.get_detected_text_for_uuid("FUBAR")) == [["bar", 0.5]]
dbram.close()
@@ -228,13 +244,15 @@ def test_export_db_in_memory():
assert db.get_previous_uuids() == ["FOO-BAR"]
assert db.get_info_for_uuid("FUBAR") is None
assert db.get_detected_text_for_uuid("FUBAR") is None
def test_export_db_in_memory_nofile():
""" test ExportDBInMemory with no dbfile """
"""test ExportDBInMemory with no dbfile"""
import os
import tempfile
from osxphotos.export_db import ExportDBInMemory, OSXPHOTOS_EXPORTDB_VERSION
from osxphotos.export_db import OSXPHOTOS_EXPORTDB_VERSION, ExportDBInMemory
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
filepath = os.path.join(tempdir.name, "test.JPG")
@@ -254,6 +272,7 @@ def test_export_db_in_memory_nofile():
dbram.set_stat_converted_for_file(filepath, (1, 2, 3))
dbram.set_stat_edited_for_file(filepath, (4, 5, 6))
dbram.set_sidecar_for_file(filepath, "FUBAR", (20, 21, 22))
dbram.set_detected_text_for_uuid("FUBAR", json.dumps([["bar", 0.5]]))
assert dbram.get_uuid_for_file(filepath_lower) == "FUBAR"
assert dbram.get_info_for_uuid("FUBAR") == INFO_DATA2
@@ -264,5 +283,6 @@ def test_export_db_in_memory_nofile():
assert dbram.get_stat_edited_for_file(filepath) == (4, 5, 6)
assert dbram.get_sidecar_for_file(filepath) == ("FUBAR", (20, 21, 22))
assert dbram.get_previous_uuids() == ["FUBAR"]
assert json.loads(dbram.get_detected_text_for_uuid("FUBAR")) == [["bar", 0.5]]
dbram.close()

View File

@@ -1,4 +1,5 @@
""" Test template.py """
import json
import os
import re
@@ -7,6 +8,7 @@ from photoinfo_mock import PhotoInfoMock
import osxphotos
from osxphotos.exiftool import get_exiftool_path
from osxphotos.export_db import ExportDBInMemory
from osxphotos.phototemplate import (
TEMPLATE_SUBSTITUTIONS,
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
@@ -262,6 +264,11 @@ TEMPLATE_VALUES_DATE_NOT_MODIFIED = {
"{modified.strftime,%Y-%m-%d-%H%M%S}": "2020-02-04-190738",
}
UUID_DETECTED_TEXT = "E2078879-A29C-4D6F-BACB-E3BBE6C3EB91"
TEMPLATE_VALUES_DETECTED_TEXT = {
"{detected_text}": "osxphotos",
"{;+detected_text:0.5}": "osxphotos;",
}
COMMENT_UUID_DICT = {
"4AD7C8EF-2991-4519-9D3A-7F44A6F031BE": [
@@ -343,6 +350,49 @@ UUID_CONDITIONAL = {
},
}
UUID_ALBUM_SEQ = {
"7783E8E6-9CAC-40F3-BE22-81FB7051C266": {
"album": "/Sorted Manual",
"templates": {
"{album_seq}": "0",
"{album_seq:02d}": "00",
"{album_seq.1}": "1",
"{album_seq.1:03d}": "001",
"{folder_album_seq}": "0",
"{folder_album_seq:02d}": "00",
"{folder_album_seq.1}": "1",
"{folder_album_seq.1:03d}": "001",
},
},
"F12384F6-CD17-4151-ACBA-AE0E3688539E": {
"album": "/Sorted Manual",
"templates": {
"{album_seq}": "2",
"{album_seq:02d}": "02",
"{album_seq.1}": "3",
"{album_seq.1:03d}": "003",
"{folder_album_seq}": "2",
"{folder_album_seq:02d}": "02",
"{folder_album_seq.1}": "3",
"{folder_album_seq.1:03d}": "003",
},
},
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": {
"album": "/Folder1/SubFolder2/AlbumInFolder",
"templates": {
"{album_seq}": "1",
"{album_seq:02d}": "01",
"{album_seq.1}": "2",
"{album_seq.1:03d}": "002",
"{folder_album_seq}": "1",
"{folder_album_seq:02d}": "01",
"{folder_album_seq.1}": "2",
"{folder_album_seq.0}": "1",
"{folder_album_seq.1:03d}": "002",
},
},
}
@pytest.fixture(scope="module")
def photosdb_places():
@@ -391,7 +441,10 @@ def test_lookup_multi(photosdb_places):
if subst in ["{exiftool}", "{photo}", "{function}"]:
continue
lookup = template.get_template_value_multi(
lookup_str, path_sep=os.path.sep, default=[]
lookup_str,
path_sep=os.path.sep,
default=[],
subfield=None,
)
assert isinstance(lookup, list)
@@ -1105,3 +1158,24 @@ def test_id(photosdb):
rendered, _ = photo.render_template("{id:03d}")
assert rendered[0] == "007"
def test_album_seq(photosdb):
"""Test {album_seq} and {folder_album_seq} templates"""
from osxphotos.phototemplate import RenderOptions
for uuid in UUID_ALBUM_SEQ:
photo = photosdb.get_photo(uuid)
album = UUID_ALBUM_SEQ[uuid]["album"]
options = RenderOptions(dest_path=album)
for template, value in UUID_ALBUM_SEQ[uuid]["templates"].items():
rendered, _ = photo.render_template(template, options=options)
assert rendered[0] == value
def test_detected_text(photosdb):
"""Test {detected_text} template"""
photo = photosdb.get_photo(UUID_DETECTED_TEXT)
for template, value in TEMPLATE_VALUES_DETECTED_TEXT.items():
rendered, _ = photo.render_template(template)
assert value in "".join(rendered)