\n \n \n )\n}\n\nconst GalleryDesktop = ({ data, width, targetHeight, maximumHeight, shouldBlur }: { data: SearchResultItem[], width: number, targetHeight: number, maximumHeight: number, shouldBlur: boolean }) => {\n const gallery = new Gallery(targetHeight, maximumHeight);\n const [render, setRender] = React.useState[]>()\n\n React.useEffect(() => {\n renderGallery();\n }, [width]);\n\n function renderGallery() {\n data.forEach(result => {\n let aspectRatio = result.thumbnail.width / result.thumbnail.height;\n\n let width = result.thumbnail.width;\n let height = result.thumbnail.height;\n\n // Images which are either very tall or wide, can screw with the gallery's\n // ability to fit them nicely in the grid. So, in order to fix that, we\n // force those images to be displayed as squares instead, just like on the mobile UI.\n if (aspectRatio < 0.6 || aspectRatio > 2) {\n width = 250;\n height = 250;\n }\n\n gallery.addImage(width, height, result);\n });\n\n const newRender = gallery.render(width, 6);\n\n const minNumberOfImages = 12;\n const minNumberOfRows = 3;\n let currentNumberOfImages = 0;\n let i = 0;\n for (; i < newRender.length; i++) {\n var currentRow = newRender[i];\n currentNumberOfImages += currentRow.images.length;\n\n if (currentNumberOfImages >= minNumberOfImages && minNumberOfRows <= i + 1) {\n break;\n }\n }\n\n setRender(newRender.slice(0, i + 1));\n }\n\n return (\n
\n Searched {data.stats.count.toLocaleString()} images in {data.stats.elapsedMilliseconds.toLocaleString()} ms\n
\n
\n {\n {\n 0: \"Oh noes! Looks like we couldn't find what you're looking for\",\n 1: \"Jolly good! We found an image similar to the one you submitted\"\n }[data.probableResults.length] || \"Jolly good! We found images similar to the one you submitted\"\n }\n
\n Searched {data.stats.count.toLocaleString()} images in {data.stats.elapsedMilliseconds.toLocaleString()} ms\n
\n
\n {\n {\n 0: \"Oh noes! Couldn't find what you're looking for\",\n 1: \"Jolly good! We found a similar image\"\n }[data.probableResults.length] || \"Jolly good! We found similar images\"\n }\n
\n
\n
\n
\n
\n
\n
\n We've hidden improbable matches to keep you from viewing content you might experience as disturbing\n
\n \n
\n \n
\n
\n
\n )\n};\n\nexport default SearchResultMobile\n","// extracted by mini-css-extract-plugin\nexport var dropZone = \"index-module--drop-zone--fWn+W\";\nexport var dropZoneActive = \"index-module--drop-zone-active--MIkqx\";","import * as React from 'react'\nimport Layout from '../components/layout'\nimport Banner from '../components/banner'\nimport { Link } from 'gatsby'\nimport ProgressBar from '../components/progress-bar'\nimport ProgressBarPart from '../components/progress-bar-part'\nimport Api, { SearchResult } from '../services/api'\nimport SearchResultDesktop from '../components/search-result-desktop'\nimport { Helmet } from 'react-helmet'\nimport Icon from '../components/icon'\nimport SearchConfig from '../services/search-config'\nimport SearchResultMobile from '../components/search-result-mobile'\nimport classNames from 'classnames'\nimport { dropZone, dropZoneActive } from './index.module.scss'\n\nconst State = {\n ERROR: -1,\n IDLE: 0,\n PREPROCESSING: 1,\n UPLOADING: 2,\n PROCESSING: 3,\n DONE: 4\n};\n\nconst SearchPage = () => {\n let searchConfig = SearchConfig();\n\n const canvasRef: React.RefObject = React.useRef();\n const containerRef: React.RefObject = React.useRef();\n\n const [state, setState] = React.useState(State.IDLE);\n const [data, setData] = React.useState(null);\n const [errorMessage, setErrorMessage] = React.useState(null);\n const [progress, setProgress] = React.useState(0);\n const [hasDrag, setHasDrag] = React.useState(false);\n\n function setError(message) {\n setErrorMessage(message);\n setState(State.ERROR);\n }\n\n function calculateThumbnailSize(width, height, target) {\n let determineSize = (sizeOne, sizeTwo, sizeOneTarget) => {\n var aspectRatio = sizeOneTarget / sizeOne;\n\n return Math.round(aspectRatio * sizeTwo);\n };\n\n if (width === height) {\n return [target, target];\n }\n\n return width > height\n ? [determineSize(height, width, target), target]\n : [target, determineSize(width, height, target)];\n }\n\n function search(files: FileList) {\n setState(State.IDLE);\n setData(null);\n setErrorMessage(null);\n setProgress(0);\n\n if (files.length > 1) {\n setError('You can only reverse search one image at a time.');\n return;\n }\n const file = files[0];\n\n setState(State.PREPROCESSING);\n\n const image = new Image();\n const canvas = canvasRef.current;\n image.onload = () => {\n const target = 256;\n const thumbnailSize = calculateThumbnailSize(image.width, image.height, target);\n\n // In the first place we scaled down the image to a fixed size (250x250), but that\n // caused such a significant loss in image quality in some instances that we had to\n // scale preserving the aspect ratio of the original image \n canvas.width = thumbnailSize[0];\n canvas.height = thumbnailSize[1];\n const ctx = canvas.getContext('2d');\n ctx.drawImage(image, 0, 0, canvas.width, canvas.height);\n\n // Convert image drawn on canvas to blob\n const dataUri = canvas.toDataURL('image/png');\n const base64EncodedData = dataUri.split(',')[1];\n const data = atob(base64EncodedData);\n const array = new Uint8Array(data.length);\n for (let i = 0; i < data.length; i++) {\n array[i] = data.charCodeAt(i);\n }\n const thumbnail = new Blob([array]);\n\n searchInternal(file, thumbnail);\n };\n\n // The error might simply be that the image format isn't supported by the canvas.\n // Therefore, we should still send it to the server.\n image.onerror = () => {\n searchInternal(file, file);\n }\n\n image.src = URL.createObjectURL(file);\n }\n\n function searchInternal(file: Blob, thumbnail: Blob) {\n setState(State.UPLOADING);\n Api.search(file, thumbnail, searchConfig.includeNsfw, 32, {\n onUploadProgress: e => {\n const progress = Math.round(e.loaded / e.total * 100);\n\n setProgress(progress);\n\n if (progress === 100) {\n setState(State.PROCESSING);\n }\n }\n }).then(data => {\n setData(data);\n setState(State.DONE);\n\n // TODO: Using a timeout has proven unrealiable in the Angular version of the application\n setTimeout(() => {\n containerRef.current.scrollIntoView({\n behavior: 'smooth'\n });\n }, 250);\n }).catch(message => {\n setError(message);\n });\n }\n\n function onSelect(event) {\n search(event.target.files);\n }\n\n function onDragover(event) {\n setHasDrag(true);\n event.preventDefault();\n }\n\n function onDragLeave(event) {\n setHasDrag(false);\n event.preventDefault();\n }\n\n function onDrop(event) {\n // Prevent file from being opened\n event.preventDefault();\n setHasDrag(false);\n\n if (event.dataTransfer.files.length == 0) {\n setError('Did you drop a file which originates from the browser? Due to your browser its limitations, Fluffle cannot access those files. Save the file to your device first, then submit this file instead.')\n return;\n }\n\n search(event.dataTransfer.files);\n }\n\n // Safari doesn't handle clickable elements the same way as other browsers.\n // We need to explicitly cancel the opening of a file dialog when another element\n // is clicked on. We use a custom ignore attribute for that.\n function onLabelClick(event) {\n if (event.target.hasAttribute('data-ignore')) {\n event.preventDefault();\n }\n }\n\n return (\n \n \n \n \n
\n
\n
\n \n \n \n {process.env.VERSION}\n \n \n
\n
\n A reverse image search service tailored to the furry community\n