diff --git a/pyxform/aliases.py b/pyxform/aliases.py
index 17cfa5c6..86496a1a 100644
--- a/pyxform/aliases.py
+++ b/pyxform/aliases.py
@@ -96,6 +96,7 @@
"big-image": "media::big-image",
"audio": "media::audio",
"video": "media::video",
+ "image-description": "media::image-description",
"count": "control::jr:count",
"repeat_count": "control::jr:count",
"jr:count": "control::jr:count",
@@ -142,6 +143,7 @@
"big-image": "media::big-image",
"audio": "media::audio",
"video": "media::video",
+ "image-description": "media::image-description",
}
# Note that most of the type aliasing happens in all.xls
_type_alias_map = {
diff --git a/pyxform/constants.py b/pyxform/constants.py
index 793d1e05..8f485937 100644
--- a/pyxform/constants.py
+++ b/pyxform/constants.py
@@ -166,5 +166,5 @@ class EntityColumns(StrEnum):
"xmlns:orx": "http://openrosa.org/xforms",
"xmlns:odk": "http://www.opendatakit.org/xforms",
}
-SUPPORTED_MEDIA_TYPES = {"image", "big-image", "audio", "video"}
+SUPPORTED_MEDIA_TYPES = {"image", "big-image", "audio", "video", "image-description"}
OR_OTHER_CHOICE = {NAME: "other", LABEL: "Other"}
diff --git a/pyxform/json_form_schema.json b/pyxform/json_form_schema.json
index b2321b1e..9b6f49a8 100644
--- a/pyxform/json_form_schema.json
+++ b/pyxform/json_form_schema.json
@@ -170,6 +170,16 @@
"type" : "string",
"description" : "A key value pair where the key is a language, and the value is the content uri in that language."
}
+ },
+ "image-description" :
+ {
+ "type" : ["object", "string"],
+ "description" : "Optional string description of the image; eg to use for accessibility."
+ "additionalProperties":
+ {
+ "type" : "string",
+ "description" : "A key value pair where the key is a language, and the value is the content in that language."
+ }
}
}
},
diff --git a/pyxform/survey.py b/pyxform/survey.py
index 50ecafe9..4cb75996 100644
--- a/pyxform/survey.py
+++ b/pyxform/survey.py
@@ -1096,6 +1096,15 @@ def itext(self) -> DetachableElement:
toParseString=output_inserted,
)
)
+ elif media_type == "image-description":
+ itext_nodes.append(
+ node(
+ "value",
+ value,
+ form=media_type,
+ toParseString=output_inserted,
+ )
+ )
elif value != "-":
itext_nodes.append(
node(
diff --git a/pyxform/survey_element.py b/pyxform/survey_element.py
index 3312e229..094e7440 100644
--- a/pyxform/survey_element.py
+++ b/pyxform/survey_element.py
@@ -530,6 +530,16 @@ def xml_label_and_hint(self, survey: "Survey") -> list["DetachableElement"]:
"To use big-image, you must also specify an image for the survey element named {self.name}."
)
+ # image-description must be associated with an image
+ if (
+ self.media is not None
+ and "image" not in self.media
+ and "image-description" in self.media
+ ):
+ raise PyXFormError(
+ "To use image-description, you must also specify an image for the survey element named {self.name}."
+ )
+
return result
def xml_bindings(
diff --git a/pyxform/validators/pyxform/choices.py b/pyxform/validators/pyxform/choices.py
index 3b347c35..b218514e 100644
--- a/pyxform/validators/pyxform/choices.py
+++ b/pyxform/validators/pyxform/choices.py
@@ -22,6 +22,11 @@
"If this is intentional, use the setting 'allow_choice_duplicates'. "
"Learn more: https://xlsform.org/#choice-names."
)
+MISSING_IMAGE = (
+ "[row : {row}] On the 'choices' sheet, an 'image' has not been specified. "
+ "Choices with 'big-image' or 'image-description' require a corresponding image. "
+ "Learn more: https://xlsform.org/en/#the-choices-worksheet"
+)
def validate_headers(
@@ -55,6 +60,14 @@ def validate_choice_list(
else:
seen_options.add(name)
+ # Check the option's media, if specified, is mutually consistent
+ if "media" in option:
+ media = option["media"]
+ if "image" not in media and (
+ "big-image" in media or "image-description" in media
+ ):
+ duplicate_errors.append(MISSING_IMAGE.format(row=option["__row"]))
+
if 0 < len(duplicate_errors):
raise PyXFormError("\n".join(duplicate_errors))
diff --git a/tests/bug_example_xls/big-image_choice_missing_image.xlsx b/tests/bug_example_xls/big-image_choice_missing_image.xlsx
new file mode 100644
index 00000000..963f7313
Binary files /dev/null and b/tests/bug_example_xls/big-image_choice_missing_image.xlsx differ
diff --git a/tests/bug_example_xls/big-image_survey_missing_image.xlsx b/tests/bug_example_xls/big-image_survey_missing_image.xlsx
new file mode 100644
index 00000000..a0b94555
Binary files /dev/null and b/tests/bug_example_xls/big-image_survey_missing_image.xlsx differ
diff --git a/tests/bug_example_xls/image-description_choice_missing_image.xlsx b/tests/bug_example_xls/image-description_choice_missing_image.xlsx
new file mode 100644
index 00000000..b0bad98f
Binary files /dev/null and b/tests/bug_example_xls/image-description_choice_missing_image.xlsx differ
diff --git a/tests/bug_example_xls/image-description_survey_missing_image.xlsx b/tests/bug_example_xls/image-description_survey_missing_image.xlsx
new file mode 100644
index 00000000..6fd8af2c
Binary files /dev/null and b/tests/bug_example_xls/image-description_survey_missing_image.xlsx differ
diff --git a/tests/example_xls/image-description.xlsx b/tests/example_xls/image-description.xlsx
new file mode 100644
index 00000000..c2e93cbf
Binary files /dev/null and b/tests/example_xls/image-description.xlsx differ
diff --git a/tests/example_xls/image-description_translated.xlsx b/tests/example_xls/image-description_translated.xlsx
new file mode 100644
index 00000000..4e00f89f
Binary files /dev/null and b/tests/example_xls/image-description_translated.xlsx differ
diff --git a/tests/example_xls/media-image-description.xlsx b/tests/example_xls/media-image-description.xlsx
new file mode 100644
index 00000000..e8e10940
Binary files /dev/null and b/tests/example_xls/media-image-description.xlsx differ
diff --git a/tests/example_xls/media-image-description_translated.xlsx b/tests/example_xls/media-image-description_translated.xlsx
new file mode 100644
index 00000000..ed88208a
Binary files /dev/null and b/tests/example_xls/media-image-description_translated.xlsx differ
diff --git a/tests/test_expected_output/image-description.xml b/tests/test_expected_output/image-description.xml
new file mode 100644
index 00000000..e6e883ab
--- /dev/null
+++ b/tests/test_expected_output/image-description.xml
@@ -0,0 +1,89 @@
+
+
+
+ image-description
+
+
+
+
+ whale
+ jr://images/a.jpg
+ whale silhouette
+
+
+ frog
+ jr://images/b.jpg
+ frog silhouette
+
+
+ crocodile
+ jr://images/c.jpg
+ crocodile silhouette
+
+
+ eagle
+ jr://images/d.jpg
+ eagle silhouette
+
+
+ What is your name
+ jr://images/small.jpg
+ small car
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ animals-0
+ a
+
+ -
+ animals-1
+ b
+
+ -
+ animals-2
+ c
+
+ -
+ animals-3
+ d
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/test_expected_output/image-description_translated.xml b/tests/test_expected_output/image-description_translated.xml
new file mode 100644
index 00000000..f7517179
--- /dev/null
+++ b/tests/test_expected_output/image-description_translated.xml
@@ -0,0 +1,121 @@
+
+
+
+ image-description_translated
+
+
+
+
+ whale
+ jr://images/a.jpg
+ whale silhouette
+
+
+ frog
+ jr://images/b.jpg
+ frog silhouette
+
+
+ crocodile
+ jr://images/c.jpg
+ crocodile silhouette
+
+
+ eagle
+ jr://images/d.jpg
+ eagle silhouette
+
+
+ What is your name
+ jr://images/small.jpg
+ small car
+
+
+ Pick your favorite animal
+
+
+
+
+ baleine
+ jr://images/a.jpg
+ silhouette de baleine
+
+
+ grenouille
+ jr://images/b.jpg
+ silhouette de grenouille
+
+
+ crocodile
+ jr://images/c.jpg
+ silhouette de crocodile
+
+
+ aigle
+ jr://images/d.jpg
+ silhouette d'aigle
+
+
+ Quel est ton nom
+ jr://images/small.jpg
+ petite voiture
+
+
+ Choisissez votre animal préféré
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ animals-0
+ a
+
+ -
+ animals-1
+ b
+
+ -
+ animals-2
+ c
+
+ -
+ animals-3
+ d
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/test_expected_output/media-image-description.xml b/tests/test_expected_output/media-image-description.xml
new file mode 100644
index 00000000..49b7b23a
--- /dev/null
+++ b/tests/test_expected_output/media-image-description.xml
@@ -0,0 +1,89 @@
+
+
+
+ media-image-description
+
+
+
+
+ whale
+ jr://images/a.jpg
+ whale silhouette
+
+
+ frog
+ jr://images/b.jpg
+ frog silhouette
+
+
+ crocodile
+ jr://images/c.jpg
+ crocodile silhouette
+
+
+ eagle
+ jr://images/d.jpg
+ eagle silhouette
+
+
+ What is your name
+ jr://images/small.jpg
+ small car
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ animals-0
+ a
+
+ -
+ animals-1
+ b
+
+ -
+ animals-2
+ c
+
+ -
+ animals-3
+ d
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/test_expected_output/media-image-description_translated.xml b/tests/test_expected_output/media-image-description_translated.xml
new file mode 100644
index 00000000..52f64a26
--- /dev/null
+++ b/tests/test_expected_output/media-image-description_translated.xml
@@ -0,0 +1,122 @@
+
+
+
+ media-image-description_translated
+
+
+
+
+ whale
+ jr://images/a.jpg
+ whale silhouette
+
+
+ frog
+ jr://images/b.jpg
+ frog silhouette
+
+
+ crocodile
+ jr://images/c.jpg
+ crocodile silhouette
+
+
+ eagle
+ jr://images/d.jpg
+ eagle silhouette
+
+
+ What is your name
+ jr://images/small.jpg
+ small car
+
+
+ Pick your favorite animal
+
+
+
+
+ baleine
+ jr://images/a.jpg
+ silhouette de baleine
+
+
+ grenouille
+ jr://images/b.jpg
+ silhouette de grenouille
+
+
+ crocodile
+ jr://images/c.jpg
+ silhouette de crocodile
+
+
+ aigle
+ jr://images/d.jpg
+ silhouette d'aigle
+
+
+ Quel est ton nom
+ jr://images/small.jpg
+ petite voiture
+
+
+ Choisissez votre animal préféré
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ animals-0
+ a
+
+ -
+ animals-1
+ b
+
+ -
+ animals-2
+ c
+
+ -
+ animals-3
+ d
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py
index c5db20d1..a628f700 100644
--- a/tests/xform_test_case/test_bugs.py
+++ b/tests/xform_test_case/test_bugs.py
@@ -31,6 +31,22 @@ def test_conversion_raises(self):
),
("duplicate_columns.xlsx", "Duplicate column header: label"),
("calculate_without_calculation.xls", "[row : 34] Missing calculation."),
+ (
+ "big-image_survey_missing_image.xlsx",
+ "To use big-image, you must also specify an image for the survey element",
+ ),
+ (
+ "big-image_choice_missing_image.xlsx",
+ "On the 'choices' sheet, an 'image' has not been specified.",
+ ),
+ (
+ "image-description_survey_missing_image.xlsx",
+ "To use image-description, you must also specify an image for the survey element",
+ ),
+ (
+ "image-description_choice_missing_image.xlsx",
+ "On the 'choices' sheet, an 'image' has not been specified.",
+ ),
)
for i, (case, err_msg) in enumerate(cases):
with self.subTest(msg=f"{i}: {case}"):
diff --git a/tests/xform_test_case/test_xform_conversion.py b/tests/xform_test_case/test_xform_conversion.py
index 8e67451e..2784dfe5 100644
--- a/tests/xform_test_case/test_xform_conversion.py
+++ b/tests/xform_test_case/test_xform_conversion.py
@@ -26,6 +26,10 @@ def test_conversion_vs_expected(self):
("xlsform_spec_test.xlsx", True),
("xml_escaping.xls", True),
("default_time_demo.xls", True),
+ ("image-description.xlsx", False),
+ ("media-image-description.xlsx", False),
+ ("image-description_translated.xlsx", False),
+ ("media-image-description_translated.xlsx", False),
)
for i, (case, set_name) in enumerate(cases):
with self.subTest(msg=f"{i}: {case}"):