Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions app/javascript/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,58 @@ import { start } from "@rails/activestorage"

start()

// Extract the raw APP1 (EXIF) segment bytes from a JPEG file, or null if absent.
function extractJpegExif(srcBytes) {
var i = 2; // skip SOI FF D8
while (i + 3 < srcBytes.length) {
if (srcBytes[i] !== 0xFF) break;
var marker = srcBytes[i + 1];
var segLen = (srcBytes[i + 2] << 8) | srcBytes[i + 3];
if (marker === 0xE1) return srcBytes.slice(i, i + 2 + segLen); // APP1
if (marker === 0xDA || marker === 0xD9) break; // SOS / EOI
i += 2 + segLen;
}
return null;
}

// Patch the EXIF orientation tag in-place to `value` (1 = normal/upright).
// Canvas drawImage already applies visual rotation, so the tag must be reset
// to avoid double-rotation in viewers that honour EXIF orientation.
function patchExifOrientation(app1, value) {
if (app1.length < 14) return;
var hdr = String.fromCharCode(app1[4], app1[5], app1[6], app1[7]);
if (hdr !== "Exif") return;
var t = 10; // tiff header offset inside app1
var le = app1[t] === 0x49; // "II" = little-endian
function r16(o) { return le ? (app1[o] | (app1[o+1] << 8)) : ((app1[o] << 8) | app1[o+1]); }
function r32(o) { return (le ? (app1[o] | (app1[o+1]<<8) | (app1[o+2]<<16) | (app1[o+3]<<24)) : ((app1[o]<<24) | (app1[o+1]<<16) | (app1[o+2]<<8) | app1[o+3])) >>> 0; }
var ifd = t + r32(t + 4);
var n = r16(ifd);
for (var j = 0; j < n; j++) {
var e = ifd + 2 + j * 12;
if (r16(e) === 0x0112) { // Orientation tag
if (le) { app1[e+8] = value & 0xFF; app1[e+9] = 0; }
else { app1[e+8] = 0; app1[e+9] = value & 0xFF; }
return;
}
}
}

// Splice `exifSegment` into a canvas-produced JPEG blob, replacing any JFIF
// APP0 header so the result is a clean EXIF JPEG.
function spliceExifIntoJpeg(exifSegment, destBytes) {
// Skip any APP0 (JFIF) segments that canvas may have emitted.
var start = 2;
while (start + 3 < destBytes.length && destBytes[start] === 0xFF && destBytes[start+1] === 0xE0) {
start += 2 + ((destBytes[start+2] << 8) | destBytes[start+3]);
}
var out = new Uint8Array(2 + exifSegment.length + destBytes.length - start);
out.set(destBytes.slice(0, 2));
out.set(exifSegment, 2);
out.set(destBytes.slice(start), 2 + exifSegment.length);
return new Blob([out], { type: "image/jpeg" });
}

function compressToJpeg(file) {
return new Promise(function (resolve) {
var MAX = 1920, QUALITY = 0.82;
Expand All @@ -22,9 +74,24 @@ function compressToJpeg(file) {
var canvas = document.createElement("canvas");
canvas.width = w; canvas.height = h;
canvas.getContext("2d").drawImage(img, 0, 0, w, h);
canvas.toBlob(function (blob) { if (!blob) { resolve(file); return; }
canvas.toBlob(function (blob) {
if (!blob) { resolve(file); return; }
var name = file.name.replace(/\.[^.]+$/, ".jpg");
resolve(new File([blob], name, { type: "image/jpeg", lastModified: Date.now() }));
// Preserve EXIF from the original file if it was a JPEG.
if (!file.type.startsWith("image/jpeg") && !file.type.startsWith("image/jpg")) {
resolve(new File([blob], name, { type: "image/jpeg", lastModified: Date.now() }));
return;
}
Promise.all([file.arrayBuffer(), blob.arrayBuffer()]).then(function(bufs) {
var exif = extractJpegExif(new Uint8Array(bufs[0]));
if (!exif) {
resolve(new File([blob], name, { type: "image/jpeg", lastModified: Date.now() }));
return;
}
patchExifOrientation(exif, 1);
var finalBlob = spliceExifIntoJpeg(exif, new Uint8Array(bufs[1]));
resolve(new File([finalBlob], name, { type: "image/jpeg", lastModified: Date.now() }));
});
}, "image/jpeg", QUALITY);
};
img.onerror = function () { resolve(file); };
Expand Down
2 changes: 1 addition & 1 deletion app/views/active_storage/blobs/_blob.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
<% if blob.representable? %>
<%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
<%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ], saver: { strip: false }) %>
<% end %>

<figcaption class="attachment__caption">
Expand Down
2 changes: 1 addition & 1 deletion app/views/admin/check_ins/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<!-- Thumbnail -->
<div class="flex-shrink-0 w-20 h-20">
<% if ci.image.attached? %>
<%= image_tag ci.image.variant(resize_to_limit: [80, 80]),
<%= image_tag ci.image.variant(resize_to_limit: [80, 80], saver: { strip: false }),
class: "w-20 h-20 object-cover border-2 border-black" %>
<% else %>
<div class="w-20 h-20 bg-gray-100 border-2 border-gray-300 flex items-center justify-center text-gray-400 text-xs">
Expand Down
2 changes: 1 addition & 1 deletion app/views/admin/check_ins/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
</div>
<div class="px-6 py-6">
<% if @check_in.image.attached? %>
<%= image_tag @check_in.image.variant(resize_to_limit: [800, 800]),
<%= image_tag @check_in.image.variant(resize_to_limit: [800, 800], saver: { strip: false }),
class: "max-w-full border-2 border-black" %>
<div class="mt-4">
<%= link_to "Download original", url_for(@check_in.image),
Expand Down
2 changes: 1 addition & 1 deletion app/views/admin/submissions/_table.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
<% blob = sub.media.blob %>
<% if blob.content_type.start_with?("image/") %>
<%= link_to url_for(sub.media), target: "_blank", rel: "noopener" do %>
<%= image_tag sub.media.variant(resize_to_limit: [64, 64]),
<%= image_tag sub.media.variant(resize_to_limit: [64, 64], saver: { strip: false }),
class: "w-16 h-16 object-cover border-2 border-black" %>
<% end %>
<% elsif blob.content_type.start_with?("video/") %>
Expand Down
4 changes: 2 additions & 2 deletions app/views/admin/submissions/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@

<% if blob.content_type.start_with?("image/") %>
<div class="relative group">
<%= image_tag submission.media.variant(resize_to_limit: [160, 160]),
<%= image_tag submission.media.variant(resize_to_limit: [160, 160], saver: { strip: false }),
class: "w-40 h-40 object-cover border-2 border-black" %>
<button
onclick='event.stopPropagation(); openMediaModal("image", <%= media_url.to_json %>, <%= modal_meta.to_json %>)'
Expand Down Expand Up @@ -318,7 +318,7 @@
<% if blob.content_type.start_with?("image/") %>
<div class="relative group inline-block">
<%= link_to url_for(sub.media), target: "_blank", rel: "noopener" do %>
<%= image_tag sub.media.variant(resize_to_limit: [64, 64]),
<%= image_tag sub.media.variant(resize_to_limit: [64, 64], saver: { strip: false }),
class: "w-16 h-16 object-cover border-2 border-black" %>
<% end %>
<button
Expand Down
2 changes: 1 addition & 1 deletion app/views/admin/submissions/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@
<td class="px-5 py-3">
<% if sub.media.attached? %>
<% if sub.media.blob.content_type.start_with?("image/") %>
<%= image_tag sub.media.variant(resize_to_limit: [48, 48]),
<%= image_tag sub.media.variant(resize_to_limit: [48, 48], saver: { strip: false }),
class: "w-12 h-12 object-cover border-2 border-black" %>
<% elsif sub.media.blob.content_type.start_with?("video/") %>
<span class="text-xs text-gray-500 font-mono">video</span>
Expand Down
2 changes: 1 addition & 1 deletion app/views/static_pages/home.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@
<% if @zone_first_submission&.media&.attached? %>
<% blob = @zone_first_submission.media.blob %>
<% if blob.content_type.start_with?("image/") %>
<%= image_tag @zone_first_submission.media.variant(resize_to_limit: [120, 120]),
<%= image_tag @zone_first_submission.media.variant(resize_to_limit: [120, 120], saver: { strip: false }),
class: "w-20 h-20 object-cover rounded-xl border-[3px] border-black flex-shrink-0" %>
<% elsif blob.content_type.start_with?("video/") %>
<video class="w-20 h-20 object-cover rounded-xl border-[3px] border-black bg-black flex-shrink-0" muted>
Expand Down
Loading