feat: support HEIF/HEIC image uploads via client-side Canvas conversion
Convert all image files to JPEG on the client before uploading, allowing iPhone HEIF/HEIC photos to work without server-side libheif dependency.
This commit is contained in:
parent
bb4aa103fb
commit
fbf5a288c1
5 changed files with 48 additions and 26 deletions
|
|
@ -56,6 +56,7 @@ pub(super) fn router() -> axum::Router<AppState> {
|
||||||
)
|
)
|
||||||
.route("/static/js/components/chip-scroll.js", get(chip_scroll_js))
|
.route("/static/js/components/chip-scroll.js", get(chip_scroll_js))
|
||||||
.route("/static/js/location.js", get(location_js))
|
.route("/static/js/location.js", get(location_js))
|
||||||
|
.route("/static/js/image-utils.js", get(image_utils_js))
|
||||||
.route("/static/js/components/world-map.js", get(world_map_js))
|
.route("/static/js/components/world-map.js", get(world_map_js))
|
||||||
.route("/static/js/components/donut-chart.js", get(donut_chart_js))
|
.route("/static/js/components/donut-chart.js", get(donut_chart_js))
|
||||||
.route(
|
.route(
|
||||||
|
|
@ -102,6 +103,16 @@ async fn location_js() -> impl IntoResponse {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn image_utils_js() -> impl IntoResponse {
|
||||||
|
(
|
||||||
|
[
|
||||||
|
("content-type", "application/javascript; charset=utf-8"),
|
||||||
|
("cache-control", "public, max-age=604800"),
|
||||||
|
],
|
||||||
|
include_str!("../../../../static/js/image-utils.js"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async fn photo_capture_js() -> impl IntoResponse {
|
async fn photo_capture_js() -> impl IntoResponse {
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -37,14 +37,12 @@ customElements.define(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleFile(file) {
|
async _handleFile(file) {
|
||||||
const entityType = this.getAttribute("entity-type");
|
const entityType = this.getAttribute("entity-type");
|
||||||
const entityId = this.getAttribute("entity-id");
|
const entityId = this.getAttribute("entity-id");
|
||||||
const mode = this.getAttribute("mode");
|
const mode = this.getAttribute("mode");
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
reader.onload = () => {
|
const dataUrl = await imageToJpegDataUrl(file);
|
||||||
const dataUrl = reader.result;
|
|
||||||
|
|
||||||
if (entityType && entityId && mode !== "deferred") {
|
if (entityType && entityId && mode !== "deferred") {
|
||||||
this._upload(entityType, entityId, dataUrl);
|
this._upload(entityType, entityId, dataUrl);
|
||||||
|
|
@ -56,9 +54,6 @@ customElements.define(
|
||||||
}
|
}
|
||||||
this._showPreview(dataUrl);
|
this._showPreview(dataUrl);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_showPreview(dataUrl) {
|
_showPreview(dataUrl) {
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,15 @@ customElements.define(
|
||||||
if (e.target !== input) input.click();
|
if (e.target !== input) input.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
input.addEventListener("change", () => {
|
input.addEventListener("change", async () => {
|
||||||
const file = input.files[0];
|
const file = input.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const reader = new FileReader();
|
const dataUrl = await imageToJpegDataUrl(file);
|
||||||
reader.onload = () => {
|
|
||||||
document.getElementById(this.getAttribute("target-input")).value =
|
document.getElementById(this.getAttribute("target-input")).value =
|
||||||
reader.result;
|
dataUrl;
|
||||||
document
|
document
|
||||||
.getElementById(this.getAttribute("target-form"))
|
.getElementById(this.getAttribute("target-form"))
|
||||||
.requestSubmit();
|
.requestSubmit();
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
input.value = "";
|
input.value = "";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
static/js/image-utils.js
Normal file
18
static/js/image-utils.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
/** Convert any image file (HEIC, AVIF, WebP, PNG, etc.) to a JPEG data URL via Canvas. */
|
||||||
|
const imageToJpegDataUrl = (file) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = img.naturalWidth;
|
||||||
|
canvas.height = img.naturalHeight;
|
||||||
|
canvas.getContext("2d").drawImage(img, 0, 0);
|
||||||
|
URL.revokeObjectURL(img.src);
|
||||||
|
resolve(canvas.toDataURL("image/jpeg", 0.92));
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
URL.revokeObjectURL(img.src);
|
||||||
|
reject(new Error("Failed to load image"));
|
||||||
|
};
|
||||||
|
img.src = URL.createObjectURL(file);
|
||||||
|
});
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.6/bundles/datastar.js"
|
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.6/bundles/datastar.js"
|
||||||
></script>
|
></script>
|
||||||
<script defer src="/static/js/location.js"></script>
|
<script defer src="/static/js/location.js"></script>
|
||||||
|
<script defer src="/static/js/image-utils.js"></script>
|
||||||
<script defer src="/static/js/components/photo-capture.js"></script>
|
<script defer src="/static/js/components/photo-capture.js"></script>
|
||||||
<script defer src="/static/js/components/searchable-select.js"></script>
|
<script defer src="/static/js/components/searchable-select.js"></script>
|
||||||
<script defer src="/static/js/components/chip-scroll.js"></script>
|
<script defer src="/static/js/components/chip-scroll.js"></script>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue