diff --git a/scripts/on_sd_start.bat b/scripts/on_sd_start.bat index 3e0299d6..1a694da5 100644 --- a/scripts/on_sd_start.bat +++ b/scripts/on_sd_start.bat @@ -15,7 +15,7 @@ @call git reset --hard @call git pull - @call git checkout d154155d4c0b43e13ec1f00eb72b7ff9d522fcf9 + @call git checkout f6cfebffa752ee11a7b07497b8529d5971de916c @call git apply ..\ui\sd_internal\ddim_callback.patch @call git apply ..\ui\sd_internal\env_yaml.patch @@ -33,7 +33,7 @@ ) @cd stable-diffusion - @call git checkout d154155d4c0b43e13ec1f00eb72b7ff9d522fcf9 + @call git checkout f6cfebffa752ee11a7b07497b8529d5971de916c @call git apply ..\ui\sd_internal\ddim_callback.patch @call git apply ..\ui\sd_internal\env_yaml.patch diff --git a/scripts/on_sd_start.sh b/scripts/on_sd_start.sh index e581232b..2941764f 100755 --- a/scripts/on_sd_start.sh +++ b/scripts/on_sd_start.sh @@ -16,7 +16,7 @@ if [ -e "scripts/install_status.txt" ] && [ `grep -c sd_git_cloned scripts/insta git reset --hard git pull - git checkout d154155d4c0b43e13ec1f00eb72b7ff9d522fcf9 + git checkout f6cfebffa752ee11a7b07497b8529d5971de916c git apply ../ui/sd_internal/ddim_callback.patch git apply ../ui/sd_internal/env_yaml.patch @@ -34,7 +34,7 @@ else fi cd stable-diffusion - git checkout d154155d4c0b43e13ec1f00eb72b7ff9d522fcf9 + git checkout f6cfebffa752ee11a7b07497b8529d5971de916c git apply ../ui/sd_internal/ddim_callback.patch git apply ../ui/sd_internal/env_yaml.patch diff --git a/ui/index.html b/ui/index.html index 677a856f..5b2675f5 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1,6 +1,8 @@ + + + + + +
-
-
-
-
-
 
- Stable Diffusion is starting.. +
+ +
    + + +
+
+ +
+
+
+
+ Stable Diffusion is starting..
@@ -291,10 +443,15 @@
-
+
+
- + + +
+ +
@@ -310,22 +467,25 @@
 
-
-

Advanced Settings

+
+

Image Settings

    -
  • -
  • - -
  • +
  • (images at once)
  • +
  • +
  • -
  • -
    -
  • -
  • (images at once)
  • -
  • +
  • -
  • -
  • + +
  • -
  • -
  • -

  • -
  •  
  • -
  • -
  • -
  • -
  • -
  • - +
  • +
  • +

  • +
    -
  • + +
  • Render Settings
  • +
  • +
  • +
  • + + +
  • +
  • +
    +
  • The system-related settings have been moved to the top-right corner.
@@ -393,10 +559,15 @@
-
-
Type a prompt and press the "Make Image" button.

You can set an "Initial Image" if you want to guide the AI.

You can also add modifiers like "Realistic", "Pencil Sketch", "ArtStation" etc by browsing through the "Image Modifiers" section and selecting the desired modifiers.

Click "Advanced Settings" for additional settings like seed, image size, number of images to generate etc.

Enjoy! :)
+
+
+
+ Type a prompt and press the "Make Image" button.

You can set an "Initial Image" if you want to guide the AI.

You can also add modifiers like "Realistic", "Pencil Sketch", "ArtStation" etc by browsing through the "Image Modifiers" section and selecting the desired modifiers.

Click "Advanced Settings" for additional settings like seed, image size, number of images to generate etc.

Enjoy! :) +
+
+
@@ -428,11 +599,14 @@ const MODIFIERS_PANEL_OPEN_KEY = "modifiersPanelOpen" const USE_FACE_CORRECTION_KEY = "useFaceCorrection" const USE_UPSCALING_KEY = "useUpscaling" const SHOW_ONLY_FILTERED_IMAGE_KEY = "showOnlyFilteredImage" +const STREAM_IMAGE_PROGRESS_KEY = "streamImageProgress" const HEALTH_PING_INTERVAL = 5 // seconds const MAX_INIT_IMAGE_DIMENSION = 768 const IMAGE_REGEX = new RegExp('data:image/[A-Za-z]+;base64') +let sessionId = new Date().getTime() + let promptField = document.querySelector('#prompt') let numOutputsTotalField = document.querySelector('#num_outputs_total') let numOutputsParallelField = document.querySelector('#num_outputs_parallel') @@ -445,8 +619,8 @@ let widthField = document.querySelector('#width') let heightField = document.querySelector('#height') let initImageSelector = document.querySelector("#init_image") let initImagePreview = document.querySelector("#init_image_preview") -// let maskImageSelector = document.querySelector("#mask") -// let maskImagePreview = document.querySelector("#mask_preview") +let maskImageSelector = document.querySelector("#mask") +let maskImagePreview = document.querySelector("#mask_preview") let turboField = document.querySelector('#turbo') let useCPUField = document.querySelector('#use_cpu') let useFullPrecisionField = document.querySelector('#use_full_precision') @@ -456,23 +630,27 @@ let diskPathField = document.querySelector('#diskPath') let useBetaChannelField = document.querySelector("#use_beta_channel") let promptStrengthSlider = document.querySelector('#prompt_strength_slider') let promptStrengthField = document.querySelector('#prompt_strength') +let samplerField = document.querySelector('#sampler') +let samplerSelectionContainer = document.querySelector("#samplerSelection") let useFaceCorrectionField = document.querySelector("#use_face_correction") let useUpscalingField = document.querySelector("#use_upscale") let upscaleModelField = document.querySelector("#upscale_model") let showOnlyFilteredImageField = document.querySelector("#show_only_filtered_image") let updateBranchLabel = document.querySelector("#updateBranchLabel") +let streamImageProgressField = document.querySelector("#stream_image_progress") let makeImageBtn = document.querySelector('#makeImage') let stopImageBtn = document.querySelector('#stopImage') let imagesContainer = document.querySelector('#current-images') let initImagePreviewContainer = document.querySelector('#init_image_preview_container') -let initImageClearBtn = document.querySelector('#init_image_clear') +let initImageClearBtn = document.querySelector('.init_image_clear') let promptStrengthContainer = document.querySelector('#prompt_strength_container') -// let maskSetting = document.querySelector('#mask_setting') +// let maskSetting = document.querySelector('#editor-inputs-mask_setting') // let maskImagePreviewContainer = document.querySelector('#mask_preview_container') // let maskImageClearBtn = document.querySelector('#mask_clear') +let maskSetting = document.querySelector('#enable_mask') let editorModifierEntries = document.querySelector('#editor-modifiers-entries') let editorModifierTagsList = document.querySelector('#editor-inputs-tags-list') @@ -483,6 +661,7 @@ let previewPrompt = document.querySelector('#preview-prompt') let showConfigToggle = document.querySelector('#configToggleBtn') // let configBox = document.querySelector('#config') let outputMsg = document.querySelector('#outputMsg') +let progressBar = document.querySelector("#progressBar") let soundToggle = document.querySelector('#sound_toggle') @@ -491,11 +670,35 @@ let serverStatusMsg = document.querySelector('#server-status-msg') let advancedPanelHandle = document.querySelector("#editor-settings .collapsible") let modifiersPanelHandle = document.querySelector("#editor-modifiers .collapsible") +let inpaintingEditorContainer = document.querySelector('#inpaintingEditor') +let inpaintingEditor = new DrawingBoard.Board('inpaintingEditor', { + color: "#ffffff", + background: false, + size: 30, + webStorage: false, + controls: [{'DrawingMode': {'filler': false}}, 'Size', 'Navigation'] +}) +let inpaintingEditorCanvasBackground = document.querySelector('.drawing-board-canvas-wrapper') +// let inpaintingEditorControls = document.querySelector('.drawing-board-controls') + +// let inpaintingEditorMetaControl = document.createElement('div') +// inpaintingEditorMetaControl.className = 'drawing-board-control' +// let initImageClearBtnToolbar = document.createElement('button') +// initImageClearBtnToolbar.className = 'init_image_clear' +// initImageClearBtnToolbar.innerHTML = 'Remove Image' +// inpaintingEditorMetaControl.appendChild(initImageClearBtnToolbar) +// inpaintingEditorControls.appendChild(inpaintingEditorMetaControl) + +let maskResetButton = document.querySelector('.drawing-board-control-navigation-reset') +maskResetButton.innerHTML = 'Clear' +maskResetButton.style.fontWeight = 'normal' +maskResetButton.style.fontSize = '10pt' let serverStatus = 'offline' let activeTags = [] let lastPromptUsed = '' let taskStopped = true +let batchesDone = 0 function getLocalStorageItem(key, fallback) { let item = localStorage.getItem(key) @@ -571,6 +774,10 @@ function isModifiersPanelOpenEnabled() { return getLocalStorageBoolItem(MODIFIERS_PANEL_OPEN_KEY, false) } +function isStreamImageProgressEnabled() { + return getLocalStorageBoolItem(STREAM_IMAGE_PROGRESS_KEY, false) +} + function setStatus(statusType, msg, msgType) { if (statusType !== 'server') { return; @@ -578,12 +785,12 @@ function setStatus(statusType, msg, msgType) { if (msgType == 'error') { // msg = '' + msg + '' - serverStatusColor.style.backgroundColor = 'red' + serverStatusColor.style.color = 'red' serverStatusMsg.style.color = 'red' serverStatusMsg.innerHTML = 'Stable Diffusion has stopped' } else if (msgType == 'success') { // msg = '' + msg + '' - serverStatusColor.style.backgroundColor = 'green' + serverStatusColor.style.color = 'green' serverStatusMsg.style.color = 'green' serverStatusMsg.innerHTML = 'Stable Diffusion is ready' serverStatus = 'online' @@ -630,14 +837,37 @@ async function healthCheck() { } } +function makeImageElement(width, height) { + let imgItem = document.createElement('div') + imgItem.className = 'imgItem' + + let img = document.createElement('img') + img.width = parseInt(width) + img.height = parseInt(height) + + imgItem.appendChild(img) + imagesContainer.appendChild(imgItem) + + return imgItem +} + // makes a single image. don't call this directly, use makeImage() instead -async function doMakeImage(reqBody) { +async function doMakeImage(reqBody, batchCount) { if (taskStopped) { return } let res = '' let seed = reqBody['seed'] + let numOutputs = parseInt(reqBody['num_outputs']) + + let images = [] + + function makeImageContainers(numImages) { + for (let i = images.length; i < numImages; i++) { + images.push(makeImageElement(reqBody.width, reqBody.height)) + } + } try { res = await fetch('/image', { @@ -648,15 +878,82 @@ async function doMakeImage(reqBody) { body: JSON.stringify(reqBody) }) + let reader = res.body.getReader() + let textDecoder = new TextDecoder() + let finalJSON = '' + let prevTime = -1 + while (true) { + try { + let t = new Date().getTime() + + const {value, done} = await reader.read() + if (done) { + break + } + + let timeTaken = (prevTime === -1 ? -1 : t - prevTime) + + let jsonStr = textDecoder.decode(value) + + try { + let stepUpdate = JSON.parse(jsonStr) + + if (stepUpdate.step === undefined) { + finalJSON += jsonStr + } else { + let batchSize = stepUpdate.total_steps + let overallStepCount = stepUpdate.step + batchesDone * batchSize + let totalSteps = batchCount * batchSize + let percent = 100 * (overallStepCount / totalSteps) + percent = (percent > 100 ? 100 : percent) + percent = percent.toFixed(0) + + stepsRemaining = totalSteps - overallStepCount + stepsRemaining = (stepsRemaining < 0 ? 0 : stepsRemaining) + timeRemaining = (timeTaken === -1 ? '' : stepsRemaining * timeTaken) // ms + + outputMsg.innerHTML = `Batch ${batchesDone+1} of ${batchCount}` + progressBar.innerHTML = `Generating image(s): ${percent}%` + + if (timeTaken !== -1) { + progressBar.innerHTML += `
Time remaining (approx): ${millisecondsToStr(timeRemaining)}` + } + progressBar.style.display = 'block' + + if (stepUpdate.output !== undefined) { + makeImageContainers(numOutputs) + + for (idx in stepUpdate.output) { + let imgItem = images[idx] + let img = imgItem.firstChild + let tmpImageData = stepUpdate.output[idx] + img.src = tmpImageData['path'] + '?t=' + new Date().getTime() + } + } + } + } catch (e) { + finalJSON += jsonStr + } + + prevTime = t + } catch (e) { + logError('Stable Diffusion had an error. Please check the logs in the command-line window.', res) + res = undefined + throw e + } + } + if (res.status != 200) { if (serverStatus === 'online') { - logError('Stable Diffusion had an error: ' + await res.text() + '. This happens sometimes. Maybe modify the prompt or seed a little bit?', res) + logError('Stable Diffusion had an error: ' + await res.text(), res) } else { - logError("Stable Diffusion is still starting up, please wait. If this goes on beyond a few minutes, Stable Diffusion has probably crashed.", res) + logError("Stable Diffusion is still starting up, please wait. If this goes on beyond a few minutes, Stable Diffusion has probably crashed. Please check the error message in the command-line window.", res) } res = undefined + progressBar.style.display = 'none' } else { - res = await res.json() + res = JSON.parse(finalJSON) + progressBar.style.display = 'none' if (res.status !== 'succeeded') { let msg = '' @@ -680,7 +977,10 @@ async function doMakeImage(reqBody) { } } catch (e) { console.log('request error', e) + logError('Stable Diffusion had an error. Please check the logs in the command-line window.

' + e + '
' + e.stack + '
', res) setStatus('request', 'error', 'error') + progressBar.style.display = 'none' + res = undefined } if (!res) { @@ -689,6 +989,8 @@ async function doMakeImage(reqBody) { lastPromptUsed = reqBody['prompt'] + makeImageContainers(res.output.length) + for (let idx in res.output) { let imgBody = '' let seed = 0 @@ -703,12 +1005,9 @@ async function doMakeImage(reqBody) { continue } - let imgItem = document.createElement('div') - imgItem.className = 'imgItem' + let imgItem = images[idx] + let img = imgItem.firstChild - let img = document.createElement('img') - img.width = parseInt(reqBody.width) - img.height = parseInt(reqBody.height) img.src = imgBody let imgItemInfo = document.createElement('span') @@ -726,19 +1025,19 @@ async function doMakeImage(reqBody) { imgSaveBtn.className = 'imgSaveBtn' imgSaveBtn.innerHTML = 'Download' - imgItem.appendChild(img) imgItem.appendChild(imgItemInfo) imgItemInfo.appendChild(imgSeedLabel) imgItemInfo.appendChild(imgUseBtn) imgItemInfo.appendChild(imgSaveBtn) - imagesContainer.appendChild(imgItem) imgUseBtn.addEventListener('click', function() { initImageSelector.value = null initImagePreview.src = imgBody initImagePreviewContainer.style.display = 'block' + inpaintingEditorContainer.style.display = 'none' promptStrengthContainer.style.display = 'block' + maskSetting.checked = false // maskSetting.style.display = 'block' @@ -789,7 +1088,7 @@ async function makeImage() { let validation = validateInput() if (validation['isValid']) { - outputMsg.innerHTML = 'Fetching..' + outputMsg.innerHTML = 'Starting..' } else { if (validation['error']) { logError(validation['error']) @@ -802,10 +1101,12 @@ async function makeImage() { setStatus('request', 'fetching..') makeImageBtn.innerHTML = 'Processing..' + makeImageBtn.disabled = true makeImageBtn.style.display = 'none' stopImageBtn.style.display = 'block' taskStopped = false + batchesDone = 0 let seed = (randomSeedField.checked ? Math.floor(Math.random() * 10000000) : parseInt(seedField.value)) let numOutputsTotal = parseInt(numOutputsTotalField.value) @@ -813,6 +1114,8 @@ async function makeImage() { let batchCount = Math.ceil(numOutputsTotal / numOutputsParallel) let batchSize = numOutputsParallel + let streamImageProgress = (numOutputsTotal > 50 ? false : streamImageProgressField.checked) + let prompt = promptField.value if (activeTags.length > 0) { let promptTags = activeTags.join(", ") @@ -822,6 +1125,7 @@ async function makeImage() { previewPrompt.innerHTML = prompt let reqBody = { + session_id: sessionId, prompt: prompt, num_outputs: batchSize, num_inference_steps: numInferenceStepsField.value, @@ -831,7 +1135,10 @@ async function makeImage() { // allow_nsfw: allowNSFWField.checked, turbo: turboField.checked, use_cpu: useCPUField.checked, - use_full_precision: useFullPrecisionField.checked + use_full_precision: useFullPrecisionField.checked, + stream_progress_updates: true, + stream_image_progress: streamImageProgress, + show_only_filtered_image: showOnlyFilteredImageField.checked } if (IMAGE_REGEX.test(initImagePreview.src)) { @@ -841,6 +1148,13 @@ async function makeImage() { // if (IMAGE_REGEX.test(maskImagePreview.src)) { // reqBody['mask'] = maskImagePreview.src // } + if (maskSetting.checked) { + reqBody['mask'] = inpaintingEditor.getImg() + } + + reqBody['sampler'] = 'ddim' + } else { + reqBody['sampler'] = samplerField.value } if (saveToDiskField.checked && diskPathField.value.trim() !== '') { @@ -855,10 +1169,6 @@ async function makeImage() { reqBody['use_upscale'] = upscaleModelField.value } - if (showOnlyFilteredImageField.checked && (useUpscalingField.checked || useFaceCorrectionField.checked)) { - reqBody['show_only_filtered_image'] = showOnlyFilteredImageField.checked - } - let time = new Date().getTime() imagesContainer.innerHTML = '' @@ -867,7 +1177,8 @@ async function makeImage() { for (let i = 0; i < batchCount; i++) { reqBody['seed'] = seed + (i * batchSize) - let success = await doMakeImage(reqBody) + let success = await doMakeImage(reqBody, batchCount) + batchesDone++ if (success) { outputMsg.innerHTML = 'Processed batch ' + (i+1) + '/' + batchCount @@ -971,6 +1282,9 @@ useFullPrecisionField.checked = isUseFullPrecisionEnabled() turboField.addEventListener('click', handleBoolSettingChange(USE_TURBO_MODE_KEY)) turboField.checked = isUseTurboModeEnabled() +streamImageProgressField.addEventListener('click', handleBoolSettingChange(STREAM_IMAGE_PROGRESS_KEY)) +streamImageProgressField.checked = isStreamImageProgressEnabled() + diskPathField.addEventListener('change', handleStringSettingChange(DISK_PATH_KEY)) saveToDiskField.addEventListener('click', function(e) { @@ -1007,8 +1321,8 @@ function updateGuidanceScale() { function updateGuidanceScaleSlider() { if (guidanceScaleField.value < 0) { guidanceScaleField.value = 0 - } else if (guidanceScaleField.value > 20) { - guidanceScaleField.value = 20 + } else if (guidanceScaleField.value > 50) { + guidanceScaleField.value = 50 } guidanceScaleSlider.value = guidanceScaleField.value * 10 @@ -1094,6 +1408,7 @@ checkRandomSeed() function showInitImagePreview() { if (initImageSelector.files.length === 0) { initImagePreviewContainer.style.display = 'none' + // inpaintingEditorContainer.style.display = 'none' promptStrengthContainer.style.display = 'none' // maskSetting.style.display = 'none' return @@ -1106,9 +1421,10 @@ function showInitImagePreview() { // console.log(file.name, reader.result) initImagePreview.src = reader.result initImagePreviewContainer.style.display = 'block' + inpaintingEditorContainer.style.display = 'none' promptStrengthContainer.style.display = 'block' - - // maskSetting.style.display = 'block' + samplerSelectionContainer.style.display = 'none' + // maskSetting.checked = false }) if (file) { @@ -1118,24 +1434,37 @@ function showInitImagePreview() { initImageSelector.addEventListener('change', showInitImagePreview) showInitImagePreview() +initImagePreview.addEventListener('load', function() { + inpaintingEditorCanvasBackground.style.backgroundImage = "url('" + this.src + "')" + // maskSetting.style.display = 'block' + // inpaintingEditorContainer.style.display = 'block' +}) + initImageClearBtn.addEventListener('click', function() { initImageSelector.value = null // maskImageSelector.value = null initImagePreview.src = '' // maskImagePreview.src = '' + maskSetting.checked = false initImagePreviewContainer.style.display = 'none' + // inpaintingEditorContainer.style.display = 'none' // maskImagePreviewContainer.style.display = 'none' // maskSetting.style.display = 'none' promptStrengthContainer.style.display = 'none' + samplerSelectionContainer.style.display = 'block' +}) + +maskSetting.addEventListener('click', function() { + inpaintingEditorContainer.style.display = (this.checked ? 'block' : 'none') }) // function showMaskImagePreview() { // if (maskImageSelector.files.length === 0) { -// maskImagePreviewContainer.style.display = 'none' +// // maskImagePreviewContainer.style.display = 'none' // return // } @@ -1143,8 +1472,8 @@ initImageClearBtn.addEventListener('click', function() { // let file = maskImageSelector.files[0] // reader.addEventListener('load', function() { -// maskImagePreview.src = reader.result -// maskImagePreviewContainer.style.display = 'block' +// // maskImagePreview.src = reader.result +// // maskImagePreviewContainer.style.display = 'block' // }) // if (file) { @@ -1157,8 +1486,32 @@ initImageClearBtn.addEventListener('click', function() { // maskImageClearBtn.addEventListener('click', function() { // maskImageSelector.value = null // maskImagePreview.src = '' -// maskImagePreviewContainer.style.display = 'none' +// // maskImagePreviewContainer.style.display = 'none' // }) + +// https://stackoverflow.com/a/8212878 +function millisecondsToStr(milliseconds) { + function numberEnding (number) { + return (number > 1) ? 's' : ''; + } + + var temp = Math.floor(milliseconds / 1000); + var hours = Math.floor((temp %= 86400) / 3600); + var s = '' + if (hours) { + s += hours + ' hour' + numberEnding(hours) + ' '; + } + var minutes = Math.floor((temp %= 3600) / 60); + if (minutes) { + s += minutes + ' minute' + numberEnding(minutes) + ' '; + } + var seconds = temp % 60; + if (!hours && minutes < 4 && seconds) { + s += seconds + ' second' + numberEnding(seconds); + } + + return s; +} - diff --git a/ui/media/drawingboard.min.css b/ui/media/drawingboard.min.css new file mode 100644 index 00000000..7d804787 --- /dev/null +++ b/ui/media/drawingboard.min.css @@ -0,0 +1,5 @@ +/* drawingboard.js v0.4.6 - https://github.com/Leimi/drawingboard.js +* Copyright (c) 2015 Emmanuel Pelletier +* Licensed MIT */ + +.drawing-board,.drawing-board *{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.drawing-board-controls-hidden,.drawing-board-utils-hidden{display:none!important}.drawing-board{position:relative;display:block}.drawing-board-canvas-wrapper{position:relative;margin:0;border:1px solid #ddd}.drawing-board-canvas{position:absolute;top:0;left:0;width:auto;cursor:crosshair;z-index:20}.drawing-board-cursor{position:absolute;top:0;left:0;pointer-events:none;border-radius:50%;background:#ccc;background:rgba(0,0,0,.2);z-index:30}.drawing-board-control-colors-rainbows,.drawing-board-control-size .drawing-board-control-inner,.drawing-board-control-size-dropdown,.drawing-board-control>button{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;overflow:hidden;background-color:#eee;padding:2px 4px;border:1px solid #ccc;box-shadow:0 1px 3px -2px #121212,inset 0 2px 5px 0 rgba(255,255,255,.3);-webkit-box-shadow:0 1px 3px -2px #121212,inset 0 2px 5px 0 rgba(255,255,255,.3);height:28px}.drawing-board-control>button{cursor:pointer;min-width:28px;line-height:14px}.drawing-board-control>button:focus,.drawing-board-control>button:hover{background-color:#ddd}.drawing-board-control>button.active,.drawing-board-control>button:active{box-shadow:inset 0 1px 2px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 2px 0 rgba(0,0,0,.2);background-color:#ddd}.drawing-board-control>button[disabled]{color:gray}.drawing-board-control>button[disabled].active,.drawing-board-control>button[disabled]:active,.drawing-board-control>button[disabled]:focus,.drawing-board-control>button[disabled]:hover{background-color:#eee;box-shadow:0 1px 3px -2px #121212,inset 0 2px 5px 0 rgba(255,255,255,.3);-webkit-box-shadow:0 1px 3px -2px #121212,inset 0 2px 5px 0 rgba(255,255,255,.3);cursor:default}.drawing-board-controls{margin:0 auto;text-align:center;font-size:0;display:table;border-spacing:9.33px 0;position:relative;min-height:28px}.drawing-board-controls[data-align=left]{margin:0;left:-9.33px}.drawing-board-controls[data-align=right]{margin:0 0 0 auto;right:-9.33px}.drawing-board-canvas-wrapper+.drawing-board-controls,.drawing-board-controls+.drawing-board-canvas-wrapper{margin-top:5px}.drawing-board-controls-hidden{height:0;min-height:0;padding:0;margin:0;border:0}.drawing-board-control{display:table-cell;border-collapse:separate;vertical-align:middle;font-size:16px;height:100%}.drawing-board-control-inner{position:relative;height:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.drawing-board-control>button{margin:0;vertical-align:middle}.drawing-board-control-colors{font-size:0;line-height:0}.drawing-board-control-colors-current{border:1px solid #ccc;cursor:pointer;display:inline-block;width:26px;height:26px}.drawing-board-control-colors-rainbows{display:inline-block;position:absolute;left:0;top:33px;margin-left:0;z-index:100;width:250px;height:auto;padding:4px}.drawing-board-control-colors-rainbow{height:18px}.drawing-board-control-colors-picker:first-child{margin-right:5px}.drawing-board-control-colors-picker{display:inline-block;width:18px;height:18px;cursor:pointer}.drawing-board-control-colors-picker[data-color="rgba(255, 255, 255, 1)"]{width:16px;height:17px;border:1px solid #ccc;border-bottom:none}.drawing-board-control-colors-picker:hover{width:16px;height:16px;border:1px solid #555}.drawing-board-control-drawingmode>button{margin-right:2px}.drawing-board-control-drawingmode>button:last-child{margin-right:0}.drawing-board-control-drawingmode-pencil-button{overflow:hidden;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAe9JREFUeNpiZAACVlFRBhYREQZcQPnbNwa3N28YlL5+ZfgLFfvPwGD9m4FhIgsDHuAO0gTUDNKIBvyBmqt/MTDMY8Gl0f31azD7L6oUIxCnAzWmAPHBfwwM01AMUAV6JfPQIVwOYgVqqPnFyOjz6///O38YGKpAgmAD1OXlGdTk5PD5hgeouZudj8/uy9evP/78/dsFFPsJNiAoKIiBABAHap4oLi9v8fTNm48//v7NBwbgWZgkE7rqt8DY+A8JZRBW+cfIuEDT0NDlzadP3z98/doPFDuCrB7TAGFhBqCNIGwM9OcKUzs7+xdv3355+f79VqDYAiTDwZgJh7ONgYpnOvn4GL949erT7UePdgL5JVCD4fgBLBBxaX74+PG789evnwby0/8jKXgExIeB+CG6Af///1e9Ki9vFSAkZPzoyZPPJy9evA9MB77/sWiEARZkzV+/fvXYtGnTpG3btj28EBT0BqjZ5D8OjXCwPksUhA1Wpggf/PHjx/9169Y9EBERaUlgZmaIAcrLE4rk5sIqBqDmlefnRPzfWGX5EaSZm5ubgRloADGA5QZ3RgK7gESY4PMNn9ZtObPpzZvfU4DiYkiB/RcHG+S7fyxAMH/lFU2GOZd2bLx18/cEUMoD4j9I+DcS/RtJHGTYf4AAAwAxaOMYHjxKFwAAAABJRU5ErkJggg==);background-position:50% 50%;background-repeat:no-repeat}.drawing-board-control-drawingmode-pencil-button:before{content:"";display:block;width:0;height:100%}.drawing-board-control-drawingmode-eraser-button{overflow:hidden;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAkpJREFUeNp0kk+IElEcx39vFBc9+OfQRTAwzFt4CaYOKStj6MoeculStzoIQSB4kCVckmDx4iGCXWYJIqjoVOzO1l4qT1F7WfBWHvxzDPyTB3XUmXn93suRybUffHmP997n9/cRsFgwGARJkiAcDsPlwgEIeEZQAhCRAkgAlOD6SQP4rgMFDWVnYCAQgFgsBqFQCBwOByzZNQOotPHx1RNCCCipu6bfb+zSnslkeOQVILPrBkAirbws9btdTEWAzZPXpfepOzaeGMBXwe/3w3+MwTc3Dl+UeghTiskbBvR6Pbh18mZHB0jjmxvCKhIfR37s3r+Sevf8ca/T4TBF2HTSODuDxP7uNjrZFFbBk8lEzOVyspa4ykGYw2zfbTb/7ilvok1YhlVVFfP5vDydTkHXdXDdlhZOOnPY4/HA0YPtp3h6LFjh8XgsFgoFGTPgsKm1zDr8ajTQh8Fh5eGjZzjGI8yjKlgjF4tFGdd/YKYmRja24hw+zu3sYe2HiH3hYzQjl8tleTQanWtou93G6Qngdrth6+1+9h6hTULJZ/PeziJXKhV5OByeg1ut1gJOp9NZTdNOcQ419ot+ggp1qoLdBFmqVmNpm3A8Huewy+Wq1RH8QH9zmBlJJpMRdCIqiiIPBgN+2MCGsW/r8/kgGo1m0fmpzWarseayHlmNeL1eFiWC0cRqtSr3+/3FpSiKHMZtjU1glbFyfKgLTqfzEka9OJvNeDnzz1JnCaFmqOl8ZdJY1SiDOXCiXKg1NtG5DIt0y6ov3dE/AgwAENFWYYLj4mYAAAAASUVORK5CYII=);background-position:50% 50%;background-repeat:no-repeat}.drawing-board-control-drawingmode-eraser-button:before{content:"";display:block;width:0;height:100%}.drawing-board-control-drawingmode-filler-button{overflow:hidden;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnNJREFUeNp0k0trE1EUx89MJpNJooYYXBgDNtCKdRPwlbqoCKUtaNVNA0Uo7UbMxoVPEARTXEi+QWfnwn6DEAlEkrSLttTGRiULEQlJ8yChmbzI++E50yTUJA78uMy953/u/557LmOz2WDEZ2m1WrckSRJSqdR2tVrdHQyYebwHtVoNuFHqTqczhQnWKaBYLDoKhcIuzgHDMKBSqeD20qd+LNdsNocSoFhRr9ctpVLJigl4xIIJQizLAmG4cAPa7bYcy9Iug5TL5UYikbD6/X7Rbre/IUcYe3WUW5ZsnQQzW9LpNOPz+UQc5aBM5mgdh7vI9FCCAesW2tnr9YqZTAby+bw8f3AQRP6853n+Ph5hemSCntjj8YjZbFYWx2IxeS2RSEMwuA87O79eqdXquVolK+GxnP0EPbHb7RZJSGABIR6PA11zJHKIR2MhHA5DIPDj7eH3j95KpfK60Wg8Yntil8slkqgnpioLghacTidoNDpEC3q9HnheCc3s1jZeLcW943pirPw/4lKpBkqlDubnl/riycnLsLy88EKj0fhzuRyZv8RFo1E6wpBYkiqy7Z54YmIcVlYeyOKC4mYwJ0nHRaQuM5vNT6hB/iceG7sIq6sPnwmC4MerDkby40AOCCoiddie1Wp92W7zQ2KTyQSLizNP8T0EsPLBbxEDnCj0GkM2qIEwyZRCobizsfH5A1ZXFhuN52F29vpz3HkL574mk8lj24Y5wsHkvjjoX0BOIWc5jruHzbK2ufmzEwpFO3jnDhQv4JoROYdoERVyGjEgZ8iBDlF3FzXo4go6utZ9lftY4N/dXisjR0i1G0ublv8KMAA0ZoUlicxrhwAAAABJRU5ErkJggg==);background-position:50% 50%;background-repeat:no-repeat}.drawing-board-control-drawingmode-filler-button:before{content:"";display:block;width:0;height:100%}.drawing-board-control-navigation>button{font-family:Helvetica,Arial,sans-serif;font-size:14px;font-weight:700;margin-right:2px}.drawing-board-control-navigation>button:last-child{margin-right:0}.drawing-board-control-size[data-drawing-board-type=range] .drawing-board-control-inner{width:75px}.drawing-board-control-size[data-drawing-board-type=dropdown] .drawing-board-control-inner{overflow:visible}.drawing-board-control-size-range-input{position:relative;width:100%;z-index:100;margin:0;padding:0;border:0}.drawing-board-control-size-dropdown span,.drawing-board-control-size-dropdown-current span,.drawing-board-control-size-range-current{display:block;background:#333;opacity:.8}.drawing-board-control-size-range-current{display:inline-block;opacity:.15;position:absolute;pointer-events:none;left:50%;top:50%;z-index:50}.drawing-board-control-size-dropdown-current{display:block;height:100%;width:40px;overflow:hidden;position:relative}.drawing-board-control-size-dropdown-current span{position:absolute;left:50%;top:50%}.drawing-board-control-size-dropdown{position:absolute;left:-6px;top:33px;height:auto;list-style-type:none;margin:0;padding:0;z-index:100}.drawing-board-control-size-dropdown li{display:block;padding:4px;margin:3px 0;min-height:16px}.drawing-board-control-size-dropdown li:hover{background:#ccc}.drawing-board-control-size-dropdown span{margin:0 auto}.drawing-board-control-download-button{overflow:hidden;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAoBJREFUeNqMkr9PU1EUx7/vR1tQ3yu10hAmTawtBSYSy2YccFOcnDQm/gewOLnj5GYMg4sdXFxkMca4OBAwdUBe/ZkIGFp+9tHXvh/3/fTcAm01aLzJybnn3nM+95tzrnDl6Tb+sibuTmWUWj3C6/Juk+LySUmyvt0FCKKA02ryOCy6LBiu15ngMbZ5DDCNBqIw6gKM+n4nECUJru3glKry8CpjQaHVYmC2rVH82DIMMMdGGARdwJ+SPNdFS9chx+MXDNMp/NzagWNatk/nQU/hiYAoih6FYTBCBs9zUXMCbAhx2OYOv351lPOJ3EwH4LteL6Dcp/Rfu3FrstDyIizt+agpaYxNDU0M9gl4v7Ck+TYrCYLQqZHUyTtdQBiutPSGUflczSXHs5lVKwZdSOBMvwztxVvN0RtzsiyXBFHsAvL5PBSnCpXV2getILFiE2SjspYbuZzPiDSZ2vOXmlvX5yQqTmMfg9ZXqtls1wnT09OHEyAq0aFLg/gSXsSWq9wWk+p9PrCoYTwcijdLOfE7UsEufN9HGIYnT4EnTGIXe1KqtNNIvuNnGamxfi7SgQD/nIJCTbzOPQ/SQh1pud7T4M6W/8qFIw/5WAr5m7Ozsw9UVc069Fls2yJzSC5/lnc9RhaHZVnfSqUnEgXP2oBqtYqBgYG2+mKxmOVADnAcB4yxHgD1RzehKKns/LyV4gUHBweQy+UyRkdH6UKJ6fQDFxcXoWkaXJeRuTgUGCdLQJ9bx72lGZimGWs2m+083oN+2iiFQiGxvLy8RrDzudyltgrG3N8U2G8CrPz4sGYYRqJSqWR4H/jNWbJhUjAWi8XG8R/L87yPpGCVttVfAgwAVpZR+8tZC08AAAAASUVORK5CYII=);background-position:50% 50%;background-repeat:no-repeat}.drawing-board-control-download-button:before{content:"";display:block;width:0;height:100%} \ No newline at end of file diff --git a/ui/media/drawingboard.min.js b/ui/media/drawingboard.min.js new file mode 100644 index 00000000..289d40ae --- /dev/null +++ b/ui/media/drawingboard.min.js @@ -0,0 +1,4 @@ +/* drawingboard.js v0.4.6 - https://github.com/Leimi/drawingboard.js +* Copyright (c) 2015 Emmanuel Pelletier +* Licensed MIT */ +!function(){"use strict";function a(a,b){for(;a.length>b;)a.shift()}var b=function(a){var b=a?a:{},c={provider:function(){throw new Error("No provider!")},maxLength:30,onUpdate:function(){}};this.provider="undefined"!=typeof b.provider?b.provider:c.provider,this.maxLength="undefined"!=typeof b.maxLength?b.maxLength:c.maxLength,this.onUpdate="undefined"!=typeof b.onUpdate?b.onUpdate:c.onUpdate,this.initialItem=null,this.clear()};b.prototype.initialize=function(a){this.stack[0]=a,this.initialItem=a},b.prototype.clear=function(){this.stack=[this.initialItem],this.position=0,this.onUpdate()},b.prototype.save=function(){this.provider(function(b){a(this.stack,this.maxLength),this.position=Math.min(this.position,this.stack.length-1),this.stack=this.stack.slice(0,this.position+1),this.stack.push(b),this.position++,this.onUpdate()}.bind(this))},b.prototype.undo=function(a){if(this.canUndo()){var b=this.stack[--this.position];this.onUpdate(),a&&a(b)}},b.prototype.redo=function(a){if(this.canRedo()){var b=this.stack[++this.position];this.onUpdate(),a&&a(b)}},b.prototype.canUndo=function(){return this.position>0},b.prototype.canRedo=function(){return this.positionh;h++){if(g=g[e[h]],g===a)throw"tim: '"+e[h]+"' not found in "+b;if(h===f-1)return g}})}}(),DrawingBoard.Utils.MicroEvent=function(){},DrawingBoard.Utils.MicroEvent.prototype={bind:function(a,b){this._events=this._events||{},this._events[a]=this._events[a]||[],this._events[a].push(b)},unbind:function(a,b){this._events=this._events||{},a in this._events!=!1&&this._events[a].splice(this._events[a].indexOf(b),1)},trigger:function(a){if(this._events=this._events||{},a in this._events!=!1)for(var b=0;b=0;g--)f+=parseInt(a.css(e[g]).replace("px",""),10);return f},DrawingBoard.Utils.boxBorderWidth=function(a,b,c){return DrawingBoard.Utils._boxBorderSize(a,b,c,"width")},DrawingBoard.Utils.boxBorderHeight=function(a,b,c){return DrawingBoard.Utils._boxBorderSize(a,b,c,"height")},DrawingBoard.Utils.isColor=function(a){return a&&a.length?/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(a)||-1!==$.inArray(a.substring(0,3),["rgb","hsl"]):!1},DrawingBoard.Utils.RGBToInt=function(a,b,c){var d=0;return d|=(255&a)<<16,d|=(255&b)<<8,d|=255&c},DrawingBoard.Utils.pixelAt=function(a,b,c){var d=4*(c*a.width+b),e=DrawingBoard.Utils.RGBToInt(a.data[d],a.data[d+1],a.data[d+2]);return[d,b,c,e]},DrawingBoard.Utils.compareColors=function(a,b,c){if(0===c)return a===b;var d=a>>16&255,e=b>>16&255,f=a>>8&255,g=b>>8&255,h=255&a,i=255&b;return Math.abs(d-e)<=c&&Math.abs(f-g)<=c&&Math.abs(h-i)<=c},function(){for(var a=["ms","moz","webkit","o"],b=0;b-1?c+='
':c='
'+c,this.$el.addClass("drawing-board").append(c),this.dom={$canvasWrapper:this.$el.find(".drawing-board-canvas-wrapper"),$canvas:this.$el.find(".drawing-board-canvas"),$cursor:this.$el.find(".drawing-board-cursor"),$controls:this.$el.find(".drawing-board-controls")},$.each(["left","right","center"],$.proxy(function(a,b){return this.opts.controlsPosition.indexOf(b)>-1?(this.dom.$controls.attr("data-align",b),!1):void 0},this)),this.canvas=this.dom.$canvas.get(0),this.ctx=this.canvas&&this.canvas.getContext&&this.canvas.getContext("2d")?this.canvas.getContext("2d"):null,this.color=this.opts.color,this.ctx?(this.storage=this._getStorage(),this.initHistory(),this.reset({webStorage:!1,history:!1,background:!1}),this.initControls(),this.resize(),this.reset({webStorage:!1,history:!1,background:!0}),this.restoreWebStorage(),this.initDropEvents(),void this.initDrawEvents()):(this.opts.errorMessage&&this.$el.html(this.opts.errorMessage),!1)},DrawingBoard.Board.defaultOpts={controls:["Color","DrawingMode","Size","Navigation"],controlsPosition:"top left",color:"#000000",size:1,background:"#fff",eraserColor:"background",fillTolerance:100,fillHack:!0,webStorage:"session",droppable:!1,enlargeYourContainer:!1,errorMessage:'

It seems you use an obsolete browser. Update it to start drawing.

',stretchImg:!1},DrawingBoard.Board.prototype={mergeOptions:function(a){return a=$.extend({},DrawingBoard.Board.defaultOpts,a),a.background||"background"!==a.eraserColor||(a.eraserColor="transparent"),a},reset:function(a){a=$.extend({color:this.opts.color,size:this.opts.size,webStorage:!0,history:!0,background:!1},a),this.setMode("pencil"),a.background&&this.resetBackground(this.opts.background,$.proxy(function(){a.history&&this.saveHistory()},this)),a.color&&this.setColor(a.color),a.size&&(this.ctx.lineWidth=a.size),this.ctx.lineCap="round",this.ctx.lineJoin="round",a.webStorage&&this.saveWebStorage(),a.history&&!a.background&&this.saveHistory(),this.blankCanvas=this.getImg(),this.ev.trigger("board:reset",a)},resetBackground:function(a,b){a=a||this.opts.background;var c=DrawingBoard.Utils.isColor(a),d=this.getMode();this.setMode("pencil"),this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height),c?(this.ctx.fillStyle=a,this.ctx.fillRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height),this.history.initialize(this.getImg()),b&&b()):a&&this.setImg(a,{callback:$.proxy(function(){this.history.initialize(this.getImg()),b&&b()},this)}),this.setMode(d)},resize:function(){this.dom.$controls.toggleClass("drawing-board-controls-hidden",!this.controls||!this.controls.length);var a,b,c=[this.$el.width(),DrawingBoard.Utils.boxBorderWidth(this.$el),DrawingBoard.Utils.boxBorderWidth(this.dom.$canvasWrapper,!0,!0)],d=[this.$el.height(),DrawingBoard.Utils.boxBorderHeight(this.$el),this.dom.$controls.height(),DrawingBoard.Utils.boxBorderHeight(this.dom.$controls,!1,!0),DrawingBoard.Utils.boxBorderHeight(this.dom.$canvasWrapper,!0,!0)],e=function(a,b){b=b||1;for(var c=a[0],d=1;d0&&q.push(DrawingBoard.Utils.pixelAt(c,p[e]-1,p[f])),p[e]0&&q.push(DrawingBoard.Utils.pixelAt(c,p[e],p[f]-1)),p[f]10&&this.isMouseHovering){this.dom.$cursor.css({width:this.ctx.lineWidth+"px",height:this.ctx.lineWidth+"px"});var a=DrawingBoard.Utils.tpl("translateX({{x}}px) translateY({{y}}px)",{x:this.coords.current.x-this.ctx.lineWidth/2,y:this.coords.current.y-this.ctx.lineWidth/2});this.dom.$cursor.css({transform:a,"-webkit-transform":a,"-ms-transform":a}),this.dom.$cursor.removeClass("drawing-board-utils-hidden")}else this.dom.$cursor.addClass("drawing-board-utils-hidden");if(this.isDrawing){var b=this._getMidInputCoords(this.coords.current);this.ctx.beginPath(),this.ctx.moveTo(b.x,b.y),this.ctx.quadraticCurveTo(this.coords.old.x,this.coords.old.y,this.coords.oldMid.x,this.coords.oldMid.y),this.ctx.stroke(),this.coords.old=this.coords.current,this.coords.oldMid=b}window.requestAnimationFrame&&requestAnimationFrame($.proxy(function(){this.draw()},this))},_onInputStart:function(a,b){this.coords.current=this.coords.old=b,this.coords.oldMid=this._getMidInputCoords(b),this.isDrawing=!0,window.requestAnimationFrame||this.draw(),this.ev.trigger("board:startDrawing",{e:a,coords:b}),a.stopPropagation(),a.preventDefault()},_onInputMove:function(a,b){this.coords.current=b,this.ev.trigger("board:drawing",{e:a,coords:b}),window.requestAnimationFrame||this.draw(),a.stopPropagation(),a.preventDefault()},_onInputStop:function(a,b){!this.isDrawing||a.touches&&0!==a.touches.length||(this.isDrawing=!1,this.saveWebStorage(),this.saveHistory(),this.ev.trigger("board:stopDrawing",{e:a,coords:b}),this.ev.trigger("board:userAction"),a.stopPropagation(),a.preventDefault())},_onMouseOver:function(a,b){this.isMouseHovering=!0,this.coords.old=this._getInputCoords(a),this.coords.oldMid=this._getMidInputCoords(this.coords.old),this.ev.trigger("board:mouseOver",{e:a,coords:b})},_onMouseOut:function(a,b){this.isMouseHovering=!1,this.ev.trigger("board:mouseOut",{e:a,coords:b})},_getInputCoords:function(a){a=a.originalEvent?a.originalEvent:a;var b,c,d=this.canvas.getBoundingClientRect(),e=this.dom.$canvas.width(),f=this.dom.$canvas.height();return a.touches&&1==a.touches.length?(b=a.touches[0].pageX,c=a.touches[0].pageY):(b=a.pageX,c=a.pageY),b-=this.dom.$canvas.offset().left,c-=this.dom.$canvas.offset().top,b*=e/d.width,c*=f/d.height,{x:b,y:c}},_getMidInputCoords:function(a){return{x:this.coords.old.x+a.x>>1,y:this.coords.old.y+a.y>>1}}},DrawingBoard.Control=function(a,b){return this.board=a,this.opts=$.extend({},this.defaults,b),this.$el=$(document.createElement("div")).addClass("drawing-board-control"),this.name&&this.$el.addClass("drawing-board-control-"+this.name),this.board.ev.bind("board:reset",$.proxy(this.onBoardReset,this)),this.initialize.apply(this,arguments),this},DrawingBoard.Control.prototype={name:"",defaults:{},initialize:function(){},addToBoard:function(){this.board.addControl(this)},onBoardReset:function(){}},DrawingBoard.Control.extend=function(a,b){var c,d=this;c=a&&a.hasOwnProperty("constructor")?a.constructor:function(){return d.apply(this,arguments)},$.extend(c,d,b);var e=function(){this.constructor=c};return e.prototype=d.prototype,c.prototype=new e,a&&$.extend(c.prototype,a),c.__super__=d.prototype,c},DrawingBoard.Control.Color=DrawingBoard.Control.extend({name:"colors",initialize:function(){this.initTemplate();var a=this;this.$el.on("click",".drawing-board-control-colors-picker",function(b){var c=$(this).attr("data-color");a.board.setColor(c),a.$el.find(".drawing-board-control-colors-current").css("background-color",c).attr("data-color",c),a.board.ev.trigger("color:changed",c),a.$el.find(".drawing-board-control-colors-rainbows").addClass("drawing-board-utils-hidden"),b.preventDefault()}),this.$el.on("click",".drawing-board-control-colors-current",function(b){a.$el.find(".drawing-board-control-colors-rainbows").toggleClass("drawing-board-utils-hidden"),b.preventDefault()}),$("body").on("click",function(b){var c=$(b.target),d=c.hasClass("drawing-board-control-colors-current")?c:c.closest(".drawing-board-control-colors-current"),e=a.$el.find(".drawing-board-control-colors-current"),f=a.$el.find(".drawing-board-control-colors-rainbows");d.length&&d.get(0)===e.get(0)||f.hasClass("drawing-board-utils-hidden")||f.addClass("drawing-board-utils-hidden")})},initTemplate:function(){var a='
{{rainbows}}
',b='
',c="";$.each([.75,.5,.25],$.proxy(function(a,d){var e=0,f=null;for(c+='
',.25==d&&(f=this._rgba(0,0,0,1)),.5==d&&(f=this._rgba(150,150,150,1)),.75==d&&(f=this._rgba(255,255,255,1)),c+=DrawingBoard.Utils.tpl(b,{color:f.toString()});330>=e;)c+=DrawingBoard.Utils.tpl(b,{color:this._hsl2Rgba(this._hsl(e-60,1,d)).toString()}),e+=30;c+="
"},this)),this.$el.append($(DrawingBoard.Utils.tpl(a,{color:this.board.color,rainbows:c}))),this.$el.find(".drawing-board-control-colors-rainbows").addClass("drawing-board-utils-hidden")},onBoardReset:function(){this.board.setColor(this.$el.find(".drawing-board-control-colors-current").attr("data-color"))},_rgba:function(a,b,c,d){return{r:a,g:b,b:c,a:d,toString:function(){return"rgba("+a+", "+b+", "+c+", "+d+")"}}},_hsl:function(a,b,c){return{h:a,s:b,l:c,toString:function(){return"hsl("+a+", "+100*b+"%, "+100*c+"%)"}}},_hex2Rgba:function(a){var b=parseInt(a.substring(1),16);return this._rgba(b>>16,b>>8&255,255&b,1)},_hsl2Rgba:function(a){function b(a,b,c){return 0>c&&(c+=1),c>1&&(c-=1),1/6>c?a+6*(b-a)*c:.5>c?b:2/3>c?a+(b-a)*(2/3-c)*6:a}var c,d,e,f=a.h/360,g=a.s,h=a.l;if(0===g)c=d=e=h;else{var i=.5>h?h*(1+g):h+g-h*g,j=2*h-i;c=Math.floor(255*b(j,i,f+1/3)),d=Math.floor(255*b(j,i,f)),e=Math.floor(255*b(j,i,f-1/3))}return this._rgba(c,d,e,1)}}),DrawingBoard.Control.DrawingMode=DrawingBoard.Control.extend({name:"drawingmode",defaults:{pencil:!0,eraser:!0,filler:!0},initialize:function(){this.prevMode=this.board.getMode(),$.each(["pencil","eraser","filler"],$.proxy(function(a,b){this.opts[b]&&this.$el.append('')},this)),this.$el.on("click","button[data-mode]",$.proxy(function(a){var b=$(a.currentTarget).attr("data-mode"),c=this.board.getMode();c!==b&&(this.prevMode=c);var d=c===b?this.prevMode:b;this.board.setMode(d),a.preventDefault()},this)),this.board.ev.bind("board:mode",$.proxy(function(a){this.toggleButtons(a)},this)),this.toggleButtons(this.board.getMode())},toggleButtons:function(a){this.$el.find("button[data-mode]").each(function(b,c){var d=$(c);d.toggleClass("active",a===d.attr("data-mode"))})}}),DrawingBoard.Control.Navigation=DrawingBoard.Control.extend({name:"navigation",defaults:{back:!0,forward:!0,reset:!0},initialize:function(){var a="";if(this.opts.back&&(a+=''),this.opts.forward&&(a+=''),this.opts.reset&&(a+=''),this.$el.append(a),this.opts.back){var b=this.$el.find(".drawing-board-control-navigation-back");this.board.ev.bind("historyNavigation",$.proxy(this.updateBack,this,b)),this.$el.on("click",".drawing-board-control-navigation-back",$.proxy(function(a){this.board.goBackInHistory(),a.preventDefault()},this)),this.updateBack(b)}if(this.opts.forward){var c=this.$el.find(".drawing-board-control-navigation-forward");this.board.ev.bind("historyNavigation",$.proxy(this.updateForward,this,c)),this.$el.on("click",".drawing-board-control-navigation-forward",$.proxy(function(a){this.board.goForthInHistory(),a.preventDefault()},this)),this.updateForward(c)}this.opts.reset&&this.$el.on("click",".drawing-board-control-navigation-reset",$.proxy(function(a){this.board.reset({background:!0}),a.preventDefault()},this))},updateBack:function(a){this.board.history.canUndo()?a.removeAttr("disabled"):a.attr("disabled","disabled")},updateForward:function(a){this.board.history.canRedo()?a.removeAttr("disabled"):a.attr("disabled","disabled")}}),DrawingBoard.Control.Size=DrawingBoard.Control.extend({name:"size",defaults:{type:"auto",dropdownValues:[1,3,6,10,20,30,40,50],min:1,max:50},types:["dropdown","range"],initialize:function(){"auto"==this.opts.type&&(this.opts.type=this._iHasRangeInput()?"range":"dropdown");var a=$.inArray(this.opts.type,this.types)>-1?this["_"+this.opts.type+"Template"]():!1;if(!a)return!1;this.val=this.board.opts.size,this.$el.append($(a)),this.$el.attr("data-drawing-board-type",this.opts.type),this.updateView();var b=this;"range"==this.opts.type&&this.$el.on("change",".drawing-board-control-size-range-input",function(a){b.val=$(this).val(),b.updateView(),b.board.ev.trigger("size:changed",b.val),a.preventDefault()}),"dropdown"==this.opts.type&&(this.$el.on("click",".drawing-board-control-size-dropdown-current",$.proxy(function(){this.$el.find(".drawing-board-control-size-dropdown").toggleClass("drawing-board-utils-hidden")},this)),this.$el.on("click","[data-size]",function(a){b.val=parseInt($(this).attr("data-size"),0),b.updateView(),b.board.ev.trigger("size:changed",b.val),a.preventDefault()}))},_rangeTemplate:function(){var a='
';return DrawingBoard.Utils.tpl(a,{min:this.opts.min,max:this.opts.max,size:this.board.opts.size})},_dropdownTemplate:function(){var a='
    ';return $.each(this.opts.dropdownValues,function(b,c){a+=DrawingBoard.Utils.tpl('
  • ',{size:c})}),a+="
"},onBoardReset:function(){this.updateView()},updateView:function(){var a=this.val;if(this.board.ctx.lineWidth=a,this.$el.find(".drawing-board-control-size-range-current, .drawing-board-control-size-dropdown-current span").css({width:a+"px",height:a+"px",borderRadius:a+"px",marginLeft:-1*a/2+"px",marginTop:-1*a/2+"px"}),this.$el.find(".drawing-board-control-inner").attr("title",a),"dropdown"==this.opts.type){var b=null;$.each(this.opts.dropdownValues,function(c,d){(null===b||Math.abs(d-a)'),this.$el.on("click",".drawing-board-control-download-button",$.proxy(function(a){this.board.downloadImg(),a.preventDefault()},this))}}); \ No newline at end of file diff --git a/ui/media/favicon-16x16.png b/ui/media/favicon-16x16.png new file mode 100644 index 00000000..7a33c0b1 Binary files /dev/null and b/ui/media/favicon-16x16.png differ diff --git a/ui/media/favicon-32x32.png b/ui/media/favicon-32x32.png new file mode 100644 index 00000000..3b21168b Binary files /dev/null and b/ui/media/favicon-32x32.png differ diff --git a/ui/media/jquery-3.6.1.min.js b/ui/media/jquery-3.6.1.min.js new file mode 100644 index 00000000..2c69bc90 --- /dev/null +++ b/ui/media/jquery-3.6.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),v={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0= 4: + old_eps.pop(0) +- if callback: callback(i) +- if img_callback: img_callback(pred_x0, i) ++ if callback: yield from callback(i) ++ if img_callback: yield from img_callback(pred_x0, i) + +- return img ++ yield from img_callback(img, len(iterator)-1) + + @torch.no_grad() + def p_sample_plms(self, x, c, t, index, repeat_noise=False, use_original_steps=False, quantize_denoised=False, +@@ -706,7 +715,8 @@ class UNet(DDPM): @torch.no_grad() def ddim_sampling(self, x_latent, cond, t_start, unconditional_guidance_scale=1.0, unconditional_conditioning=None, @@ -22,13 +100,233 @@ index dcf7901..1f99adc 100644 timesteps = self.ddim_timesteps timesteps = timesteps[:t_start] -@@ -710,6 +712,9 @@ class UNet(DDPM): - x_dec = self.p_sample_ddim(x_dec, cond, ts, index=index, use_original_steps=use_original_steps, +@@ -730,10 +740,13 @@ class UNet(DDPM): unconditional_guidance_scale=unconditional_guidance_scale, unconditional_conditioning=unconditional_conditioning) -+ -+ if callback: callback(i) -+ if img_callback: img_callback(x_dec, i) ++ if callback: yield from callback(i) ++ if img_callback: yield from img_callback(x_dec, i) ++ if mask is not None: - return x0 * mask + (1. - mask) * x_dec +- return x0 * mask + (1. - mask) * x_dec ++ x_dec = x0 * mask + (1. - mask) * x_dec + +- return x_dec ++ yield from img_callback(x_dec, len(iterator)-1) + + + @torch.no_grad() +@@ -779,13 +792,16 @@ class UNet(DDPM): + + + @torch.no_grad() +- def euler_sampling(self, ac, x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None,callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.): ++ def euler_sampling(self, ac, x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None,callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1., ++ img_callback=None): + """Implements Algorithm 2 (Euler steps) from Karras et al. (2022).""" + extra_args = {} if extra_args is None else extra_args + cvd = CompVisDenoiser(ac) + sigmas = cvd.get_sigmas(S) + x = x*sigmas[0] + ++ print(f"Running Euler Sampling with {len(sigmas) - 1} timesteps") ++ + s_in = x.new_ones([x.shape[0]]).half() + for i in trange(len(sigmas) - 1, disable=disable): + gamma = min(s_churn / (len(sigmas) - 1), 2 ** 0.5 - 1) if s_tmin <= sigmas[i] <= s_tmax else 0. +@@ -807,13 +823,18 @@ class UNet(DDPM): + d = to_d(x, sigma_hat, denoised) + if callback is not None: + callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigma_hat, 'denoised': denoised}) ++ ++ if img_callback: yield from img_callback(x, i) ++ + dt = sigmas[i + 1] - sigma_hat + # Euler method + x = x + d * dt +- return x ++ ++ yield from img_callback(x, len(sigmas)-1) + + @torch.no_grad() +- def euler_ancestral_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None, callback=None, disable=None): ++ def euler_ancestral_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None, callback=None, disable=None, ++ img_callback=None): + """Ancestral sampling with Euler method steps.""" + extra_args = {} if extra_args is None else extra_args + +@@ -822,6 +843,8 @@ class UNet(DDPM): + sigmas = cvd.get_sigmas(S) + x = x*sigmas[0] + ++ print(f"Running Euler Ancestral Sampling with {len(sigmas) - 1} timesteps") ++ + s_in = x.new_ones([x.shape[0]]).half() + for i in trange(len(sigmas) - 1, disable=disable): + +@@ -837,17 +860,22 @@ class UNet(DDPM): + sigma_down, sigma_up = get_ancestral_step(sigmas[i], sigmas[i + 1]) + if callback is not None: + callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised}) ++ ++ if img_callback: yield from img_callback(x, i) ++ + d = to_d(x, sigmas[i], denoised) + # Euler method + dt = sigma_down - sigmas[i] + x = x + d * dt + x = x + torch.randn_like(x) * sigma_up +- return x ++ ++ yield from img_callback(x, len(sigmas)-1) + + + + @torch.no_grad() +- def heun_sampling(self, ac, x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.): ++ def heun_sampling(self, ac, x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1., ++ img_callback=None): + """Implements Algorithm 2 (Heun steps) from Karras et al. (2022).""" + extra_args = {} if extra_args is None else extra_args + +@@ -855,6 +883,8 @@ class UNet(DDPM): + sigmas = cvd.get_sigmas(S) + x = x*sigmas[0] + ++ print(f"Running Heun Sampling with {len(sigmas) - 1} timesteps") ++ + + s_in = x.new_ones([x.shape[0]]).half() + for i in trange(len(sigmas) - 1, disable=disable): +@@ -876,6 +906,9 @@ class UNet(DDPM): + d = to_d(x, sigma_hat, denoised) + if callback is not None: + callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigma_hat, 'denoised': denoised}) ++ ++ if img_callback: yield from img_callback(x, i) ++ + dt = sigmas[i + 1] - sigma_hat + if sigmas[i + 1] == 0: + # Euler method +@@ -895,11 +928,13 @@ class UNet(DDPM): + d_2 = to_d(x_2, sigmas[i + 1], denoised_2) + d_prime = (d + d_2) / 2 + x = x + d_prime * dt +- return x ++ ++ yield from img_callback(x, len(sigmas)-1) + + + @torch.no_grad() +- def dpm_2_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.): ++ def dpm_2_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1., ++ img_callback=None): + """A sampler inspired by DPM-Solver-2 and Algorithm 2 from Karras et al. (2022).""" + extra_args = {} if extra_args is None else extra_args + +@@ -907,6 +942,8 @@ class UNet(DDPM): + sigmas = cvd.get_sigmas(S) + x = x*sigmas[0] + ++ print(f"Running DPM2 Sampling with {len(sigmas) - 1} timesteps") ++ + s_in = x.new_ones([x.shape[0]]).half() + for i in trange(len(sigmas) - 1, disable=disable): + gamma = min(s_churn / (len(sigmas) - 1), 2 ** 0.5 - 1) if s_tmin <= sigmas[i] <= s_tmax else 0. +@@ -924,7 +961,7 @@ class UNet(DDPM): + e_t_uncond, e_t = (x_in + eps * c_out).chunk(2) + denoised = e_t_uncond + unconditional_guidance_scale * (e_t - e_t_uncond) + +- ++ if img_callback: yield from img_callback(x, i) + + d = to_d(x, sigma_hat, denoised) + # Midpoint method, where the midpoint is chosen according to a rho=3 Karras schedule +@@ -945,11 +982,13 @@ class UNet(DDPM): + + d_2 = to_d(x_2, sigma_mid, denoised_2) + x = x + d_2 * dt_2 +- return x ++ ++ yield from img_callback(x, len(sigmas)-1) + + + @torch.no_grad() +- def dpm_2_ancestral_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None): ++ def dpm_2_ancestral_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None, ++ img_callback=None): + """Ancestral sampling with DPM-Solver inspired second-order steps.""" + extra_args = {} if extra_args is None else extra_args + +@@ -957,6 +996,8 @@ class UNet(DDPM): + sigmas = cvd.get_sigmas(S) + x = x*sigmas[0] + ++ print(f"Running DPM2 Ancestral Sampling with {len(sigmas) - 1} timesteps") ++ + s_in = x.new_ones([x.shape[0]]).half() + for i in trange(len(sigmas) - 1, disable=disable): + +@@ -973,6 +1014,9 @@ class UNet(DDPM): + sigma_down, sigma_up = get_ancestral_step(sigmas[i], sigmas[i + 1]) + if callback is not None: + callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised}) ++ ++ if img_callback: yield from img_callback(x, i) ++ + d = to_d(x, sigmas[i], denoised) + # Midpoint method, where the midpoint is chosen according to a rho=3 Karras schedule + sigma_mid = ((sigmas[i] ** (1 / 3) + sigma_down ** (1 / 3)) / 2) ** 3 +@@ -993,11 +1037,13 @@ class UNet(DDPM): + d_2 = to_d(x_2, sigma_mid, denoised_2) + x = x + d_2 * dt_2 + x = x + torch.randn_like(x) * sigma_up +- return x ++ ++ yield from img_callback(x, len(sigmas)-1) + + + @torch.no_grad() +- def lms_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None, order=4): ++ def lms_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None, order=4, ++ img_callback=None): + extra_args = {} if extra_args is None else extra_args + s_in = x.new_ones([x.shape[0]]) + +@@ -1005,6 +1051,8 @@ class UNet(DDPM): + sigmas = cvd.get_sigmas(S) + x = x*sigmas[0] + ++ print(f"Running LMS Sampling with {len(sigmas) - 1} timesteps") ++ + ds = [] + for i in trange(len(sigmas) - 1, disable=disable): + +@@ -1017,6 +1065,7 @@ class UNet(DDPM): + e_t_uncond, e_t = (x_in + eps * c_out).chunk(2) + denoised = e_t_uncond + unconditional_guidance_scale * (e_t - e_t_uncond) + ++ if img_callback: yield from img_callback(x, i) + + d = to_d(x, sigmas[i], denoised) + ds.append(d) +@@ -1027,4 +1076,5 @@ class UNet(DDPM): + cur_order = min(i + 1, order) + coeffs = [linear_multistep_coeff(cur_order, sigmas.cpu(), i, j) for j in range(cur_order)] + x = x + sum(coeff * d for coeff, d in zip(coeffs, reversed(ds))) +- return x ++ ++ yield from img_callback(x, len(sigmas)-1) +diff --git a/optimizedSD/openaimodelSplit.py b/optimizedSD/openaimodelSplit.py +index abc3098..7a32ffe 100644 +--- a/optimizedSD/openaimodelSplit.py ++++ b/optimizedSD/openaimodelSplit.py +@@ -13,7 +13,7 @@ from ldm.modules.diffusionmodules.util import ( + normalization, + timestep_embedding, + ) +-from splitAttention import SpatialTransformer ++from .splitAttention import SpatialTransformer + + + class AttentionPool2d(nn.Module): diff --git a/ui/sd_internal/runtime.py b/ui/sd_internal/runtime.py index 0fe21ee6..f03a0d34 100644 --- a/ui/sd_internal/runtime.py +++ b/ui/sd_internal/runtime.py @@ -1,9 +1,10 @@ +import json import os, re import traceback import torch import numpy as np from omegaconf import OmegaConf -from PIL import Image +from PIL import Image, ImageOps from tqdm import tqdm, trange from itertools import islice from einops import rearrange @@ -32,10 +33,11 @@ filename_regex = re.compile('[^a-zA-Z0-9]') from . import Request, Response, Image as ResponseImage import base64 from io import BytesIO +#from colorama import Fore # local -session_id = str(uuid.uuid4())[-8:] stop_processing = False +temp_images = {} ckpt_file = None gfpgan_file = None @@ -184,23 +186,47 @@ def load_model_real_esrgan(real_esrgan_to_use): print('loaded ', real_esrgan_to_use, 'to', device, 'precision', precision) def mk_img(req: Request): - global modelFS, device + try: + yield from do_mk_img(req) + except Exception as e: + print(traceback.format_exc()) + + gc() + + if device != "cpu": + modelFS.to("cpu") + modelCS.to("cpu") + + model.model1.to("cpu") + model.model2.to("cpu") + + gc() + + yield json.dumps({ + "status": 'failed', + "detail": str(e) + }) + +def do_mk_img(req: Request): + global model, modelCS, modelFS, device global model_gfpgan, model_real_esrgan global stop_processing stop_processing = False res = Response() - res.session_id = session_id res.request = req res.images = [] + temp_images.clear() + model.turbo = req.turbo if req.use_cpu: if device != 'cpu': device = 'cpu' if model_is_half: + del model, modelCS, modelFS load_model_ckpt(ckpt_file, device) load_model_gfpgan(gfpgan_file) @@ -215,7 +241,8 @@ def mk_img(req: Request): (req.init_image is None and model_fs_is_half) or \ (req.init_image is not None and not model_fs_is_half and not force_full_precision): - load_model_ckpt(ckpt_file, device, model.turbo, unet_bs, ('full' if req.use_full_precision else 'autocast'), half_model_fs=(req.init_image is not None and not req.use_full_precision)) + del model, modelCS, modelFS + load_model_ckpt(ckpt_file, device, req.turbo, unet_bs, ('full' if req.use_full_precision else 'autocast'), half_model_fs=(req.init_image is not None and not req.use_full_precision)) if prev_device != device: load_model_gfpgan(gfpgan_file) @@ -248,6 +275,7 @@ def mk_img(req: Request): opt_use_upscale = req.use_upscale opt_show_only_filtered = req.show_only_filtered_image opt_format = 'png' + opt_sampler_name = req.sampler print(req.to_string(), '\n device', device) @@ -265,6 +293,8 @@ def mk_img(req: Request): else: precision_scope = nullcontext + mask = None + if req.init_image is None: handler = _txt2img @@ -284,18 +314,22 @@ def mk_img(req: Request): init_image = repeat(init_image, '1 ... -> b ...', b=batch_size) init_latent = modelFS.get_first_stage_encoding(modelFS.encode_first_stage(init_image)) # move to latent space - if device != "cpu": - mem = torch.cuda.memory_allocated() / 1e6 - modelFS.to("cpu") - while torch.cuda.memory_allocated() / 1e6 >= mem: - time.sleep(1) + if req.mask is not None: + mask = load_mask(req.mask, opt_W, opt_H, init_latent.shape[2], init_latent.shape[3], True).to(device) + mask = mask[0][0].unsqueeze(0).repeat(4, 1, 1).unsqueeze(0) + mask = repeat(mask, '1 ... -> b ...', b=batch_size) + + if device != "cpu" and precision == "autocast": + mask = mask.half() + + move_fs_to_cpu() assert 0. <= opt_strength <= 1., 'can only work with strength in [0.0, 1.0]' t_enc = int(opt_strength * opt_ddim_steps) print(f"target t_enc is {t_enc} steps") if opt_save_to_disk_path is not None: - session_out_path = os.path.join(opt_save_to_disk_path, session_id) + session_out_path = os.path.join(opt_save_to_disk_path, req.session_id) os.makedirs(session_out_path, exist_ok=True) else: session_out_path = None @@ -326,29 +360,60 @@ def mk_img(req: Request): else: c = modelCS.get_learned_conditioning(prompts) + modelFS.to(device) + partial_x_samples = None def img_callback(x_samples, i): nonlocal partial_x_samples partial_x_samples = x_samples + if req.stream_progress_updates: + n_steps = opt_ddim_steps if req.init_image is None else t_enc + progress = {"step": i, "total_steps": n_steps} + + if req.stream_image_progress and i % 5 == 0: + partial_images = [] + + for i in range(batch_size): + x_samples_ddim = modelFS.decode_first_stage(x_samples[i].unsqueeze(0)) + x_sample = torch.clamp((x_samples_ddim + 1.0) / 2.0, min=0.0, max=1.0) + x_sample = 255.0 * rearrange(x_sample[0].cpu().numpy(), "c h w -> h w c") + x_sample = x_sample.astype(np.uint8) + img = Image.fromarray(x_sample) + buf = BytesIO() + img.save(buf, format='JPEG') + buf.seek(0) + + del img, x_sample, x_samples_ddim + # don't delete x_samples, it is used in the code that called this callback + + temp_images[str(req.session_id) + '/' + str(i)] = buf + partial_images.append({'path': f'/image/tmp/{req.session_id}/{i}'}) + + progress['output'] = partial_images + + yield json.dumps(progress) + if stop_processing: raise UserInitiatedStop("User requested that we stop processing") # run the handler try: if handler == _txt2img: - x_samples = _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, None, opt_C, opt_f, opt_ddim_eta, c, uc, opt_seed, img_callback) + x_samples = _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, None, opt_C, opt_f, opt_ddim_eta, c, uc, opt_seed, img_callback, mask, opt_sampler_name) else: - x_samples = _img2img(init_latent, t_enc, batch_size, opt_scale, c, uc, opt_ddim_steps, opt_ddim_eta, opt_seed, img_callback) + x_samples = _img2img(init_latent, t_enc, batch_size, opt_scale, c, uc, opt_ddim_steps, opt_ddim_eta, opt_seed, img_callback, mask) + + yield from x_samples + + x_samples = partial_x_samples except UserInitiatedStop: if partial_x_samples is None: continue x_samples = partial_x_samples - modelFS.to(device) - print("saving images") for i in range(batch_size): @@ -358,6 +423,14 @@ def mk_img(req: Request): x_sample = x_sample.astype(np.uint8) img = Image.fromarray(x_sample) + has_filters = (opt_use_face_correction is not None and opt_use_face_correction.startswith('GFPGAN')) or \ + (opt_use_upscale is not None and opt_use_upscale.startswith('RealESRGAN')) + + return_orig_img = not has_filters or not opt_show_only_filtered + + if stop_processing: + return_orig_img = True + if opt_save_to_disk_path is not None: prompt_flattened = filename_regex.sub('_', prompts[0]) prompt_flattened = prompt_flattened[:50] @@ -368,12 +441,12 @@ def mk_img(req: Request): img_out_path = os.path.join(session_out_path, f"{file_path}.{opt_format}") meta_out_path = os.path.join(session_out_path, f"{file_path}.txt") - if not opt_show_only_filtered: + if return_orig_img: save_image(img, img_out_path) - save_metadata(meta_out_path, prompts, opt_seed, opt_W, opt_H, opt_ddim_steps, opt_scale, opt_strength, opt_use_face_correction, opt_use_upscale) + save_metadata(meta_out_path, prompts, opt_seed, opt_W, opt_H, opt_ddim_steps, opt_scale, opt_strength, opt_use_face_correction, opt_use_upscale, opt_sampler_name) - if not opt_show_only_filtered: + if return_orig_img: img_data = img_to_base64_str(img) res_image_orig = ResponseImage(data=img_data, seed=opt_seed) res.images.append(res_image_orig) @@ -381,8 +454,10 @@ def mk_img(req: Request): if opt_save_to_disk_path is not None: res_image_orig.path_abs = img_out_path - if (opt_use_face_correction is not None and opt_use_face_correction.startswith('GFPGAN')) or \ - (opt_use_upscale is not None and opt_use_upscale.startswith('RealESRGAN')): + del img + + if has_filters and not stop_processing: + print('Applying filters..') gc() filters_applied = [] @@ -410,18 +485,19 @@ def mk_img(req: Request): save_image(filtered_image, filtered_img_out_path) res_image_filtered.path_abs = filtered_img_out_path + del filtered_image + seeds += str(opt_seed) + "," opt_seed += 1 - if device != "cpu": - mem = torch.cuda.memory_allocated() / 1e6 - modelFS.to("cpu") - while torch.cuda.memory_allocated() / 1e6 >= mem: - time.sleep(1) - del x_samples + move_fs_to_cpu() + gc() + del x_samples, x_samples_ddim, x_sample print("memory_final = ", torch.cuda.memory_allocated() / 1e6) - return res + print('Task completed') + + yield json.dumps(res.json()) def save_image(img, img_out_path): try: @@ -429,8 +505,8 @@ def save_image(img, img_out_path): except: print('could not save the file', traceback.format_exc()) -def save_metadata(meta_out_path, prompts, opt_seed, opt_W, opt_H, opt_ddim_steps, opt_scale, opt_prompt_strength, opt_correct_face, opt_upscale): - metadata = f"{prompts[0]}\nWidth: {opt_W}\nHeight: {opt_H}\nSeed: {opt_seed}\nSteps: {opt_ddim_steps}\nGuidance Scale: {opt_scale}\nPrompt Strength: {opt_prompt_strength}\nUse Face Correction: {opt_correct_face}\nUse Upscaling: {opt_upscale}" +def save_metadata(meta_out_path, prompts, opt_seed, opt_W, opt_H, opt_ddim_steps, opt_scale, opt_prompt_strength, opt_correct_face, opt_upscale, sampler_name): + metadata = f"{prompts[0]}\nWidth: {opt_W}\nHeight: {opt_H}\nSeed: {opt_seed}\nSteps: {opt_ddim_steps}\nGuidance Scale: {opt_scale}\nPrompt Strength: {opt_prompt_strength}\nUse Face Correction: {opt_correct_face}\nUse Upscaling: {opt_upscale}\nSampler: {sampler_name}" try: with open(meta_out_path, 'w') as f: @@ -438,7 +514,7 @@ def save_metadata(meta_out_path, prompts, opt_seed, opt_W, opt_H, opt_ddim_steps except: print('could not save the file', traceback.format_exc()) -def _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, start_code, opt_C, opt_f, opt_ddim_eta, c, uc, opt_seed, img_callback): +def _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, start_code, opt_C, opt_f, opt_ddim_eta, c, uc, opt_seed, img_callback, mask, sampler_name): shape = [opt_n_samples, opt_C, opt_H // opt_f, opt_W // opt_f] if device != "cpu": @@ -458,12 +534,13 @@ def _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, start_code, eta=opt_ddim_eta, x_T=start_code, img_callback=img_callback, - sampler = 'plms', + mask=mask, + sampler = sampler_name, ) - return samples_ddim + yield from samples_ddim -def _img2img(init_latent, t_enc, batch_size, opt_scale, c, uc, opt_ddim_steps, opt_ddim_eta, opt_seed, img_callback): +def _img2img(init_latent, t_enc, batch_size, opt_scale, c, uc, opt_ddim_steps, opt_ddim_eta, opt_seed, img_callback, mask): # encode (scaled latent) z_enc = model.stochastic_encode( init_latent, @@ -472,6 +549,8 @@ def _img2img(init_latent, t_enc, batch_size, opt_scale, c, uc, opt_ddim_steps, o opt_ddim_eta, opt_ddim_steps, ) + x_T = None if mask is None else init_latent + # decode it samples_ddim = model.sample( t_enc, @@ -480,10 +559,19 @@ def _img2img(init_latent, t_enc, batch_size, opt_scale, c, uc, opt_ddim_steps, o unconditional_guidance_scale=opt_scale, unconditional_conditioning=uc, img_callback=img_callback, + mask=mask, + x_T=x_T, sampler = 'ddim' ) - return samples_ddim + yield from samples_ddim + +def move_fs_to_cpu(): + if device != "cpu": + mem = torch.cuda.memory_allocated() / 1e6 + modelFS.to("cpu") + while torch.cuda.memory_allocated() / 1e6 >= mem: + time.sleep(1) def gc(): if device == 'cpu': @@ -525,6 +613,31 @@ def load_img(img_str, w0, h0): image = torch.from_numpy(image) return 2.*image - 1. +def load_mask(mask_str, h0, w0, newH, newW, invert=False): + image = base64_str_to_img(mask_str).convert("RGB") + w, h = image.size + print(f"loaded input mask of size ({w}, {h})") + + if invert: + print("inverted") + image = ImageOps.invert(image) + # where_0, where_1 = np.where(image == 0), np.where(image == 255) + # image[where_0], image[where_1] = 255, 0 + + if h0 is not None and w0 is not None: + h, w = h0, w0 + + w, h = map(lambda x: x - x % 64, (w, h)) # resize to integer multiple of 64 + + print(f"New mask size ({w}, {h})") + image = image.resize((newW, newH), resample=Image.Resampling.LANCZOS) + image = np.array(image) + + image = image.astype(np.float32) / 255.0 + image = image[None].transpose(0, 3, 1, 2) + image = torch.from_numpy(image) + return image + # https://stackoverflow.com/a/61114178 def img_to_base64_str(img): buffered = BytesIO() diff --git a/ui/server.py b/ui/server.py index f55c1091..ad3b1ae0 100644 --- a/ui/server.py +++ b/ui/server.py @@ -15,7 +15,8 @@ CONFIG_DIR = os.path.join(SD_UI_DIR, '..', 'scripts') OUTPUT_DIRNAME = "Stable Diffusion UI" # in the user's home folder from fastapi import FastAPI, HTTPException -from starlette.responses import FileResponse +from fastapi.staticfiles import StaticFiles +from starlette.responses import FileResponse, StreamingResponse from pydantic import BaseModel import logging @@ -31,6 +32,7 @@ outpath = os.path.join(os.path.expanduser("~"), OUTPUT_DIRNAME) # defaults from https://huggingface.co/blog/stable_diffusion class ImageRequest(BaseModel): + session_id: str = "session" prompt: str = "" init_image: str = None # base64 mask: str = None # base64 @@ -41,6 +43,7 @@ class ImageRequest(BaseModel): height: int = 512 seed: int = 42 prompt_strength: float = 0.8 + sampler: str = None # "ddim", "plms", "heun", "euler", "euler_a", "dpm2", "dpm2_a", "lms" # allow_nsfw: bool = False save_to_disk_path: str = None turbo: bool = True @@ -50,9 +53,14 @@ class ImageRequest(BaseModel): use_upscale: str = None # or "RealESRGAN_x4plus" or "RealESRGAN_x4plus_anime_6B" show_only_filtered_image: bool = False + stream_progress_updates: bool = False + stream_image_progress: bool = False + class SetAppConfigRequest(BaseModel): update_branch: str = "main" +app.mount('/media', StaticFiles(directory=os.path.join(SD_UI_DIR, 'media/')), name="media") + @app.get('/') def read_root(): headers = {"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0"} @@ -87,6 +95,7 @@ def image(req : ImageRequest): from sd_internal import runtime r = Request() + r.session_id = req.session_id r.prompt = req.prompt r.init_image = req.init_image r.mask = req.mask @@ -97,6 +106,7 @@ def image(req : ImageRequest): r.height = req.height r.seed = req.seed r.prompt_strength = req.prompt_strength + r.sampler = req.sampler # r.allow_nsfw = req.allow_nsfw r.turbo = req.turbo r.use_cpu = req.use_cpu @@ -106,10 +116,24 @@ def image(req : ImageRequest): r.use_face_correction = req.use_face_correction r.show_only_filtered_image = req.show_only_filtered_image - try: - res: Response = runtime.mk_img(r) + r.stream_progress_updates = True # the underlying implementation only supports streaming + r.stream_image_progress = req.stream_image_progress - return res.json() + try: + if not req.stream_progress_updates: + r.stream_image_progress = False + + res = runtime.mk_img(r) + + if req.stream_progress_updates: + return StreamingResponse(res, media_type='application/json') + else: # compatibility mode: buffer the streaming responses, and return the last one + last_result = None + + for result in res: + last_result = result + + return json.loads(last_result) except Exception as e: print(traceback.format_exc()) return HTTPException(status_code=500, detail=str(e)) @@ -128,6 +152,13 @@ def stop(): print(traceback.format_exc()) return HTTPException(status_code=500, detail=str(e)) +@app.get('/image/tmp/{session_id}/{img_id}') +def get_image(session_id, img_id): + from sd_internal import runtime + buf = runtime.temp_images[session_id + '/' + img_id] + buf.seek(0) + return StreamingResponse(buf, media_type='image/jpeg') + @app.post('/app_config') async def setAppConfig(req : SetAppConfigRequest): try: @@ -173,14 +204,6 @@ def getAppConfig(): print(traceback.format_exc()) return HTTPException(status_code=500, detail=str(e)) -@app.get('/media/ding.mp3') -def read_ding(): - return FileResponse(os.path.join(SD_UI_DIR, 'media/ding.mp3')) - -@app.get('/media/kofi.png') -def read_modifiers(): - return FileResponse(os.path.join(SD_UI_DIR, 'media/kofi.png')) - @app.get('/modifiers.json') def read_modifiers(): return FileResponse(os.path.join(SD_UI_DIR, 'modifiers.json'))