Repository: iiif-prezi/iiif-prezi3 (language: python)

Issue:
Set HWD from file
<!-- Add a short name of the proposed helper method in the Title above -->

## Description
Introspect a file and set the height, width, and duration properties of a Canvas or Content Resource object, allowing nulls

## Function name / Parameters / Return
<!-- Suggest a name for the helper's function, the parameters it should accept, and what it should return (if anything) -->
Function name: `.set_hwd_from_file(file)`

Parameters: `file` - the file path or file object to introspect

Returns: `None`

## Objects
<!-- Which objects should this helper apply to? -->
- [ ] Base Object (all Prezi3 Classes)
- [ ] Collection
- [ ] Manifest
- [x] Canvas
- [ ] Range
- [ ] AnnotationCollection
- [ ] AnnotationPage
- [ ] Annotation
- [x] Content Resource - Image
- [x] Content Resource - Video
- [ ] Content Resource - Text
- [ ] Service
- [ ] Provider
- [ ] Other - please list below
<!-- Add other Objects/Classes here -->

## Considerations
<!-- Any extra considerations for the behavior of this helper -->
The helper should be able to work with either a file path or an already open file object, so type checking on the parameter will be required.
This helper will introduce new dependencies - probably Pillow for Images and ??? for Audio and Video resources. Please discuss in the ticket or Slack before adding.

For Canvas objects, if `height` is present, then `width` must also be set, and vice-versa (https://iiif.io/api/presentation/3.0/#height / https://iiif.io/api/presentation/3.0/#width), Therefore the helper must be type aware, and raise an error if one is set without the other when the target is a Canvas object

Helper: ID generating methods
We need an easy way to create URIs for objects like Manifests and Canvases

 * Config has default ID generation method
 * It makes ID optional if supplied it is override otherwise it creates a default. 
 * Set base_uri



Discussion / hints:
See also #9 for some discussion about approaches
It occurs to me that this helper _could_ call the Set HWD helper #(54) once it has established the values, to minimise code duplication a little. This is something we could add after if we wanted, so not a hard requirement for the hackathon.
This would be a helper class in https://github.com/iiif-prezi/iiif-prezi3/tree/main/iiif_prezi3/helpers

Should:
 * Encode special characters (UTF-8) 
 * Warn if it contains # as it could be a fragment selector 


Something like: https://github.com/linked-art/crom/blob/master/cromulent/model.py#L288-L314
I don't know how to do this with pydantic *and* as a Helper that manipulates the class hierarchy.

Pydantic expects a minimally valid record to be passed as keywords into the __init__ function of BaseModel.  We can inject some further code into base.Base.__init__ ... but it needs to manipulate the call to BaseModel's constructor, rather than running after the instance has been built.


Update - I stopped trying to monkeypatch the class in, and instead just set a property on the skeleton generated classes. Doesn't follow the same pattern as the other helpers, but it doesn't need to, as it doesn't expose functionality on instances.

Comments welcome (here) on the approach taken:
* https://github.com/iiif-prezi/iiif-prezi3/blob/id_generator/iiif_prezi3/base.py#L6-L13 
* https://github.com/iiif-prezi/iiif-prezi3/blob/id_generator/iiif_prezi3/helpers/auto_fields.py

eg, in use:

```
>>> from iiif_prezi3 import Manifest, Canvas
>>> m = Manifest(label="My Manifest")
>>> m
Manifest(id=Id(__root__=AnyUrl('http://foo.bar.com/', ...)), type='Manifest', label=LngString(), ...)
```

Thanks! (@glenrobson @mbennett-uoe @zimeon)
Discussed on the call (25th Feb) and are happy with the approach. We think you could do something like:

```

class NewDefault(object):
    def generate_defaults(self, what, **kw):
        if not 'id' in kw:
            return {'id': "http://glen.com/" + reposerviceGetNewID()}


Manifest.defaults.append(NewDefault())

m = Manifest(label="My Manifest")
```

to set your own default id generator. We think AutoType maybe handled with this [pull request](https://github.com/IIIF/presentation-validator/pull/131) and if it is then we might be able to remove it from the helper function. 
Assuming that `reposerviceGetNewID()` is visible to the function, then yes :)


Fix patch:
diff --git a/iiif_prezi3/helpers/__init__.py b/iiif_prezi3/helpers/__init__.py
index b3ddea7..3c20a80 100644
--- a/iiif_prezi3/helpers/__init__.py
+++ b/iiif_prezi3/helpers/__init__.py
@@ -5,6 +5,7 @@
 from .auto_fields import *  # noqa: F401,F403
 from .canvas_helpers import AddImageToCanvas  # noqa: F401
 from .canvas_sizes import MaxHelper, MinHelper  # noqa: F401
+from .set_hwd_from_file import SetHeightWidthDurationFileHelper  # noqa: F401
 from .set_hwd_from_iiif import SetHwdFromIIIF  # noqa: F401
 from .make_collection import MakeCollection # noqa: F401
 from .make_manifest import MakeManifest # noqa: F401
diff --git a/iiif_prezi3/helpers/set_hwd_from_file.py b/iiif_prezi3/helpers/set_hwd_from_file.py
new file mode 100644
index 0000000..63f523b
--- /dev/null
+++ b/iiif_prezi3/helpers/set_hwd_from_file.py
@@ -0,0 +1,37 @@
+import mimetypes
+
+from PIL import Image
+
+from ..loader import monkeypatch_schema
+from ..skeleton import Canvas
+
+
+class SetHeightWidthDurationFileHelper:
+    """Introspect a file and set the height, width, and duration properties.
+
+    Args:
+        file_path_or_object (filepath or fileobject): the file path or file object to introspect
+
+    Returns:
+        None
+    """
+
+    def set_hwd_from_file(self, file_path_or_object):
+        if isinstance(file_path_or_object, str):
+            filetype, _ = mimetypes.guess_type(file_path_or_object)
+            if not filetype.startswith("image/"):
+                raise NotImplementedError
+
+        tmp_image = Image.open(file_path_or_object)
+        w, h = tmp_image.size
+        self.set_hwd(h, w, None)
+        tmp_image.close()
+
+    def set_hwd(self, h, w, d):
+        # stub preparing for Issue #54
+        self.width = w
+        self.height = h
+        self.duration = d
+
+
+monkeypatch_schema(Canvas, SetHeightWidthDurationFileHelper)
diff --git a/requirements.dev.txt b/requirements.dev.txt
index 23e9ed5..4002b1b 100644
--- a/requirements.dev.txt
+++ b/requirements.dev.txt
@@ -4,3 +4,4 @@ flake8>=4.0.1,<5.0.0
 flake8-docstrings>=1.6.0,<2.0.0
 flake8-isort>=4.1.1,<5.0.0
 tox>=3.25.0,<4.0.0
+Pillow>=9.1.1
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 6404432..f36ec1e 100644
--- a/setup.py
+++ b/setup.py
@@ -15,20 +15,20 @@
 REQUIREMENTS = [
     "pydantic",
     "requests",
+    "Pillow"
 ]
 
 DOCS_REQUIREMENTS = [
     "mkdocs",
     "mkdocs-material",
     "mkdocstrings-python",
-]
 
 # Setting up
 setup(
-    name='iiif-prezi3',
+    name="iiif-prezi3",
     version=VERSION,
-    author='IIIF Prezi3 Team',
-    description='IIIF Presentation v3 API implementation',
+    author="IIIF Prezi3 Team",
+    description="IIIF Presentation v3 API implementation",
     long_description=long_description,
     long_description_content_type='text/markdown',
     packages=['iiif_prezi3', 'iiif_prezi3.helpers', 'iiif_prezi3.extensions'],
diff --git a/tox.ini b/tox.ini
index dafb12c..078277d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,7 +12,9 @@ python =
 [testenv]
 deps =
 	pydantic
+  Pillow
   requests
+
 skip_install = True
 commands =
 	python -m unittest discover -s tests


Test patch:
diff --git a/tests/fixtures/resources/page1-full.png b/tests/fixtures/resources/page1-full.png
new file mode 100644
index 0000000..524a10b
Binary files /dev/null and b/tests/fixtures/resources/page1-full.png differ
diff --git a/tests/test_set_hwd_from_file_helper.py b/tests/test_set_hwd_from_file_helper.py
new file mode 100644
index 0000000..3f6d8cd
--- /dev/null
+++ b/tests/test_set_hwd_from_file_helper.py
@@ -0,0 +1,25 @@
+import unittest
+
+from iiif_prezi3 import Canvas
+
+
+class SetHeightWidthDurationFileHelperTests(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(id="http://iiif.example.org/prezi/Canvas/0")
+        self.test_image = "tests/fixtures/resources/page1-full.png"
+
+    def test_set_hwd_from_filepath_notimage(self):
+        self.assertRaises(NotImplementedError, self.canvas.set_hwd_from_file, "foo.mp3")
+
+    def test_set_hwd_from_filepath(self):
+        self.canvas.set_hwd_from_file(self.test_image)
+        self.assertEqual(self.canvas.width, 1200)  # width
+        self.assertEqual(self.canvas.height, 1800)  # height
+        self.assertEqual(self.canvas.duration, None)  # duration
+
+    def test_set_hwd_from_fileobject(self):
+        with open(self.test_image, "rb") as image_file:
+            self.canvas.set_hwd_from_file(image_file)
+        self.assertEqual(self.canvas.width, 1200)  # width
+        self.assertEqual(self.canvas.height, 1800)  # height
+        self.assertEqual(self.canvas.duration, None)  # duration