PageSourceSearch

https://souqmadar.com/js/image-intake.js?v=msekocwu.43r

js souqmadar.com collected 2026-10-02 01:44:03 UTC 17,264 bytes, 406 lines download raw bytes

1/* ═══════════════════════════════════════════════════════════════════════
2   IMAGE INTAKE FOUNDATION (Phase 1)
3   ───────────────────────────────────────────────────────────────────────
4   Sits BETWEEN "user picks/drops/pastes a file" and the EXISTING image
5   search validation/compression pipeline (handleImageFile → _validateImageFile
6   → _compressImageForSearch → POST /api/image-search).
7
8   Contract:
9     ImageIntake.normalize(file) → Promise<File>
10       - Resolves with a File object that is ALWAYS a real, re-encoded
11         JPEG (image/jpeg), regardless of the original format.
12       - Rejects with { code, message } when a format truly cannot be
13         decoded in the current browser (documented per-format below —
14         never silently swallowed; the caller decides how to surface it).
15
16   Nothing downstream of this module is modified. handleImageFile(),
17   _validateImageFile(), _compressImageForSearch(), startImageSearch(),
18   and POST /api/image-search are all untouched — they simply always
19   receive a JPEG File now, which they already fully support today.
20
21   Supported inputs (Phase 1 scope):
22     JPEG, JPG      → re-encoded via canvas (EXIF-corrected, alpha n/a)
23     PNG            → re-encoded via canvas (alpha flattened onto white)
24     WEBP           → re-encoded via canvas (alpha flattened onto white)
25     GIF            → first frame only, re-encoded via canvas
26     BMP            → re-encoded via canvas
27     AVIF           → re-encoded via canvas IF the browser can natively
28                      decode AVIF (Chrome/Firefox/Edge — evergreen; Safety
29                      16+). No AVIF decoder is bundled — see documented
30                      limitation below if decode fails.
31     HEIC, HEIF     → decoded via vendored heic2any (WASM-based libheif
32                      port), then re-encoded via canvas for a single
33                      consistent normalization path.
34     TIFF           → decoded via vendored UTIF.js (+ pako for Deflate-
35                      compressed TIFFs), then re-encoded via canvas.
36
37   Format detection is done by reading the first bytes of the file
38   (magic-byte sniffing), NOT by trusting file.type / file extension —
39   iOS in particular reports HEIC files with an empty or generic MIME
40   type in some contexts.
41   ═══════════════════════════════════════════════════════════════════ */
42
43(function (global) {
44  'use strict';
45
46  var VENDOR_BASE = '/js/vendor/';
47  var _scriptLoadPromises = {};
48
49  /** Lazily load a vendor <script> exactly once; safe to call repeatedly. */
50  function _loadScriptOnce(src) {
51    if (_scriptLoadPromises[src]) return _scriptLoadPromises[src];
52    _scriptLoadPromises[src] = new Promise(function (resolve, reject) {
53      var el = document.createElement('script');
54      el.src = src;
55      el.onload = function () { resolve(); };
56      el.onerror = function () {
57        delete _scriptLoadPromises[src];
58        reject(new Error('Failed to load ' + src));
59      };
60      document.head.appendChild(el);
61    });
62    return _scriptLoadPromises[src];
63  }
64
65  // ── Format detection (magic bytes) ─────────────────────────────────────
66
67  function _readHeaderBytes(file, n) {
68    return new Promise(function (resolve, reject) {
69      var reader = new FileReader();
70      reader.onload = function () { resolve(new Uint8Array(reader.result)); };
71      reader.onerror = function () { reject(new Error('Cannot read file header')); };
72      reader.readAsArrayBuffer(file.slice(0, n));
73    });
74  }
75
76  function _bytesToAscii(bytes, start, len) {
77    var s = '';
78    for (var i = 0; i < len; i++) s += String.fromCharCode(bytes[start + i]);
79    return s;
80  }
81
82  /**
83   * Detect the real image format from magic bytes.
84   * Returns one of: 'jpeg' | 'png' | 'gif' | 'bmp' | 'webp' | 'tiff' |
85   *                  'heic' | 'avif' | 'unknown'
86   */
87  function _detectFormat(bytes) {
88    if (!bytes || bytes.length < 12) return 'unknown';
89
90    // JPEG: FF D8 FF
91    if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) return 'jpeg';
92
93    // PNG: 89 50 4E 47 0D 0A 1A 0A
94    if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) return 'png';
95
96    // GIF: "GIF87a" / "GIF89a"
97    if (_bytesToAscii(bytes, 0, 4) === 'GIF8') return 'gif';
98
99    // BMP: "BM"
100    if (bytes[0] === 0x42 && bytes[1] === 0x4D) return 'bmp';
101
102    // WEBP: "RIFF"....."WEBP"
103    if (_bytesToAscii(bytes, 0, 4) === 'RIFF' && _bytesToAscii(bytes, 8, 4) === 'WEBP') return 'webp';
104
105    // TIFF: little-endian "II*\0" or big-endian "MM\0*"
106    if ((bytes[0] === 0x49 && bytes[1] === 0x49 && bytes[2] === 0x2A && bytes[3] === 0x00) ||
107        (bytes[0] === 0x4D && bytes[1] === 0x4D && bytes[2] === 0x00 && bytes[3] === 0x2A)) {
108      return 'tiff';
109    }
110
111    // ISO-BMFF container (HEIC/HEIF/AVIF): bytes 4-8 = "ftyp", brand at 8-12
112    if (_bytesToAscii(bytes, 4, 4) === 'ftyp') {
113      var brand = _bytesToAscii(bytes, 8, 4).toLowerCase();
114      var heicBrands = ['heic', 'heix', 'hevc', 'hevx', 'heim', 'heis', 'hevm', 'hevs', 'mif1', 'msf1'];
115      var avifBrands = ['avif', 'avis'];
116      if (heicBrands.indexOf(brand) !== -1) return 'heic';
117      if (avifBrands.indexOf(brand) !== -1) return 'avif';
118      return 'unknown'; // some other ISO-BMFF brand (e.g. plain MP4) — not an image we support
119    }
120
121    return 'unknown';
122  }
123
124  // ── EXIF orientation (JPEG only) ────────────────────────────────────────
125
126  /**
127   * Parse the EXIF Orientation tag (0x0112) out of a JPEG ArrayBuffer.
128   * Returns an integer 1-8 (1 = normal / no correction needed).
129   * Returns 1 on any parse failure (safe default — no rotation applied).
130   */
131  function _readJpegExifOrientation(buffer) {
132    try {
133      var view = new DataView(buffer);
134      if (view.getUint16(0, false) !== 0xFFD8) return 1; // not a JPEG SOI marker
135
136      var offset = 2;
137      var length = view.byteLength;
138      while (offset < length) {
139        var marker = view.getUint16(offset, false);
140        offset += 2;
141        if (marker === 0xFFE1) { // APP1
142          var app1Length = view.getUint16(offset, false);
143          var exifStart = offset + 2;
144          if (_bytesToAscii(new Uint8Array(buffer, exifStart, 4), 0, 4) !== 'Exif') {
145            offset += app1Length;
146            continue;
147          }
148          var tiffOffset = exifStart + 6; // skip "Exif\0\0"
149          var little = view.getUint16(tiffOffset, false) === 0x4949;
150          var firstIfdOffset = view.getUint32(tiffOffset + 4, little);
151          var dirStart = tiffOffset + firstIfdOffset;
152          var entries = view.getUint16(dirStart, little);
153          for (var i = 0; i < entries; i++) {
154            var entryOffset = dirStart + 2 + i * 12;
155            var tag = view.getUint16(entryOffset, little);
156            if (tag === 0x0112) {
157              var value = view.getUint16(entryOffset + 8, little);
158              return (value >= 1 && value <= 8) ? value : 1;
159            }
160          }
161          return 1; // APP1/Exif present but no Orientation tag
162        } else if ((marker & 0xFF00) !== 0xFF00) {
163          break; // not a valid marker — stop scanning
164        } else if (marker === 0xFFDA) {
165          break; // Start Of Scan — no more metadata markers follow
166        } else {
167          offset += view.getUint16(offset, false); // skip this segment
168        }
169      }
170      return 1;
171    } catch (_) {
172      return 1; // never let a malformed EXIF segment break intake
173    }
174  }
175
176  /** Apply the canvas transform matrix for a given EXIF orientation (1-8). */
177  function _applyOrientationTransform(ctx, orientation, w, h) {
178    switch (orientation) {
179      case 2: ctx.transform(-1, 0, 0, 1, w, 0); break;
180      case 3: ctx.transform(-1, 0, 0, -1, w, h); break;
181      case 4: ctx.transform(1, 0, 0, -1, 0, h); break;
182      case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
183      case 6: ctx.transform(0, 1, -1, 0, h, 0); break;
184      case 7: ctx.transform(0, -1, -1, 0, h, w); break;
185      case 8: ctx.transform(0, -1, 1, 0, 0, w); break;
186      default: break; // 1 = normal, no transform needed
187    }
188  }
189
190  /** True for orientations that swap the visual width/height (90°/270° rotations). */
191  function _orientationSwapsDims(orientation) {
192    return orientation >= 5 && orientation <= 8;
193  }
194
195  // ── Canvas normalization core ───────────────────────────────────────────
196  // Every supported format funnels through here exactly once: draws onto a
197  // WHITE-filled canvas (flattens any alpha transparency), applies EXIF
198  // rotation/flip when relevant, then re-encodes as a real JPEG. Re-encoding
199  // via canvas.toBlob() naturally drops all source metadata (EXIF, ICC
200  // profiles, XMP, etc.) since canvas only ever holds raw pixel data —
201  // satisfying "remove metadata" without any separate stripping step.
202
203  function _drawToJpegBlob(drawFn, srcW, srcH, orientation) {
204    return new Promise(function (resolve, reject) {
205      var swap = _orientationSwapsDims(orientation);
206      var canvas = document.createElement('canvas');
207      canvas.width  = swap ? srcH : srcW;
208      canvas.height = swap ? srcW : srcH;
209      var ctx = canvas.getContext('2d');
210
211      // Flatten transparency onto white BEFORE any orientation transform,
212      // so the fill always covers the final (rotated) canvas dimensions.
213      ctx.fillStyle = '#FFFFFF';
214      ctx.fillRect(0, 0, canvas.width, canvas.height);
215
216      ctx.save();
217      _applyOrientationTransform(ctx, orientation, srcW, srcH);
218      drawFn(ctx);
219      ctx.restore();
220
221      if (canvas.toBlob) {
222        canvas.toBlob(function (blob) {
223          if (!blob) { reject(new Error('Canvas re-encode produced no data')); return; }
224          resolve(blob);
225        }, 'image/jpeg', 0.92);
226      } else {
227        // Extremely old browsers only — toDataURL fallback.
228        var dataUrl = canvas.toDataURL('image/jpeg', 0.92);
229        var bin = atob(dataUrl.split(',')[1]);
230        var arr = new Uint8Array(bin.length);
231        for (var i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
232        resolve(new Blob([arr], { type: 'image/jpeg' }));
233      }
234    });
235  }
236
237  function _blobToFile(blob, originalName) {
238    var base = (originalName || 'image').replace(/\.[^.]+$/, '');
239    var name = base + '.jpg';
240    try {
241      return new File([blob], name, { type: 'image/jpeg', lastModified: Date.now() });
242    } catch (_) {
243      // Safari/old-browser File constructor fallback: attach fields to the Blob.
244      blob.name = name;
245      blob.lastModifiedDate = new Date();
246      return blob;
247    }
248  }
249
250  // ── Path A: formats the browser's own <img>/canvas can decode natively ──
251  // (jpeg, png, gif [first frame], bmp, webp, avif-if-supported)
252
253  function _normalizeViaImageElement(file, orientation) {
254    return new Promise(function (resolve, reject) {
255      var url = URL.createObjectURL(file);
256      var img = new Image();
257      img.onload = function () {
258        URL.revokeObjectURL(url);
259        var w = img.naturalWidth, h = img.naturalHeight;
260        if (!w || !h) { reject(_err('INTAKE_DECODE_FAILED', 'Image decoded to 0×0 dimensions.')); return; }
261        _drawToJpegBlob(function (ctx) { ctx.drawImage(img, 0, 0, w, h); }, w, h, orientation)
262          .then(function (blob) { resolve(_blobToFile(blob, file.name)); })
263          .catch(reject);
264      };
265      img.onerror = function () {
266        URL.revokeObjectURL(url);
267        reject(_err('INTAKE_DECODE_UNSUPPORTED',
268          'This browser could not decode the image. See format limitation notes.'));
269      };
270      img.src = url;
271    });
272  }
273
274  // ── Path B: HEIC / HEIF via vendored heic2any (WASM libheif port) ───────
275
276  function _normalizeHeic(file) {
277    return _loadScriptOnce(VENDOR_BASE + 'heic2any.min.js').then(function () {
278      if (typeof global.heic2any !== 'function') {
279        return Promise.reject(_err('INTAKE_LIB_UNAVAILABLE', 'heic2any failed to initialize.'));
280      }
281      return global.heic2any({ blob: file, toType: 'image/jpeg', quality: 0.92 }).then(function (result) {
282        var jpegBlob = Array.isArray(result) ? result[0] : result;
283        // Re-run through the standard canvas pass: guarantees the SAME
284        // white-flatten + metadata-strip contract as every other format,
285        // rather than trusting heic2any's own JPEG bytes as the final output.
286        return _normalizeViaImageElement(jpegBlob, 1);
287      });
288    });
289  }
290
291  // ── Path C: TIFF via vendored UTIF.js (+ pako for Deflate compression) ──
292
293  function _normalizeTiff(file) {
294    return Promise.all([
295      _loadScriptOnce(VENDOR_BASE + 'pako.min.js'),
296      _loadScriptOnce(VENDOR_BASE + 'UTIF.js'),
297    ]).then(function () {
298      return new Promise(function (resolve, reject) {
299        var reader = new FileReader();
300        reader.onerror = function () { reject(_err('INTAKE_DECODE_FAILED', 'Cannot read TIFF file.')); };
301        reader.onload = function () {
302          try {
303            var UTIF = global.UTIF;
304            var buffer = reader.result;
305            var ifds = UTIF.decode(buffer);
306            if (!ifds || !ifds.length) {
307              reject(_err('INTAKE_DECODE_FAILED', 'No image directories found in TIFF.'));
308              return;
309            }
310            var ifd = ifds[0];
311            UTIF.decodeImage(buffer, ifd);
312            var rgba = UTIF.toRGBA8(ifd);
313            var w = ifd.width, h = ifd.height;
314            if (!w || !h) {
315              reject(_err('INTAKE_DECODE_FAILED', 'TIFF decoded to 0×0 dimensions.'));
316              return;
317            }
318
319            var orientation = 1;
320            if (ifd.t274 && ifd.t274.length) {
321              var v = ifd.t274[0];
322              if (v >= 1 && v <= 8) orientation = v;
323            }
324
325            // Paint the decoded RGBA buffer onto an intermediate canvas first
326            // (putImageData ignores transforms/fills), then draw THAT canvas
327            // through the shared white-flatten + orientation pipeline.
328            var srcCanvas = document.createElement('canvas');
329            srcCanvas.width = w;
330            srcCanvas.height = h;
331            var srcCtx = srcCanvas.getContext('2d');
332            var imageData = srcCtx.createImageData(w, h);
333            imageData.data.set(rgba);
334            srcCtx.putImageData(imageData, 0, 0);
335
336            _drawToJpegBlob(function (ctx) { ctx.drawImage(srcCanvas, 0, 0, w, h); }, w, h, orientation)
337              .then(function (blob) { resolve(_blobToFile(blob, file.name)); })
338              .catch(reject);
339          } catch (err) {
340            reject(_err('INTAKE_DECODE_FAILED', 'TIFF decode error: ' + err.message));
341          }
342        };
343        reader.readAsArrayBuffer(file);
344      });
345    });
346  }
347
348  function _err(code, message) {
349    var e = new Error(message);
350    e.code = code;
351    return e;
352  }
353
354  // ── Public entry point ──────────────────────────────────────────────────
355
356  /**
357   * Normalize any supported input file into a single, real JPEG File.
358   * @param {File} file
359   * @returns {Promise<File>}
360   */
361  function normalize(file) {
362    if (!file) return Promise.reject(_err('NO_FILE', 'No file provided.'));
363
364    return _readHeaderBytes(file, 32).then(function (bytes) {
365      var format = _detectFormat(bytes);
366
367      switch (format) {
368        case 'jpeg': {
369          return new Promise(function (resolve, reject) {
370            var fr = new FileReader();
371            fr.onload = function () {
372              var orientation = _readJpegExifOrientation(fr.result);
373              _normalizeViaImageElement(file, orientation).then(resolve, reject);
374            };
375            fr.onerror = function () {
376              // Cannot read for EXIF — proceed with no rotation rather than fail.
377              _normalizeViaImageElement(file, 1).then(resolve, reject);
378            };
379            fr.readAsArrayBuffer(file);
380          });
381        }
382
383        case 'png':
384        case 'gif':
385        case 'bmp':
386        case 'webp':
387        case 'avif':
388          // No EXIF orientation concept for these formats in practice.
389          return _normalizeViaImageElement(file, 1);
390
391        case 'heic':
392          return _normalizeHeic(file);
393
394        case 'tiff':
395          return _normalizeTiff(file);
396
397        case 'unknown':
398        default:
399          return Promise.reject(_err('INTAKE_UNSUPPORTED_FORMAT',
400            'Unrecognized image format (no matching file signature).'));
401      }
402    });
403  }
404
405  global.ImageIntake = { normalize: normalize, _detectFormat: _detectFormat };
406})(window);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.