Add saved pixel rotation for JPG, PNG and TIFF v0.9.2
This commit is contained in:
1 parent
34602e4ebd
commit
ad52a95d4f
9 files changed
+251
-17
No files matched your search
+56
-7
@@ -40,6 +40,7 @@ const elements = {
|
||||
viewerEmpty: $('#viewer-empty'),
|
||||
viewerContent: $('#viewer-content'),
|
||||
preview: $('#photo-preview'),
|
||||
rotationStatus: $('#rotation-status'),
|
||||
filename: $('#current-filename'),
|
||||
filesize: $('#current-filesize'),
|
||||
position: $('#viewer-position'),
|
||||
@@ -208,6 +209,7 @@ async function scanFolder() {
|
||||
fileValues: null,
|
||||
metadataLoaded: false,
|
||||
metadataPresent: false,
|
||||
pendingRotation: 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -447,7 +449,9 @@ async function selectPhoto(index, force = false) {
|
||||
elements.filename.textContent = photo.name;
|
||||
elements.filesize.textContent = `${photo.format} · ${formatBytes(file.size)} · modified ${new Date(file.lastModified).toLocaleString()}`;
|
||||
elements.position.textContent = `${index + 1} / ${state.photos.length}`;
|
||||
resetView();
|
||||
state.zoom = 1;
|
||||
state.rotation = normalizeRotation(photo.pendingRotation || 0);
|
||||
applyView();
|
||||
restoreValues(photo.values);
|
||||
updatePhotoStatus(photo);
|
||||
updateNavigation();
|
||||
@@ -626,9 +630,11 @@ async function savePhoto(moveNext) {
|
||||
payload.append('longitude', values.longitude);
|
||||
payload.append('location_name', values.locationName);
|
||||
}
|
||||
const rotationToSave = supportsPixelRotation(photo) ? normalizeRotation(state.rotation) : 0;
|
||||
payload.append('rotation', String(rotationToSave));
|
||||
|
||||
const saveStarted = performance.now();
|
||||
setSaveStatus('saving', 'Uploading and rewriting metadata…', `${photo.name} · large RAW files can take a while`);
|
||||
setSaveStatus('saving', 'Uploading and applying changes…', `${photo.name} · large files can take a while`);
|
||||
const response = await fetch('/api/process', { method: 'POST', body: payload });
|
||||
if (!response.ok) {
|
||||
let message = `Save failed (${response.status}).`;
|
||||
@@ -637,6 +643,7 @@ async function savePhoto(moveNext) {
|
||||
}
|
||||
|
||||
const responseReceivedAt = performance.now();
|
||||
const rotationMode = response.headers.get('x-pixel-rotation') || 'none';
|
||||
const firstByteSeconds = (responseReceivedAt - saveStarted) / 1000;
|
||||
const writable = await photo.handle.createWritable({ keepExistingData: false });
|
||||
let timing;
|
||||
@@ -700,6 +707,8 @@ async function savePhoto(moveNext) {
|
||||
photo.fileValues = { ...savedValues };
|
||||
photo.metadataLoaded = true;
|
||||
photo.metadataPresent = true;
|
||||
photo.pendingRotation = 0;
|
||||
state.rotation = 0;
|
||||
photo.lastSaveTiming = timing;
|
||||
saveFolderState();
|
||||
setSaveStatus(
|
||||
@@ -707,7 +716,10 @@ async function savePhoto(moveNext) {
|
||||
'Saved in place',
|
||||
`${formatMetadataSummary(savedValues)} · ${formatSaveTiming(timing)}`,
|
||||
);
|
||||
showToast(`Saved ${photo.name}: ${formatSaveTiming(timing)}`);
|
||||
const rotationNote = rotationMode === 'jpeg-reencoded'
|
||||
? ' · rotation applied; JPEG re-encoded at high quality'
|
||||
: rotationMode !== 'none' ? ' · pixel rotation applied' : '';
|
||||
showToast(`Saved ${photo.name}: ${formatSaveTiming(timing)}${rotationNote}`);
|
||||
updateProgress();
|
||||
|
||||
const oldThumb = state.thumbUrls.get(photo.name);
|
||||
@@ -1075,8 +1087,45 @@ async function searchAddress() {
|
||||
|
||||
function applyView() {
|
||||
elements.preview.style.transform = `scale(${state.zoom}) rotate(${state.rotation}deg)`;
|
||||
updateRotationStatus();
|
||||
}
|
||||
|
||||
function normalizeRotation(rotation) {
|
||||
return ((Number(rotation) % 360) + 360) % 360;
|
||||
}
|
||||
|
||||
function supportsPixelRotation(photo) {
|
||||
return Boolean(photo && ['JPG', 'PNG', 'TIFF'].includes(photo.format));
|
||||
}
|
||||
|
||||
function updateRotationStatus() {
|
||||
const rotation = normalizeRotation(state.rotation);
|
||||
const photo = state.photos[state.currentIndex];
|
||||
elements.rotationStatus.classList.toggle('hidden', rotation === 0 || !photo);
|
||||
if (!rotation || !photo) return;
|
||||
const supported = supportsPixelRotation(photo);
|
||||
elements.rotationStatus.classList.toggle('preview-only', !supported);
|
||||
elements.rotationStatus.textContent = supported
|
||||
? `Rotation ${rotation}° will be applied to the image when saved`
|
||||
: `Preview rotation ${rotation}° · ${photo.format} files are not altered`;
|
||||
}
|
||||
|
||||
function setPendingRotation(rotation) {
|
||||
state.rotation = normalizeRotation(rotation);
|
||||
const photo = state.photos[state.currentIndex];
|
||||
if (photo) photo.pendingRotation = state.rotation;
|
||||
applyView();
|
||||
}
|
||||
|
||||
function fitView() {
|
||||
state.zoom = 1;
|
||||
applyView();
|
||||
}
|
||||
|
||||
function resetView() {
|
||||
state.zoom = 1;
|
||||
setPendingRotation(0);
|
||||
}
|
||||
function resetView() { state.zoom = 1; state.rotation = 0; applyView(); }
|
||||
|
||||
for (const button of elements.openButtons) button.addEventListener('click', openFolder);
|
||||
elements.rescan.addEventListener('click', scanFolder);
|
||||
@@ -1102,9 +1151,9 @@ for (const input of [elements.latitude, elements.longitude]) {
|
||||
elements.locationName.addEventListener('input', markLocationDirty);
|
||||
elements.zoomOut.addEventListener('click', () => { state.zoom = Math.max(.25, state.zoom - .15); applyView(); });
|
||||
elements.zoomIn.addEventListener('click', () => { state.zoom = Math.min(4, state.zoom + .15); applyView(); });
|
||||
elements.fit.addEventListener('click', resetView);
|
||||
elements.rotateLeft.addEventListener('click', () => { state.rotation -= 90; applyView(); });
|
||||
elements.rotateRight.addEventListener('click', () => { state.rotation += 90; applyView(); });
|
||||
elements.fit.addEventListener('click', fitView);
|
||||
elements.rotateLeft.addEventListener('click', () => setPendingRotation(state.rotation - 90));
|
||||
elements.rotateRight.addEventListener('click', () => setPendingRotation(state.rotation + 90));
|
||||
elements.resetView.addEventListener('click', resetView);
|
||||
elements.timeMode.addEventListener('change', () => elements.timeField.classList.toggle('hidden', elements.timeMode.value !== 'known'));
|
||||
elements.description.addEventListener('input', () => { elements.descriptionCount.textContent = `${elements.description.value.length} / 2000`; captureCurrentValues(); });
|
||||
|
||||
Reference in new issue
Block a user