Full Code of devforth/painterro for AI

master ce1d44b9dfc0 cached
57 files
265.0 KB
74.3k tokens
249 symbols
1 requests
Download .txt
Showing preview only (284K chars total). Download the full file or copy to clipboard to get everything.
Repository: devforth/painterro
Branch: master
Commit: ce1d44b9dfc0
Files: 57
Total size: 265.0 KB

Directory structure:
gitextract_hkg7u874/

├── .browserslistrc
├── .github/
│   ├── FUNDING.yml
│   └── ISSUE_TEMPLATE/
│       ├── bug_report.md
│       └── feature_request.md
├── .gitignore
├── .npmignore
├── CONTRIBUTORS.md
├── LICENSE
├── README.md
├── Release.md
├── build/
│   ├── contained.html
│   └── index.html
├── css/
│   ├── bar-styles.css
│   ├── icons/
│   │   ├── ptroiconfont.css
│   │   └── ptroiconfont.html
│   └── styles.css
├── example/
│   ├── server.py
│   └── templates/
│       ├── common.html
│       ├── images_list.html
│       ├── paste_as_base64.html
│       ├── paste_as_bin.html
│       └── paste_to_tinymce.html
├── generate_font.js
├── js/
│   ├── colorPicker.js
│   ├── controlbuilder.js
│   ├── customEvents.js
│   ├── filters.js
│   ├── inserter.js
│   ├── main.js
│   ├── paintBucket.js
│   ├── params.js
│   ├── primitive.js
│   ├── resizer.js
│   ├── selecter.js
│   ├── settings.js
│   ├── text.js
│   ├── translation.js
│   ├── utils.js
│   ├── worklog.js
│   └── zoomHelper.js
├── langs/
│   ├── ca.lang.js
│   ├── de.lang.js
│   ├── en.lang.js
│   ├── es.lang.js
│   ├── fa.lang.js
│   ├── fr.lang.js
│   ├── ja.lang.js
│   ├── nl.lang.js
│   ├── pl.lang.js
│   ├── pt-BR.lang.js
│   ├── pt-PT.lang.js
│   ├── ru.lang.js
│   └── uk.lang.js
├── package.json
├── publish.sh
├── res/
│   └── font/
│       └── font-css.hbs
└── webpack.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .browserslistrc
================================================
defaults

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: # [devforth]
patreon: devforth
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**Painterro version**
E.g. v1.0.2

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots (Don't be lazy, PrnScr, Ctrl+V)

**Browser**
 - e.g.  Google Chrome 11 on Windows 


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Create a feature which makes Painterro better
title: ''
labels: ''
assignees: ''

---

**Describe the feature**
A clear and concise description of what you want to see and how it should work. Step by step, then we will adjust if needed

**Possible analogs?**
Gimp, MSPaint, Inkscape, etc...


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

.idea

# custom 
build/painterro*
build/report*

wp/

================================================
FILE: .npmignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

.idea

# custom
build/painterro-*

wp/*
docs/*



================================================
FILE: CONTRIBUTORS.md
================================================


Painterro contributors
============================================

* **[Jesfery](https://github.com/Jesfery)**

  * defaultTool param
  
* **[Ivan Borshchov](https://github.com/ivictbor)**  
  * maintainer

================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2017 Ivan Borshchov

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

================================================
FILE: README.md
================================================
<img src="https://raw.githubusercontent.com/devforth/painterro/master/res/painterro.png" align="right" style="padding:5px; width:70px" /> 

**[live demo](https://tracklify.com/painterro_demo/)** | [npm](https://www.npmjs.com/package/painterro) | [GitHub](https://github.com/devforth/painterro)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  ![npmvers](https://img.shields.io/npm/v/painterro) ![npmDown](https://img.shields.io/npm/dw/painterro?label=npm%20💾) ![totalNpm](https://img.shields.io/npm/dt/painterro?label=npm%20💾) ![ghdownloads](https://img.shields.io/github/downloads/devforth/painterro/total?label=github%20script%20💾) 
[![Ask AI](http://tluma.ai/badge)](http://tluma.ai/ask-ai/devforth/painterro)

JavaScript painting plugin which allows editing images in a browser.
Can be easily integrated into any website or webapp by calling simple initialization code.

🆕 Check out our simple and game-changing opensource [Vue admin framework](https://adminforth.dev/) with a great look and extensibility!


> 🙏🙏🙏 Scream for help to Ukraine 
> 
> 24 February 2022, Russia started bombing cities with peacefully civilized population in whole Ukraine. And has been doing it up to this day. Breaking all laws of war. Their bombs has been killing children and adults. This deserves Hague court.
> - 🏠 If you are from Russia, please stop your government by any means including protests, don't trust local media, they are bribed by the government. They always was. I am sure you already feel lie by unexplainable crazy things in your country caused by world sanctions.
> - 💣 Please spread the information about bombing of Ukraine in all social way you could. Russia treacherously broke into the territory of a sovereign state. Do not trust to anything from Russian media, most likely it will be bullshit
> - 💼 If you have any kind of business cooperation with Russia, please block it now and keep most of money on your side, this is the only possible ethical decision
> - ☢️ Ask your government to stop Russia from spreading invasion in any way. Russia is nuclear threat to the whole world. You think it is not possible? We thought that bombing of independent country with population of 44.13 million is also not possible.


Features
========

- Paste image from clipboard with `Ctrl+V` (e.g. `PtnScr` screenshot), drag and drop it into widget, or load with file select dialog
- Crop image by defined area
- Paint primitives: line, rectangle, ellipse (alpha color can be used)
- Brush – free drawing tool e.g. to implement finger-based signatures on tablet screens
- Add text (you can use `Ctrl+B` - bold, `Ctrl+I` - italic, `Ctrl+U` - underlined, or just pase formatted HTML)
- Rotate / resize, scale image
- Pixelize some area to hide sensitive data
- Draw arrows
- Trash can tool to clear the canvas
- Paint bucket tool for color fills
- Add filters to images

<img alt="Painterro gif preview" src="https://raw.githubusercontent.com/devforth/painterro/master/docs/painterro_gif.gif" 
 style="box-shadow: 0 0 20px lightgrey; margin: 0 0 20px 0;" /> 

Used by
=================

  <table border="0">
  <tr>
    <td align="center">
      <br>
      <a href="https://nasa.github.io/openmct/"><img src="https://nasa.github.io/openmct/static/res/images/logo-nasa.svg" height='100px'/></a>
      <br>
      <a href="https://nasa.github.io/openmct/">NASA Open MCT</a>
      <br>
      <br>
    </td>
    <td align="center">
      <br>
      <a href="https://github.com/CiscoDevNet"><img src="https://upload.wikimedia.org/wikipedia/commons/6/64/Cisco_logo.svg" height='100px'/></a>
      <br>
      <a href="https://github.com/CiscoDevNet">Cisco DevNet</a>
      <br>
      <br>
    </td>
    <td align="center"> 
      <br>
      <a href="https://tracklify.com" ><img src="https://tracklify.com/static/img/header-logo.4916e646b063.svg" height='100px' /></a>
      <br>
      <a href="https://tracklify.com">Tracklify</a>
      <br>
      <br>
    </td>
    <td align="center"> 
      <br>
      <a href="https://fastdivs.com" ><img src="https://fastdivs.com/static/svg/logo.c1c15aa6d612.svg" height='100px' /></a>
      <br>
      <a href="https://fastdivs.com">FastDivs</a>
      <br>
      <br>
    </td>
  </tr>
  </table>
  <br>


Advantages 💪
=============

- It is lightweight and minimalistic - written with vanilla JS, you don't need dependencies to use it
- Designed to process images with minimal clicks, most actions support hot-keys
- Could be easily integrated into SPA application (React, Vue, Angular)
- Could be used in Electron and Cordova apps
- Flexibale image saving - you provide your save handler, and get base64 data with any jpeg/png compression
- Could be translated to any language 

Originally Painterro was designed for quick screenshots processing: You make screenshot by pressing `PrtSc` button,
then open Painterro on your website, paste an image with `Ctrl+V`,
crop it to interested area, highlight something with line/rectangle tool and/or add some text 
to the image and save on server with custom save handler (e.g. simple `XHR` request to your backend).
In addition, you can use Painterro as image editor for any kind of raster images. Please try a [demo](https://tracklify.com/painterro_demo/).
Also painterro has [Wordpress Plugin](https://wordpress.org/plugins/painterro/).

If you want to see some feature in Painterro, please leave (or vote for) an issue [here](https://github.com/devforth/painterro/issues).
There is no promise that it will be implemented soon or ever, but it is interesting to know what features users want to have.

Usefull hints and tweaks 😋:

- [Painterro JS paint features review on HINTY](https://hinty.io/devforth/js-paint-plugin-for-your-website/)
- [Dark theme for Painterro JS paint](https://hinty.io/devforth/painterro-dark-theme/)
- [Round buttons for Painterro JS paint](https://hinty.io/devforth/how-to-round-the-painterro-buttons/)


Table of contents
=================

  * [Table of contents](#table-of-contents)
  * [Installation](#installation)
    * [With npm](#with-npm)
    * [By including script](#by-including-script)
    * [Read after installation](#read-after-installation)
  * [Supported hotkeys](#supported-hotkeys-)
  * [Configuration](#configuration-)
    * [Events](#events)
    * [UI color scheme](#ui-color-scheme)
    * [API](#api)
    * [Translation](#translation-)
  * [Saving image](#saving-image-)
    * [Base64 saving](#base64-saving)
    * [Binary saving](#binary-saving)
    * [Saving to WYSIWYG](#saving-to-wysiwyg)
    * [Format and quality](#format-and-quality)
    * [Example: Open Painterro by Ctrl+V](#example-open-painterro-by-ctrlv)
  * [Development](#development-)
    * [Building painterro](#building-painterro)
    * [Dev-server](#dev-server)
    * [Regenerating icons font](#regenerating-icons-font)


Installation
============


With npm
--------

If you have npm-based project (e.g. SPA like React/Vue) you can run:
```bash
npm install painterro --save
```
Then in your code

```js
import Painterro from 'painterro'
...
Painterro().show()
```

By including script
-------------------

You can download latest `painterro-*.min.js` here https://github.com/devforth/painterro/releases/ 
or [build it by yourself](#building-painterro).

Then insert `<script>` e.g to `<head>` section of your HTML file:
```html
<script src="/xxx/painterro-x.x.x.min.js"></script>
```
Then in your code (`body` section, `onclick` handler, etc):
```html
<script>
  Painterro().show()
</script>
```
See [jsfiddle.net example](https://jsfiddle.net/vanbrosh/wnebj4h7/)


Read after installation
-----------------------

To be able to save edited images on server or client see [Saving image](#saving-image). For configurations see [Configuration](#configuration)

Supported hotkeys ⌨
=================

| | |
|-|-|
| `Ctrl + Z` | Cancel last operation |
| `Ctrl + V` | Paste image from clipboard |
| `Ctrl + C` | Copy selected aria to clipboard |
| `Shift` when drawing **rect**/**ellipse** | Draw **square**/**circle** |
| `Shift` when drawing **line** | draw at angles of `0`, `45`, `90`, `135` etc degrees | 
| `Alt` when using pipette | Hide zoom helper (colored grid) |
| `Ctrl` + `Wheel mouse up/down` | Zoom image |
| `Ctrl + S` | Save image |

Also some tools have own one-button hotkeys e.g. `C` - crop, you could see this shortcuts if you will hold mouse on toolbutton.

Configuration ⚙
=============

You can pass parameters map to Painterro constructor:
```js
Painterro({
  activeColor: '#00ff00', // default brush color is green
  // ... other params here
})
```

| Param | Description | Default |
|-|-|-|
| `id` | If provided, then Painterro will be placed to some holder on page with this `id`, in other case holder-element will be created (fullscreen with margins). Important note: If you are using your block and id option, please add `position`:`relative` or `absolute` or `fixed` on your container, default (`static`) will lead to positioning issues | `undefined` |
|`activeColor`| Line/Text color that selected by default | `'#ff0000'` |
|`activeColorAlpha` | Transparancy of `activeColor` from `0.0` to `1.0`, `0.0` = transparent | `1` |
|`activeFillColor` | Fill color that selected by default | `'#000000'` |
|`activeFillColorAlpha` | Transparancy of `activeColor` from `0.0` to `1.0` | `0` |
|`defaultLineWidth` | Line width in `px` that selected by default | `5` |
|`defaultPrimitiveShadowOn` | Enable Shadow for primitive tools (easier recognize them on a screenshots) | `true` |
|`defaultEraserWidth` | Eraser width in `px` that selected by default | `5` |
|`backgroundFillColor` | Default background color when image created/erased | `'#ffffff'` |
|`backgroundFillColorAlpha`| Transparancy of `backgroundFillColor` from `0.0` to `1.0` | `1.0` |
|`textStrokeColor`| Stroke color of text tool | `'#ffffff'` |
|`textStrokeColorAlpha`| Stroke color of text tool | `1.0` |
|`shadowScale`| Change text shadow blur for text and arrow | `1.0` |
|`defaultFontSize` | Default font size in pixels | `24` |
|`backplateImgUrl`| background for drawing, doesn't include in final image |`undefined` |
|`defaultTextStrokeAndShadow` | Enables Stroke and Shadow for text tool by default (easier recognize text on screenshots) | `true` |
|`defaultSize` | default image size, should be string in format `<width>x<height>` in pixel, e.g. `'200x100'`. If value is `'fill'`(default) than all container size will be used | `'fill'` |
|`defaultTool` | Tool selected by default | `'select'` | 
|`hiddenTools` | List of tools that you wish to exclude from toolbar. Subset from this list `['crop', 'line', 'arrow', 'rect', 'ellipse', 'brush', 'text', 'rotate', 'resize',  'save', 'open', 'close', 'undo', 'redo', 'zoomin', 'zoomout', 'bucket']`, You can't hide default tool | `['redo']` |
|`initText` | Display some centered text before painting (supports HTML). If null, no text will be shown | `null` |
|`initTextColor` | Color of init text | `'#808080'` |
|`initTextStyle` | Style of init text | `"26px 'Open Sans', sans-serif"` |
|`pixelizePixelSize` | Default pixel size of pixelize tool. Can accept values - `x` - x pixels, `x%` - means percents of minimal area rectangle side | `20%` |
|`pixelizeHideUserInput` | Don't allow users to enter pixel size In settings tools (and save in localstorage), this would allow developer to freeze pixel size by using params `pixelizePixelSize` to make sure users will not set low pixel sizes | `false` |
|`availableLineWidths` | A list of the line width values that are available for selection in a drop down list e.g. `[1,2,4,8,16,64]`.  Otherwise an input field is used. | `undefined` |
|`availableArrowLengths` | A list of the arrow sizes values that are available for selection in a drop down list e.g. `[10,20,30,40,50,60]`.  Otherwise an input field is used. | `undefined` |
| `defaultArrowLength` | default arrow length | `15` |
|`availableEraserWidths` | A list of the eraser width values that are available for selection in a drop down list e.g. `[1,2,4,8,16,64]`.  Otherwise an input field is used. | `undefined` |
|`availableFontSizes` | A list of the font size values that are available for selection in a drop down list e.g. `[1,2,4,8,16,64]`.  Otherwise an input field is used. | `undefined` |
|`toolbarPosition` | Whether to position the toolbar at the top or bottom. | `'bottom'` |
|`fixMobilePageReloader` | By default painterro adds overflow-y: hidden to page body on mobile devices to prevent "super smart" feature lice Chrom's reload page. Unfortunately we can't prevent it by preventDefault. If your want to scroll page when painterro is open, set this to false | `true` |
|`language` | Language of the widget. | `'en'` |
|`how_to_paste_actions`| List of paste options that will be suggested on paste using some paste dialog e.g. `['extend_right', 'extend_down'] `. If there is only one option in list, then it will chosen automatically without dialog | `['replace_all', 'paste_over', 'extend_right', 'extend_down']` |
|`replaceAllOnEmptyBackground`| Whether to select `replace_all` without dialog on first paste after painterro was just opened. So it will replaces background with image (will change dimensions to pasted image when background is empty) | `true` |
|`hideByEsc`| If `true` then `ESC` press will hide widget | `false` | 
|`saveByEnter`| If `true` then `ENTER` press will do same as `Ctrl+S` | `false` | 
|`extraFonts`| By default Text tool supports only several [predefined](https://github.com/devforth/painterro/blob/master/js/text.js#L38) fonts due to compatibility considirations , but yousing this option you can add any fonts you want if you are sure they are available on your page/app | `['Roboto']` |
|`toolbarHeightPx`| Height of toolbar in pixels | `40` | 
|`buttonSizePx`| Button for toolbar in pixels | `32` |
|`bucketSensivity`| Bucket tool sensivity | `100` |
|`customTools`| List of the custom tools which will appear at the left menu after default options. Custom tool includes three options : | `{name:string, callBack:function, iconUrl:dataURL string or URL}` |
|`disableWheelZoom`| Disables the mousewheel zoom with ctrl | `false` |

## Events

| Param | Description | Accepted Arguments |
|-|-|-|
| `onBeforeClose` | Function that will be called when user closes painterro it, call `doClose` to confirm close | `hasUnsavedChaged: bool`, `doCloseCallback: function` |
| `onClose` | If passed will be triggered when painterro closed by X button (use `onHide` for all close reasons) | `undefined` |
| `onHide` | If passed will be triggered when painterro hides (by X button or save or any other way) | `undefined` |
| `onChange` | Function that will be called if something will be changed (painted, erased, resized, etc) | `<exportable image>` | `undefined` |
| `onUndo` | Function that will be called if user will undo (`Ctrl+Z`) | `{<current history state>}` |
| `onRedo` | Function that will be called if user will redo (`Ctrl+Z`) | `{<current history state>}` |
|`onImageFailedOpen`| Function that will be called if image can`t open | `undefined` |
| `onImageLoaded` | Function that will be called if you passed image to `show` and when it was loaded | `undefined` | 
| `saveHandler` | Function that will be called when user presses Save (or `Ctrl+S`), Call `doneCallback` to reflect in painterro that image was saved | `{<exportable image>}`, `doneCallback : function` |


Events accepted arguments:

* `{<exportable image>}` is object:

```
{ 
  image: {
   asBlob: ƒ asBlob(type, quality) // returns blob
   asDataURL: ƒ asDataURL(type, quality) // returns e.g. "data:image/jpeg;base64,/9j/4AAQS...."
   suggestedFileName: ƒ suggestedFileName(type) // returns string
   hasAlphaChannel(): ƒ suggestedFileName() // returns true or false
   getOriginalMimeType: ƒ getOriginalMimeType() // e.g. image/jpeg;
   getWidth: ƒ getWidth() // integer
   getHeight: ƒ getHeight() // integer
  }
  operationsDone: int // integer
} 
```

* `{<current history state>}` is object:

```
{
  prev: {<current history state>} or undefined
  next: {<current history state>} or undefined
  prevCount: int
  sizeh: int
  sizew: int
}
```




UI color scheme
---------------

Next group of params used to configure painterro user interface in simple "JS way". 
They should be placed under `colorScheme` group, for example:
```js
Painterro({
  colorScheme: {
    main: '#fdf6b8', // make panels light-yellow
    control: '#FECF67' // change controls color
  }
}).show()
```

| Param | Description | Default |
|-|-|-|
|`main` | Color of panels, take most of UI space | `'#fff'` |
|`control` | Color of controls background (e.g. button background) | `'#fff'` |
|`controlShadow` | Color controls box shadow | `'0px 0px 3px 1px #bbb'` |
|`controlContent` | Content of controls (e.g. button text) | `'#000000'` |
|`activeControl` | Color for control when it active (e.g. button pressed) | `'#7485B1'` |
|`activeControlContent` | Color for activated control content | `main` |
|`inputBorderColor` | You can add border to inputs, by default color is same as `main` so borders will not be seen | `main` |
|`inputBackground` | Background of inputs | `'#ffffff'` |
|`inputShadow` | shadow of input | `'inset 0 0 4px 1px #ccc'` |
|`inputText` | Color of text in input | `activeControl` |
|`backgroundColor`| Background color of component area which left outside of image due to it size/ratio | `'#999999'` |
|`dragOverBarColor`| Color of bar when dropping file to painterro | `'#899dff'` |
|`hoverControl`| Controls color when mouse hovered | `control` |
|`hoverControlContent`| Controls background color when mouse hovered | `'#1a3d67'` |
|`toolControlNameColor`| Color of toolbar labels that prepend controls | `rgba(255,255,255,0.7)` |

> NOTE: all these params are defined only for simplicity, you are free to redefine them in your cascade style files (we don't use importants and so on, so all props should be easily editable). This mettod is recommended for experts - because you can use your CSS preprocessor variables and adopt Painterro for your design. Example usecase is different color of shadows for a buttons with `::after`/`::before`

API
-------

**.show([optional]openImage, [optional]initialMimeType)** - Shows painterro instance. `openImage` can have next values:

* `false` - will open image that already was drawn before last close
* `some string value`, e.g. `'http://placehold.it/120x120&text=image1'` - will try to load image from url
* all another values - will clear content before open

`initialMimeType` could be used to help painterro understand which file do you try to load there. Could be useful if you want to save the original mime and file opened explicitly (painterro open tool or dnd/ctrl+v handlers get it automatically)

**.hide()** - hide instance

**.setColor(options)** - sets the color of the chosen tool , or changes initial value of color. `options` should be array with two values, `[target,colorWidgetState]`

available values for `target`:

|`line`|, |`bg`| 

`colorWidgetState` - object with requred properties :

* `palleteColor` - color string . Just indicates which color will be shown on the collor pallete.
* `alpha` - number in range from 0 to 1. The same but for alpha channel. 
* `alphaColor` - color string. Color with alpha, which will be using for drawing element.

>NOTE: `paleteColor` and `alpha` is using only for displaing right values in color picker widget, this two options don't effect on color which will be used for drawing elements.

**.setLineWidth()** - set line width for chosen tool. 

**.setArrowLength()** - set width for chosen arrow.

**.setEraserWidth()** - set width of eraser

**.setShadowOn()** - set shadowfor line elements or arrow element. It takes boolean value. 

**.save()** - call save (same save as on buttons bar). Can be used if save button is hidden (`hiddenTools: ['save']`)

**.doScale({ width, height, scale })** - scale the image and resize area.

Scale to match the width and scale height proportinally (e.g. 50x32 will become 100->64):

```
.doScale({width: 100})
```

Scale to fill width and height (e.g. 50x32 will become 11->15):

```
.doScale({width: 11, height: 15})
```

Scale x2  (e.g. 11x12 will become 22->24):

```
.doScale({ scale: 2 })
```

**setZoom(zoomPercentage)** - sets the current zoom percentage


Example:

```js
var p = Painterro()
p.show()
```

Translation 📙
--------------

Want to translate Painterro into your language?

If you need one of languages in table below, just pass pass `language` parameter, for example:
 
```js
Painterro({
  language: 'es'  // Spanish
}).show()
```
Translated languages:

| `language` param | Name |
|-|-|
| `ca` | Catalan |
| `de` | German |
| `en` | English |
| `es` | Spanish |
| `fa` | Iran-Farsi (Persian (Ir-Fa) |
| `fr` | French |
| `ja` | Japanese |
| `pl` | Polish |
| `pt-PT` | European Portuguese |
| `pt-BR` | Brazilian  Portuguese |
| `ru` | Russian |
| `nl` | Dutch |



If you want to add another language, then:

1. fork to your GitHub with button on top.
2. Create empty file in folder langs [<LANG_ISO_CODE>.lang.js] for your translation. `LANG_ISO_CODE` should follow [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
3. Copy content from [langs/en.lang.js] to a new file
4. Then translate all `'Strings'`
5. Add reference in [js/translation.js] inside of your repo. 
5. After that create pull-request, or just open [issue](https://github.com/devforth/painterro/issues) if you don't know how to create a PR.

🤔 Found a bug in some word for your language? Feel free to edit on GitHub directly and suggest a fix.
 
If you want to translate or change strings without contributing you can do this by passing 
`translation` parameter, for example:

```js
Painterro({
  translation: {
    name: 'ua',
    strings: {
      apply: 'Застосувати'    
      // other strings
    }
  }
}).show()
```
For all strings that should be translated, see [langs/en.lang.js] 
    

Saving image 💾
===============

You should provide your own save handler, that will post/update image on server or will pass image to other
frontend components. In this section we will provide several backend examples on python Flask (easiest web server for python). Anyway if you will face any python exception you can use super-helpfull [fixexception.com](https://fixexception.com/) service to fix any issue you will face 💪.

Binary saving
-------------

You can post data with binary `multipart/form-data` request which is the most efficient way to pass data to backend. Example uses raw `XMLHttpRequest`. Of course,
 you can use `fetch`, `jQuery`, etc insead.

```js
var ptro = Painterro({
  saveHandler: function (image, done) {
    var formData = new FormData();
    formData.append('image', image.asBlob());
    // you can also pass suggested filename 
    // formData.append('image', image.asBlob(), image.suggestedFileName());
    var xhr = new XMLHttpRequest();
    xhr.open('POST', 'http://127.0.0.1:5000/save-as-binary/', true);
    xhr.onload = xhr.onerror = function () {
      // after saving is done, call done callback
      done(true); //done(true) will hide painterro, done(false) will leave opened
    };
    xhr.send(formData);
  }
})
ptro.show();
```
Here is python flask backend example (of course same can be implemented using any technology):
```python
@app.route("/save-as-binary/", methods=['POST'])
def binary_saver():
    filename = '{:10d}.png'.format(int(time()))  # generate some filename
    filepath = os.path.join(get_tmp_dir(), filename)
    request.files['image'].save(filepath)
    return jsonify({})
```

See full example in `example` directory. You can run it used python3 with installed `Flask` (`pip install flask`).

Base64 saving
-------------


You can also same image by posting `base64` string via plain POST json call.
Please note that base64 encoding is less efficient then binary data, for example some `1920 x 1080` image took `402398` bytes for `base64` upload.
The same image took `301949` bytes with `multipart/form-data`.


```js
var ptro = Painterro({
    saveHandler: function (image, done) {
      // of course, instead of raw XHR you can use fetch, jQuery, etc
      var xhr = new XMLHttpRequest();
      xhr.open("POST", "http://127.0.0.1:5000/save-as-base64/");
      xhr.setRequestHeader("Content-Type", "application/json");
      xhr.send(JSON.stringify({
        image: image.asDataURL()
      }));
      xhr.onload = function (e) {
        // after saving is done, call done callback
        done(true); //done(true) will hide painterro, done(false) will leave opened
      }
    },
    activeColor: '#00b400'  // change active color to green
});
ptro.show();
```
Backend should convert `base64` to binary and save file:
```python
@app.route("/save-as-base64/", methods=['POST'])
def base64_saver():
    filename = '{:10d}.png'.format(int(time()))  # generate some filename
    filepath = os.path.join(get_tmp_dir(), filename)
    with open(filepath, "wb") as fh:
        base64_data = request.json['image'].replace('data:image/png;base64,', '')
        fh.write(base64.b64decode(base64_data))
    return jsonify({})
```


Saving to WYSIWYG
-----------------

You can just insert image as data url to any WYSIWYG editor, e.g. TinyMCE:
```js
    tinymce.init({ selector:'textarea', });
    var ptro = Painterro({
      saveHandler: function (image, done) {
        tinymce.activeEditor.execCommand('mceInsertContent', false, '<img src="' + image.asDataURL() + '" />');
        // after saving is done, call done callback
        done(true); //done(true) will hide painterro, done(false) will leave opened
      }
    })
```

Format and quality
------------------

When you call `image.asDataURL()` or `image.asBlob()`, you can also specify image mime type (format), e.g.
`image.asDataURL('image/jpeg')`. 

Default type is mimetype used by image which was loaded into Painterro, or "image/png" if image was created from scratch.

If type is `image/jpeg` or `image/webp`, you can also define image quality from `0.0` to `1.0`, default is `0.92`,
example: `image.asDataURL('image/jpeg', 0.5)`


Save to jpeg or png depending on whether there is an alpha channel
------------------

An efficient way to save an image might be implmented by checking whether image has some alpha pixels:
* If yes - we need to serve image in less efficient png format
* Otherwise lets just use JPEG
This is very simple with next:

```
var ptro = Painterro({
  saveHandler: function (image, done) {

    image.asBlob(image.hasAlphaChannel() ? 'image/png' : 'image/jpeg');
    // upload blob
  }
})
```

Example: Open Painterro by Ctrl+V
-----------------

```js
document.onpaste = (event) => {
  const { items } = event.clipboardData || event.originalEvent.clipboardData;
  Array.from(items).forEach((item) => {
    if (item.kind === 'file') {
      if (!window.painterroOpenedInstance) {
        // if painterro already opened - it will handle onpaste
        const blob = item.getAsFile();
        const reader = new FileReader();
        reader.onload = (readerEvent) => {
            window.painterroOpenedInstance = Painterro({
              onHide: () => {
                window.painterroOpenedInstance = undefined;
              },
              saveHandler: (image, done) => {
                console.log('Save it here', image.asDataURL());  // you could provide your save handler
                done(true);
              },
            }).show(readerEvent.target.result, item.type);
        };
        reader.readAsDataURL(blob);
      }
    }
  });
};
```

If you face any painterro errors (exceptions), please reffer to [Painterro page on FixJSError](https://fixjserror.com/package/painterro/)

Development 🔨
==============

Latest supported NodeJS version is 16, use nvm to switch to it:

```
nvm install 16
nvm use 16
```

Code written on ES6 which transplited by Babel and packed (minified) to a single file using webpack. All configs are inside so all you have to do after pulling repo is installing node modules:

```bash
cd painterro
npm ci
```

Building painterro
------------------

```bash
npm run build
```

Result file for `<script>` import is `build/painterro.min.js`.

Actually, above command produces 4 versions of library:

- `build/painterro-x.y.z.min.js`, `build/painterro.min.js` the same files but with different filenames (with and without versiontag) - this is `var` version which will be loaded as global variable (`var painterro = <Library class>`) when you will import it as `<script src='painterro.min.js' />` tag. So this is for `script` tag only.   
- `build/painterro.commonjs2.js` - this version sutable for js `require/import`. That's why it is used as entry point in `package.json` file - if you are using webpack or other tool that can handle `require/import` of `commonjs2` libraries then you can do `npm install painterro`, and do `import painterro` and it will use `commonjs2` version.
- `build/painterro.amd.js` and `build/painterro.umd.js` - these both are same as above but for `AMD` and `UMD` importers respectivly.


Dev-server
----------

To start hot-reload dev server (for reloading code "on the fly"):
```bash
npm run dev
```
Then open http://localhost:3000 with demo page


Editing source on the fly for painterro imported from side webpack app (e.g. your project SPA)
------------------------------------------

1. If your side app uses 'eslint' it, most likely side app will need eslint-plugin-import:

```
npm i eslint-plugin-import
```

2. Since compiled painterro commonjs2 file already linted and minimized you need to exclude it from linting:

Add to package.json of your side app:
```
  "eslintIgnore": [
    "/home/ivan/devforth/painterro/build/painterro2.commonjs.js"
  ],
```
where `/home/ivan/devforth/painterro` is a folder with Painterro sources

3. Replace

```
import Painterro from 'painterro';
```
with
 
```
import Painterro from '/home/ivan/devforth/painterro/build/painterro.commonjs2.js';
```

4. Go to painterro source folder and run:

```
watch npm run build
```

Regenerating icons font
-----------------------

If you need add/edit icons in `res` folder, please after editing run:

```bash
npm run buildfont
```

For font generation we use method described here: [How to generate a webfont (automated setup)](https://hinty.io/brucehardywald/how-to-generate-a-webfont-automated-setup/)



Contributing 
------------

Pull-requests are welcome.

If you want to say thank us [Patreon is here](https://www.patreon.com/devforth)


[npm]: https://img.shields.io/npm/v/painterro.svg
[npm-url]: https://npmjs.com/package/painterro

[deps]: https://david-dm.org/webpack/painterro.svg
[deps-url]: https://david-dm.org/webpack/painterro

Supported by [DevForth](https://devforth.io) - Best quality, rapid,  modern tech development services


================================================
FILE: Release.md
================================================


npm login

Tocken from github:

GH_PASS=`cat ~/.ghtoken`

Password from WP:
WP_PASSWORD=`cat ~/.wppassword`

================================================
FILE: build/contained.html
================================================
<html>
  <head>
    <title>Painterro demo</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
      html,body,#wrapper {
        height:100%;
        margin:0;
        padding:0;
        background-color: lightgray;
      }

      #conatiner {
        position: relative;
        height: 300px;
        width: 300px;
      }

    </style>
  </head>
<body>

<div id="wrapper" style="height: 100%;">
  <div style="height: 300px;  background-color: antiquewhite"></div>
  <div id="conatiner"></div>
</div>

<!--<script src="https://github.com/ivictbor/painterro/releases/download/0.1.7/painterro-0.1.7.min.js"></script>-->
<script src="/painterro.min.js"></script>

<script>

  Painterro({
    id: 'conatiner',
    backgroundFillColor: '#eee',
    backplateImg: 'https://images.pexels.com/photos/3653762/pexels-photo-3653762.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
    colorScheme: {
      main: '#faf',
      control: '#d5d',
      activeControl: '#572257',
      inputBackground: '#cf99ca',
    }
  }).show();
</script>
</body>
</html>


================================================
FILE: build/index.html
================================================
<html>
  <head>
    <title>Painterro demo</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
      html,body,#wrapper {
        height:100%;
        margin:0;
        padding:0;
        background-color: white;
      }

      #conatiner {
        position: absolute;
        top: 50px;
        bottom: 50px;
        left: 20px;
        right: 20px;
      }

#holder {
  display: flex;
}

    </style>
  </head>
<body>
  <div id="app">
  </div>

  <div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div>
  <div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div><div>asf</div>
</div>

<!--<script src="https://github.com/ivictbor/painterro/releases/download/0.1.7/painterro-0.1.7.min.js"></script>-->
<script src="painterro.min.js"></script>

<script>

window.p =  Painterro({
            language: 'uk',
            backgroundFillColorAlpha: 0,
            hideByEsc: true,
            colorScheme: {
                main: '#8B817A',
                control: '#1A1A1A',
                controlContent: '#fff',
                controlShadow: 'none',
                activeControl: '#6F6762',
                activeControlContent: '#fff',
                hoverControl: '#6F6762',
                hoverControlContent: '#fff',
            },
         
            //defaultTool : 'brush',
            //how_to_paste_actions: ['extend_right'],
            /*hiddenTools: [
                'select',
                'crop',
                'pixelize',
                'line',
                'arrow',
                'rect',
                'ellipse',
                // 'brush',
                // 'eraser',
                'text',
                'rotate',
                'resize',
                // 'save',
                'open',
                // 'close',
                // 'undo',
                'redo',
                // 'zoomin',
                // 'zoomout',
                'bucket',
                'clear',
                'settings',
            ],
            */
           saveHandler: this._onPainterroSave,
        });
  window.p.show()
  const ctx = window.p.ctx; 

  // ctx.beginPath();
  // ctx.moveTo(0,0);
  // ctx.lineTo(300,150);
  // ctx.strokeStyle="#FF0000";
  // ctx.stroke(); 


// document.onpaste = (event) => {
//   const { items } = event.clipboardData || event.originalEvent.clipboardData;
//   Array.from(items).forEach((item) => {
//     if (item.kind === 'file') {
//       if (!window.painterroOpenedInstance) {
//         // if painterro already opened - it will handle onpaste
//         const blob = item.getAsFile();
//         const reader = new FileReader();
//         reader.onload = (readerEvent) => {
//             window.painterroOpenedInstance = Painterro({
//               initText: 'Press <b>PrtScr</b>, <b>Ctrl+V</b> to paste screenshot.<br>Press <b>Ctrl+S</b> to save', // todo: different for os-es
//               onHide: () => {
//                 window.painterroOpenedInstance = undefined;
//               },
//               saveHandler: (image, done) => {
//                 console.log('Save it here', image.asDataURL());  // you could provide your save handler
//                 done(true);
//               },
//             }).show(readerEvent.target.result, item.type);
//         };
//         reader.readAsDataURL(blob);
//       }
//     }
//   });
// };

const el = document.getElementById('app')
el.addEventListener('changeActiveTool',(e)=>{console.log('activeToolHasBeenCHanged',e)})

</script>
</body>
</html>


================================================
FILE: css/bar-styles.css
================================================
.color-diwget-btn {
    height: 32px;
    width: 32px;
    cursor: pointer;
    z-index: 1;
}

.color-diwget-btn-substrate {
    width: 32px;
}

select.ptro-input[data-id='fontName'] {
    width: 45px;
}

.ptro-bar .ptro-tool-ctl-name {
    padding: 0 2px 0 0;
    font-family: "Open Sans", sans-serif;
    line-height: 22px;
}

.ptro-bar .ptro-tool-ctl-name {
    margin-left: 5px;
    border-top-left-radius: 10px;
    border-bottom-left-radius: 10px;
    padding-left: 3px;
    padding-top: 4px;
    padding-bottom: 4px;
}

.ptro-info {
    font-family: "Open Sans", sans-serif;
    font-size: 10px;
    padding: 4px;
    margin-left: auto;
    word-break: keep-all;
    white-space: normal;
    text-align: right;
}

.ptro-info > span {
    opacity: 0.5;
}

@media screen and (max-width: 768px) {
    .ptro-bar > div {
        white-space: nowrap;
    }
    span.ptro-bar-right {
        float: none;
    }
    span.ptro-info {
        display: none;
    }
    
}

.ptro-bar .ptro-input {
    height: 32px;
    line-height: 32px;
    font-family: "Open Sans", sans-serif;
    font-size: 16px;
    position: relative;
    padding-left: 2px;
    padding-right: 0;
}

.ptro-bar .ptro-input[type="number"] {
    width: 42px;
}

.ptro-bar .ptro-named-btn p {
    margin: 0;
}

.ptro-bar {
    bottom: 0;
    position: absolute;
    width: 100%;
    font-size: 16px;
    line-height: normal;
}

.ptro-bar > div {
    position: relative;
}

.ptro-bar > div::-webkit-scrollbar {
    height: 2px;
}
    
/* Track */
.ptro-bar > div::-webkit-scrollbar-track {
    background: #f1f1f1;
}
    
/* Handle */
.ptro-bar > div::-webkit-scrollbar-thumb {
    background: #888;
}
    
/* Handle on hover */
.ptro-bar > div::-webkit-scrollbar-thumb:hover {
    background: #555;
}

.ptro-bar .ptro-icon-btn {
}

button.ptro-icon-right:first-of-type {
    margin-right: 4px;
}

button.ptro-input[data-value="false"],button.ptro-input[data-value="true"] {
    width: 28px;
    height: 28px;
    border: 0;
    background: transparent;
    display: flex;
    justify-content: center;
    align-items: center;
    padding-left: 0px;
    cursor: pointer;
    outline: 0;
}

button.ptro-input[data-value="true"]::after {
    content: '✔';
    font-size: 20px;
    line-height: 12px;
    width: 12px;
    height: 12px;
    border: 0;
    /* background: rgba(0,0,0,0.5); */
    display: inline-block;
}

@-webkit-keyframes ptro-spin {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(359deg);
    transform: rotate(359deg);
  }
}
@keyframes ptro-spin {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(359deg);
    transform: rotate(359deg);
  }
}

.ptro-spinning {
    -webkit-animation: ptro-spin 0.5s infinite steps(9);
    animation: ptro-spin 0.8s infinite steps(9);
    display: inline-block;
    text-rendering: auto;
    -webkit-font-smoothing: antialiased;
}

#container-bar {
    display: block;
}

================================================
FILE: css/icons/ptroiconfont.css
================================================
@font-face {
	font-family: "ptroiconfont";
	src: url("ptroiconfont.woff?9d16276326db52747d3405a7ca0e1306") format("woff"),
url("ptroiconfont.ttf?9d16276326db52747d3405a7ca0e1306") format("truetype");
	font-weight: normal;
    font-style: normal;
}

.ptro-icon {
}

.ptro-icon:before {
	font-family: ptroiconfont !important;
	font-style: normal !important;
	font-weight: normal !important;
	font-variant: normal !important;
	text-transform: none !important;
	speak: none;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

.ptro-icon-apply:before {
	content: "\f101";
}
.ptro-icon-arrow:before {
	content: "\f102";
}
.ptro-icon-brush:before {
	content: "\f103";
}
.ptro-icon-bucket:before {
	content: "\f104";
}
.ptro-icon-clear:before {
	content: "\f105";
}
.ptro-icon-close:before {
	content: "\f106";
}
.ptro-icon-crop:before {
	content: "\f107";
}
.ptro-icon-ellipse:before {
	content: "\f108";
}
.ptro-icon-eraser:before {
	content: "\f109";
}
.ptro-icon-filters:before {
	content: "\f10a";
}
.ptro-icon-line:before {
	content: "\f10b";
}
.ptro-icon-linked:before {
	content: "\f10c";
}
.ptro-icon-loading:before {
	content: "\f10d";
}
.ptro-icon-mirror:before {
	content: "\f10e";
}
.ptro-icon-open:before {
	content: "\f10f";
}
.ptro-icon-painterro0:before {
	content: "\f110";
}
.ptro-icon-paste_extend_down:before {
	content: "\f111";
}
.ptro-icon-paste_extend_left:before {
	content: "\f112";
}
.ptro-icon-paste_extend_right:before {
	content: "\f113";
}
.ptro-icon-paste_extend_top:before {
	content: "\f114";
}
.ptro-icon-paste_fit:before {
	content: "\f115";
}
.ptro-icon-paste_over:before {
	content: "\f116";
}
.ptro-icon-pipette:before {
	content: "\f117";
}
.ptro-icon-pixelize:before {
	content: "\f118";
}
.ptro-icon-rect:before {
	content: "\f119";
}
.ptro-icon-redo:before {
	content: "\f11a";
}
.ptro-icon-resize:before {
	content: "\f11b";
}
.ptro-icon-rotate:before {
	content: "\f11c";
}
.ptro-icon-save:before {
	content: "\f11d";
}
.ptro-icon-select:before {
	content: "\f11e";
}
.ptro-icon-settings:before {
	content: "\f11f";
}
.ptro-icon-text:before {
	content: "\f120";
}
.ptro-icon-undo:before {
	content: "\f121";
}
.ptro-icon-unlinked:before {
	content: "\f122";
}
.ptro-icon-zoomin:before {
	content: "\f123";
}
.ptro-icon-zoomout:before {
	content: "\f124";
}


================================================
FILE: css/icons/ptroiconfont.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>ptroiconfont</title>
	<style>
		body {
			font-family: sans-serif;
			margin: 0;
			padding: 10px 20px;
		}

		.preview {
			line-height: 2em;
		}

		.preview__icon {
			display: inline-block;
			width: 32px;
			text-align: center;
		}

		.ptro-icon {
			display: inline-block;
			font-size: 16px;
		}

		@font-face {
	font-family: "ptroiconfont";
	src: url("ptroiconfont.woff?ebe1a4332c0507d6283582e66d937ed1") format("woff"),
url("ptroiconfont.ttf?ebe1a4332c0507d6283582e66d937ed1") format("truetype");
	font-weight: normal;
    font-style: normal;
}

.ptro-icon {
}

.ptro-icon:before {
	font-family: ptroiconfont !important;
	font-style: normal !important;
	font-weight: normal !important;
	font-variant: normal !important;
	text-transform: none !important;
	speak: none;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

.ptro-icon-apply:before {
	content: "\f101";
}
.ptro-icon-arrow:before {
	content: "\f102";
}
.ptro-icon-brush:before {
	content: "\f103";
}
.ptro-icon-bucket:before {
	content: "\f104";
}
.ptro-icon-clear:before {
	content: "\f105";
}
.ptro-icon-close:before {
	content: "\f106";
}
.ptro-icon-crop:before {
	content: "\f107";
}
.ptro-icon-ellipse:before {
	content: "\f108";
}
.ptro-icon-eraser:before {
	content: "\f109";
}
.ptro-icon-filters:before {
	content: "\f10a";
}
.ptro-icon-line:before {
	content: "\f10b";
}
.ptro-icon-linked:before {
	content: "\f10c";
}
.ptro-icon-loading:before {
	content: "\f10d";
}
.ptro-icon-mirror:before {
	content: "\f10e";
}
.ptro-icon-open:before {
	content: "\f10f";
}
.ptro-icon-painterro0:before {
	content: "\f110";
}
.ptro-icon-paste_extend_down:before {
	content: "\f111";
}
.ptro-icon-paste_extend_left:before {
	content: "\f112";
}
.ptro-icon-paste_extend_right:before {
	content: "\f113";
}
.ptro-icon-paste_extend_top:before {
	content: "\f114";
}
.ptro-icon-paste_fit:before {
	content: "\f115";
}
.ptro-icon-paste_over:before {
	content: "\f116";
}
.ptro-icon-pipette:before {
	content: "\f117";
}
.ptro-icon-pixelize:before {
	content: "\f118";
}
.ptro-icon-rect:before {
	content: "\f119";
}
.ptro-icon-redo:before {
	content: "\f11a";
}
.ptro-icon-resize:before {
	content: "\f11b";
}
.ptro-icon-rotate:before {
	content: "\f11c";
}
.ptro-icon-save:before {
	content: "\f11d";
}
.ptro-icon-select:before {
	content: "\f11e";
}
.ptro-icon-settings:before {
	content: "\f11f";
}
.ptro-icon-text:before {
	content: "\f120";
}
.ptro-icon-undo:before {
	content: "\f121";
}
.ptro-icon-unlinked:before {
	content: "\f122";
}
.ptro-icon-zoomin:before {
	content: "\f123";
}
.ptro-icon-zoomout:before {
	content: "\f124";
}

	</style>
</head>
<body>
	<h1>ptroiconfont</h1>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-apply"></span>
		</span>
		<span>apply</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-arrow"></span>
		</span>
		<span>arrow</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-brush"></span>
		</span>
		<span>brush</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-bucket"></span>
		</span>
		<span>bucket</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-clear"></span>
		</span>
		<span>clear</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-close"></span>
		</span>
		<span>close</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-crop"></span>
		</span>
		<span>crop</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-ellipse"></span>
		</span>
		<span>ellipse</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-eraser"></span>
		</span>
		<span>eraser</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-filters"></span>
		</span>
		<span>filters</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-line"></span>
		</span>
		<span>line</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-linked"></span>
		</span>
		<span>linked</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-loading"></span>
		</span>
		<span>loading</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-mirror"></span>
		</span>
		<span>mirror</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-open"></span>
		</span>
		<span>open</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-painterro0"></span>
		</span>
		<span>painterro0</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-paste_extend_down"></span>
		</span>
		<span>paste_extend_down</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-paste_extend_left"></span>
		</span>
		<span>paste_extend_left</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-paste_extend_right"></span>
		</span>
		<span>paste_extend_right</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-paste_extend_top"></span>
		</span>
		<span>paste_extend_top</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-paste_fit"></span>
		</span>
		<span>paste_fit</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-paste_over"></span>
		</span>
		<span>paste_over</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-pipette"></span>
		</span>
		<span>pipette</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-pixelize"></span>
		</span>
		<span>pixelize</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-rect"></span>
		</span>
		<span>rect</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-redo"></span>
		</span>
		<span>redo</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-resize"></span>
		</span>
		<span>resize</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-rotate"></span>
		</span>
		<span>rotate</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-save"></span>
		</span>
		<span>save</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-select"></span>
		</span>
		<span>select</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-settings"></span>
		</span>
		<span>settings</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-text"></span>
		</span>
		<span>text</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-undo"></span>
		</span>
		<span>undo</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-unlinked"></span>
		</span>
		<span>unlinked</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-zoomin"></span>
		</span>
		<span>zoomin</span>
	</div>
	<div class="preview">
		<span class="preview__icon">
			<span class="ptro-icon ptro-icon-zoomout"></span>
		</span>
		<span>zoomout</span>
	</div>
</body>
</html>


================================================
FILE: css/styles.css
================================================
.ptro-wrapper {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    text-align: center;
    z-index: 10;
    font-family: "Open Sans", sans-serif;
}

@media screen and (min-width: 869px) {
    .ptro-holder {
        position: fixed;
        left: 35px;
        right: 35px;
        top: 35px;
        bottom: 35px;
        box-shadow: 1px 1px 5px #888;
    }
}

@media screen and (max-width: 868px) {
    .ptro-holder {
        position: fixed;
        box-shadow: 3px 3px 15px #787878;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
    }
}

.ptro-holder-wrapper {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0,0,0,0.2);
}

.ptro-wrapper.ptro-v-aligned:before {
    content: "";
    display: inline-block;
    vertical-align: middle;
    height: 100%;
}


.ptro-icon {
    font-size: 14px;
    display: flex;
    align-items: center;
    justify-content: center;
}


.ptro-icon-btn:disabled {
    color: gray;
}

.ptro-wrapper canvas {
    /* vertical-align: middle; */
    display: inline-block;
    touch-action: none;
    margin-left: auto;
    margin-right: auto;
    width: auto;
    height: auto;
}

.ptro-center-table {
    display:table;
    width: 100%;
    height: 100%;
}

.ptro-center-tablecell {
    display:table-cell;
    vertical-align:middle;
}

.ptro-icon-btn {
    border: 0;
    cursor: pointer;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}

.ptro-icon-btn i {
}


.ptro-named-btn {
    border: 0;
    display: inline-block;
    height: 30px;
    margin-left: 4px;
    font-family: "Open Sans", sans-serif;
    position: relative;
    top:-5px;
    font-size: 14px;
    cursor: pointer;
}

.ptro-icon-btn:focus,
.ptro-named-btn:focus,
.color-diwget-btn:focus,
.ptro-color-btn:focus,
.ptro-selector-btn:focus {
    outline: none;
}

.ptro-color-btn {
    height: 32px;
    width: 32px;
    cursor: pointer;
}


.ptro-wrapper .select-handler {
    background-color: white;
    border: 1px solid black;
    width: 6px;
    height: 6px;
    position: absolute;
    z-index: 10;
}

.ptro-wrapper .ptro-crp-el {
    position: absolute;
}

.ptro-wrapper .ptro-substrate {
    opacity: 0.3;
    background-image: url("checkers.svg");
    background-size: 32px 32px;
    z-index: -1;
    position: absolute;
}

.ptro-wrapper .ptro-close-color-picker {
    height: 24px;
    margin-top: 5px;
    margin-bottom: -5px;
    margin-left: auto;
}

.ptro-tool-controls {
    flex-shrink: 0;
    display: flex;
    align-items: center;
}

.ptro-wrapper .ptro-crp-rect {
    position: absolute;
    background-color: rgba(225, 225, 225, .5);
    border: 1px dashed black;
    cursor: move;
    -moz-user-select: none;
    /* -webkit-user-select: none; */
    -ms-user-select: none;
    user-select: none;
    -webkit-user-drag: none;
    user-drag: none;
    -webkit-touch-callout: none;
    background-repeat: no-repeat;
    background-size: 100% 100%;
}

.ptro-wrapper .ptro-crp-tl {
    position: absolute;
    top: 0;
    left: 0;
    margin: -4px 0 0 -4px;
    cursor: se-resize;
}

.ptro-wrapper .ptro-crp-bl {
    position: absolute;
    left: 0;
    bottom: 0;
    margin: 0 0 -4px -4px;
    cursor: ne-resize;
}

.ptro-wrapper .ptro-crp-br {
    position: absolute;
    right: 0;
    bottom: 0;
    margin: 0 -4px -4px 0;
    cursor: se-resize;
}

.ptro-wrapper .ptro-crp-tr {
    position: absolute;
    right: 0;
    top: 0;
    margin: -4px -4px 0 0;
    cursor: ne-resize;
}


.ptro-wrapper .ptro-crp-l {
    position: absolute;
    top: 50%;
    left: 0;
    margin: -4px 0 0 -4px;
    cursor: e-resize;
}

.ptro-wrapper .ptro-crp-t {
    position: absolute;
    top: 0;
    left: 50%;
    margin: -4px 0 0 -4px;
    cursor: s-resize;
}

.ptro-wrapper .ptro-crp-r {
    position: absolute;
    top: 50%;
    right: 0;
    margin: -4px -4px 0 0 ;
    cursor: e-resize;
}

.ptro-wrapper .ptro-crp-b {
    position: absolute;
    left: 50%;
    bottom: 0;
    margin: 0 0 -4px -4px;
    cursor: s-resize;
}

.ptro-wrapper div,
.ptro-wrapper span,
.ptro-wrapper i,
.ptro-bar .ptro-tool-ctl-name,
.ptro-bar input,
.ptro-bar .ptro-named-btn p {
    -moz-user-select: none;
   /* -webkit-user-select: none; */
    -ms-user-select: none;
    user-select: none;
    -webkit-user-drag: none;
    user-drag: none;
    -webkit-touch-callout: none;
}

.ptro-bar > div {
    overflow-x: auto;
    overflow-y: hidden;
    height: 100%;
    display: flex;
    align-items: center;
}



.ptro-wrapper .ptro-common-widget-wrapper {
    position: absolute;
    background-color: rgba(0, 0, 0, 0.6);
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
}

.ptro-wrapper .ptro-pallet canvas {
    cursor: crosshair;
}

div.ptro-pallet {
    line-height: 0;
}

.ptro-wrapper .ptro-pallet,
.ptro-wrapper .ptro-resize-widget{
    width: 200px;
    padding: 10px;
    z-index: 100;
    box-sizing: border-box;
}

.ptro-error {
    background-color: rgba(200, 0, 0, 0.5);
    padding: 5px;
    margin: 5px;
    color: white;
}

.ptro-v-middle:before {
    content: "";
    display: inline-block;
    vertical-align: middle;
    height: 100%;
}

.ptro-v-middle-in {
    display: inline-block;
    vertical-align: middle;
    position: relative;
}

.ptro-wrapper .ptro-settings-widget {
    width: 300px;
    padding: 10px;
    z-index: 100;
    box-sizing: border-box;
}

td.ptro-resize-table-left {
    text-align: right;
    padding-right: 5px;
    float: none;
    font-size: 14px;
}

.ptro-wrapper .ptro-color-edit {
    margin-top: 15px;
}

.ptro-wrapper .ptro-color-edit input {
    float: left;
    height: 24px;
    text-align: center;
    font-family: monospace;
    font-size: 14px;
}

.ptro-wrapper .ptro-color-edit input:focus {
    outline: none;
}

.ptro-wrapper .ptro-color-edit input.ptro-color {
    width: 70px;
}

.ptro-wrapper .ptro-color-edit input.ptro-color-alpha {
    font-size: 14px;
    width: 55px;
    padding: 0 0 0 2px;
    line-height: 23px;
    height: 23px;
}

.ptro-wrapper .ptro-color-alpha-label,
.ptro-wrapper .ptro-label  {
    float: left;
    padding: 0 2px 0 0;
    margin-left: 5px;
    font-family: "Open Sans", sans-serif;
}

.ptro-pixel-size-input {
    width: 60px;
}

.ptro-wrapper .ptro-pipette {
    height: 24px;
    width: 24px;
    margin: 0;
}

div.ptro-color-widget-wrapper {
	z-index: 1000;
}

.ptro-wrapper .ptro-pipette i {
    line-height: 16px;
}

.ptro-wrapper .ptro-pipette:active {
    outline: none;
}

.ptro-wrapper .ptro-color-widget-wrapper .ptro-canvas-light,
.ptro-wrapper .ptro-color-widget-wrapper .ptro-canvas-alpha {
    margin-top: 10px;
}

span.ptro-color-light-regulator,
span.ptro-color-alpha-regulator {
    display: block;
    margin-top: -5px;
    margin-left: 5px;
    position: absolute;
    width: 0;
    height: 0;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-bottom: 5px solid;
    cursor: crosshair;
}

span.ptro-color-alpha-regulator {
    margin-top: 0;
}

.alpha-checkers {
    background-image: url("checkers.svg");
    display: block;
    width: 100%;
    height: 15px;
    background-size: 10px 10px;
    margin-top: -20px;
}

input.ptro-input:focus,
select.ptro-input:focus {
    outline: none;
    box-shadow: none ;
}

input.ptro-input,
select.ptro-input {
    vertical-align: initial;
    padding-top: 0;
    padding-bottom: 0;
    padding-right: 0;
}

.ptro-named-btn p {
    font-size: inherit;
    line-height: normal;
    margin: inherit;
}

.ptro-wrapper .ptro-zoomer {
    border-top:1px solid white;
    border-left:1px solid white;
    position: absolute;
    z-index: 2000;
    display: none;
}

.ptro-text-tool-input {
    background-color: rgba(0,0,0,0);
    width: auto;
    outline: 1px dotted;
    display: block;
    min-width: 5px;
    padding: 0 1px;
    overflow-x: hidden;
    word-wrap: break-word;
    overflow-y: hidden;
    box-sizing: content-box;
    line-height: normal;
    text-align: left;
}
.ptro-paster-wrappers-fits {
    display: flex;
    justify-content: space-around;
    align-items: center;
}
.ptro-selector-extend[type] {
    height: 70px;
    width: 70px;
}
.ptro-selector-extend div:last-child {
    display: none;
}
.ptro-selector-fit[type] {
    height: 220px;
    width: 220px;
    margin: 0px;
}
.ptro-paster-fit[class] {
    margin-right: 46px;
}
.ptro-text-tool-buttons {
    display: flex;
    position: absolute;
}
.ptro-text-tool-input-wrapper {
    position: absolute;
}

span.ptro-btn-color-checkers {
    background-image: url("checkers.svg");
    display: block;
    width: 32px;
    height: 32px;
    background-size: 16px 16px;
    margin-top: -32px;
}

span.ptro-btn-color-checkers-bar {
    background-image: url("checkers.svg");
    width: 32px;
    line-height: 12px;
    height: 32px;
    background-size: 16px 16px;
    z-index: 0;
    position: relative;
    margin-left: -32px;
}

.ptro-bar-right {
    display: flex;

}

.ptro-link {
    float: left;
    margin-right: -12px;
    margin-top: -23px;
}

.ptro-resize-link-wrapper {
    display: inline-block;
    height: 40px;
}


input.ptro-resize-width-input,
input.ptro-resize-heigth-input,
input.ptro-pixel-size-input {
    line-height: 22px;
    padding: 0 0 0 4px;
    height: 22px;
    width: 80px;
}

.ptro-selector-btn i {
    font-size: 56px;
}

.ptro-selector-btn {
    opacity: 0.8;
    border: 0;
    width: 100px;
    cursor: pointer;
}

.ptro-selector-btn {
    margin-left: 5px;
    margin-right: 5px;
    margin-top: 5px;
    margin-bottom: 5px;
}

.ptro-selector-btn div {
    margin: 5px 0;
}

.ptro-paster-select .ptro-in div {
    font-family: "Open Sans", sans-serif;
    font-size: 14px;
}

.ptro-selector-btn:hover {
    opacity: 0.6;
}

.ptro-paster-select {
    display: inline-block;
    margin-left: auto;
    margin-right: auto;
    height: 100%;
}

.ptro-paster-select .ptro-in {
    background-color: rgba(0,0,0,0.7);
    padding: 40px;
}
.ptro-paster-select-wrapper {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
}
.ptro-paster-select-wrapper-extends button:first-child {
    display: block;
    margin: 0 auto;
}
.ptro-paster-select-wrapper-extends button:last-child{
    display: block;
    margin: 0 auto;
}
.ptro-paster-select-wrapper-extends button:nth-child(2){
    display: inline-block;
    margin-right: 78px;
}
.ptro-paster-fit .ptro-paster-wrapper-label[class] {
    display: block; 
    color: white;
    font-size: 20px;
    text-align: center;
    margin-top: 10px;
    text-transform: uppercase;
}
.ptro-paster-select-wrapper-extends .ptro-paster-wrapper-label[class] {
    display: block; 
    color: white;
    font-size: 20px;
    text-align: center;
    margin-top: 10px;
    text-transform: uppercase;
}
.ptro-paste-label {
    color: white;
    margin-bottom: 10px;
}

.ptro-iframe {
    width: 100%;
    height: 100%;
    border: 0;
}



i.mce-i-painterro:before, span.mce_painterro:before {
	font-size: 20px;
	font-family: ptroiconfont;
	font-style: normal;
	font-weight: normal;
	font-variant: normal;
	text-transform: none;
	speak: none;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
	content: "\f101";
}

.ptro-scroller {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
}

td.ptro-strict-cell {
    font-size: 8px;
    line-height: normal;
}

================================================
FILE: example/server.py
================================================
import base64
import os
import tempfile
from time import time
from flask import Flask, jsonify
from flask.globals import request
from flask.helpers import send_from_directory
from flask.templating import render_template

app = Flask(__name__)

TMP_DIR_NAME = 'painterro_server'

def get_tmp_dir():
    d = os.path.join(tempfile.gettempdir(), TMP_DIR_NAME)
    if not os.path.exists(d):
        os.makedirs(d)
    return d


@app.route("/")
def base64_page():
    files = reversed(
        sorted(
            [f for f in os.listdir(get_tmp_dir()) if f.endswith('png')]))
    return render_template('paste_as_base64.html', files=files)


@app.route("/bin/")
def bin_page():
    files = reversed(
        sorted(
            [f for f in os.listdir(get_tmp_dir()) if f.endswith('png')]))
    return render_template('paste_as_bin.html', files=files)


@app.route("/paste/")
def paste_page():
    return render_template('paste_to_tinymce.html')


@app.route("/save-as-base64/", methods=['POST'])
def base64_saver():
    filename = '{:10d}.png'.format(int(time()))  # generate some filename
    filepath = os.path.join(get_tmp_dir(), filename)
    with open(filepath, "wb") as fh:
        base64_data = request.json['image'].replace('data:image/png;base64,', '')
        fh.write(base64.b64decode(base64_data))

    return jsonify({})


@app.route("/save-as-binary/", methods=['POST'])
def binary_saver():
    filename = '{:10d}.png'.format(int(time()))  # generate some filename
    filepath = os.path.join(get_tmp_dir(), filename)
    request.files['image'].save(filepath)

    return jsonify({})


@app.route('/image/<path:filename>')
def get_file(filename):
    return send_from_directory(get_tmp_dir(), filename)

print("""
====================================================================
||                WELCOME TO  THE PAINTERRO DEMO                  ||
||  To make this work, please go to painterro root dir, and run"  ||
||                                                                ||
||    npm install                                                 ||
||    npm run dev                                                 ||
====================================================================
""")

app.run()



================================================
FILE: example/templates/common.html
================================================
<div>
  Painterro demo: <a href="/">Upload as base64 DEMO</a> | <a href="/bin/">Upload as binary DEMO</a> |
  <a href="/paste/">Paste to TinyMCE DEMO</a>
</div>

<div style="margin: 10px">
  <button aria-label='Open painterro' onclick='ptro.show()'>OPEN PAINTERRO</button>
</div>


================================================
FILE: example/templates/images_list.html
================================================
<div>
  {% for f in files %}
  <div class="img-browse">
    <a href="/image/{{f}}"><img src="/image/{{f}}"/></a>
  </div>
  {% endfor %}
</div>
<style>
  .img-browse {
    width: 600px;
    border: 1px solid gray;
    padding: 10px;
    margin: 10px;
  }

  .img-browse img {
    width: 100%;
  }
</style>

================================================
FILE: example/templates/paste_as_base64.html
================================================
<html>
  <head>
    <script src="http://localhost:8080/painterro.min.js"></script>
  </head>
  <body>
    {% include 'common.html' %}
    {% include 'images_list.html' %}
    <script>
      var ptro = Painterro({
        saveHandler: function (image, done) {
          var xhr = new XMLHttpRequest()
          xhr.open('POST', 'http://127.0.0.1:5000/save-as-base64/')
          xhr.setRequestHeader('Content-Type', 'application/json')
          xhr.send(JSON.stringify({
            image: image.asDataURL('image/png')
          }))
          xhr.onload = function (e) {
            done(true)
            window.location.reload()
          }
        }
      })
    </script>
  </body>
</html>

================================================
FILE: example/templates/paste_as_bin.html
================================================
<html>
<head>
  <script src="http://localhost:8080/painterro.min.js"></script>
</head>
<body>
  {% include 'common.html' %}
  {% include 'images_list.html' %}
  <script>
    var ptro = Painterro({
      saveHandler: function (image, done) {
        var formData = new FormData()
        formData.append('image', image.asBlob());

        var xhr = new XMLHttpRequest();
        xhr.open('POST', 'http://127.0.0.1:5000/save-as-binary/', true);
        xhr.onload = xhr.onerror = function () {
          done(true)
          window.location.reload()
        };
        xhr.send(formData)
      }
    })
  </script>
</body>
</html>

================================================
FILE: example/templates/paste_to_tinymce.html
================================================
<html>
  <script src="http://localhost:8080/painterro.min.js"></script>
  <script src="https://cloud.tinymce.com/stable/tinymce.min.js"></script>
  <script>
    tinymce.init({
      selector: 'textarea',
      plugins: [ 'fullscreen' ],
      toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table | fontsizeselect | painterro',
      height : "700",
      setup: function(editor) {
        editor.addButton("painterro", {
          title: "Open Painterro",
          cmd: "painterro_cmd",
          'icon': "painterro ptro-icon ptro-icon-painterro",
        });

        //button functionality.
        editor.addCommand("painterro_cmd", function () {
          ptro.show();
        });
      }
    });
  </script>
<body>
  {% include 'common.html' %}

  <textarea>Example content</textarea>

  <script>
    var ptro = Painterro({
      defaultSize: '500x400',
      saveHandler: function (image, done) {
        tinymce.activeEditor.execCommand('mceInsertContent',false, '<img src="' + image.asDataURL() + '" />');
        done(true)
      }
    })
  </script>
</body>
</html>

================================================
FILE: generate_font.js
================================================
const webfontsGenerator = require('@vusion/webfonts-generator');
const fs = require('fs');

fs.readdir('res/font', function(err, items) {
  if (err) {
    console.log('cant read res directory');
  }
  const files = items.filter((i) => i.toLowerCase().endsWith('.svg')).map(
    (i) => 'res/font/'+i);

  webfontsGenerator({
    files: files,
    dest: 'css/icons',
    fontName: 'ptroiconfont',
    
    html: true,
    // https://github.com/nfroidure/svgicons2svgfont
    normalize: true,
    round: 10e2,
    
    cssTemplate: 'res/font/font-css.hbs',
    templateOptions: {
      classPrefix: 'ptro-icon-',
      baseSelector: '.ptro-icon'
    },
    types: ['ttf', 'woff']
  }, function (error) {
    if (error) {
      console.log('Fail!', error);
    } else {
      console.log('Done!');
    }
  })

})




================================================
FILE: js/colorPicker.js
================================================
import { tr } from './translation';
import { KEYS } from './utils';

export function HexToRGB(hex) {
  let parse = /^#?([a-fA-F\d]{2})([a-fA-F\d]{2})([a-fA-F\d]{2})$/i.exec(hex);
  if (parse) {
    return {
      r: parseInt(parse[1], 16),
      g: parseInt(parse[2], 16),
      b: parseInt(parse[3], 16),
    };
  }
  parse = /^#?([a-fA-F\d])([a-fA-F\d])([a-fA-F\d])$/i.exec(hex);
  if (parse) {
    return {
      r: parseInt(parse[1].repeat(2), 16),
      g: parseInt(parse[2].repeat(2), 16),
      b: parseInt(parse[3].repeat(2), 16),
    };
  }
}

export function HexToRGBA(hex, alpha) {
  const rgb = HexToRGB(hex);
  return `rgba(${rgb.r},${rgb.g},${rgb.b},${alpha})`;
}

function format2Hex(val) {
  const hex = val.toString(16);
  return (hex.length === 1 && (`0${hex}`)) || hex;
}

export function rgbToHex(r, g, b) {
  return `#${format2Hex(r)}${format2Hex(g)}${format2Hex(b)}`;
}

function reversedColor(color) {
  const rgb = HexToRGB(color);
  const index = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000;
  return (index >= 128 && 'black') || 'white';
}

export default class ColorPicker {
  constructor(main, callback) {
    this.callback = callback;
    this.main = main;
    this.w = 180;
    this.h = 150;
    const w = this.w;
    const h = this.h;
    this.lightPosition = this.w - 1;
    this.wrapper = main.wrapper.querySelector('.ptro-color-widget-wrapper');
    this.input = main.wrapper.querySelector('.ptro-color-widget-wrapper .ptro-color');
    this.pipetteButton = main.wrapper.querySelector('.ptro-color-widget-wrapper button.ptro-pipette');
    this.closeButton = main.wrapper.querySelector('.ptro-color-widget-wrapper button.ptro-close-color-picker');
    this.canvas = main.wrapper.querySelector('.ptro-color-widget-wrapper canvas');
    this.ctx = this.canvas.getContext('2d');

    this.canvasLight = main.wrapper.querySelector('.ptro-color-widget-wrapper .ptro-canvas-light');
    this.colorRegulator = main.wrapper.querySelector('.ptro-color-widget-wrapper .ptro-color-light-regulator');

    this.canvasAlpha = main.wrapper.querySelector('.ptro-color-widget-wrapper .ptro-canvas-alpha');
    this.alphaRegulator = main.wrapper.querySelector('.ptro-color-widget-wrapper .ptro-color-alpha-regulator');

    this.ctxLight = this.canvasLight.getContext('2d');
    this.ctxAlpha = this.canvasAlpha.getContext('2d');
    this.canvas.setAttribute('width', `${w}`);
    this.canvas.setAttribute('height', `${h}`);
    this.canvasLight.setAttribute('width', `${w}`);
    this.canvasLight.setAttribute('height', `${20}`);
    this.canvasAlpha.setAttribute('width', `${w}`);
    this.canvasAlpha.setAttribute('height', `${20}`);
    const palette = this.ctx.createLinearGradient(0, 0, w, 0);
    palette.addColorStop(1 / 15, '#ff0000');
    palette.addColorStop(4 / 15, '#ffff00');
    palette.addColorStop(5 / 15, '#00ff00');
    palette.addColorStop(9 / 15, '#00ffff');
    palette.addColorStop(12 / 15, '#0000ff');
    palette.addColorStop(14 / 15, '#ff00ff');
    this.ctx.fillStyle = palette;
    this.ctx.fillRect(0, 0, w, h);

    const darkOverlay = this.ctx.createLinearGradient(0, 0, 0, h);
    darkOverlay.addColorStop(0, 'rgba(0, 0, 0, 0)');
    darkOverlay.addColorStop(0.99, 'rgba(0, 0, 0, 1)');
    darkOverlay.addColorStop(1, 'rgba(0, 0, 0, 1)');
    this.ctx.fillStyle = darkOverlay;
    this.ctx.fillRect(0, 0, w, h);

    this.closeButton.onclick = () => {
      this.close();
    };
    this.pipetteButton.onclick = () => {
      this.wrapper.setAttribute('hidden', 'true');
      this.opened = false;
      this.choosing = true;
    };

    this.input.onkeyup = () => {
      this.setActiveColor(this.input.value, true);
    };
  }

  open(state, addCallback) {
    this.target = state.target;
    this.palleteColor = state.palleteColor;
    this.alpha = state.alpha;
    this.lightPosition = this.lightPosition || this.w - 1;

    this.drawLighter();
    this.colorRegulator.style.left = `${this.lightPosition}px`;
    this.alphaRegulator.style.left = `${Math.round(this.alpha * this.w)}px`;
    this.regetColor();

    this.wrapper.removeAttribute('hidden');
    this.opened = true;
    this.addCallback = addCallback;
  }

  close() {
    this.wrapper.setAttribute('hidden', 'true');
    this.opened = false;
  }

  getPaletteColorAtPoint(e) {
    let x = e.clientX - this.canvas.documentOffsetLeft;
    let y = e.clientY - this.canvas.documentOffsetTop;
    x = (x < 1 && 1) || x;
    y = (y < 1 && 1) || y;
    x = (x > this.w && this.w - 1) || x;
    y = (y > this.h && this.h - 1) || y;
    const p = this.ctx.getImageData(x, y, 1, 1).data;
    this.palleteColor = rgbToHex(p[0], p[1], p[2]);
    this.drawLighter();
    this.regetColor();
  }

  regetColor() {
    const p = this.ctxLight.getImageData(this.lightPosition, 5, 1, 1).data;
    this.setActiveColor(rgbToHex(p[0], p[1], p[2]));
    this.drawAlpher();
  }

  regetAlpha() {
    const p = this.ctxAlpha.getImageData(this.alphaPosition, 5, 1, 1).data;
    this.alpha = p[3] / 255;
    this.setActiveColor(this.color, true);
  }

  getColorLightAtClick(e) {
    let x = e.clientX - this.canvasLight.documentOffsetLeft;
    x = (x < 1 && 1) || x;
    x = (x > this.w - 1 && this.w - 1) || x;
    this.lightPosition = x;
    this.colorRegulator.style.left = `${x}px`;
    this.regetColor();
  }

  getAlphaAtClick(e) {
    let x = e.clientX - this.canvasAlpha.documentOffsetLeft;
    x = (x < 1 && 1) || x;
    x = (x > this.w - 1 && this.w - 1) || x;
    this.alphaPosition = x;
    this.alphaRegulator.style.left = `${x}px`;
    this.regetAlpha();
  }

  handleKeyDown(event) {
    if (this.opened && event.keyCode === KEYS.enter) {
      return true; // mark as handled - user might expect doing save by enter
    }
    if (this.opened && event.keyCode === KEYS.esc) {
      this.close();
      return true;
    }
    return false;
  }

  handleMouseDown(e) {
    if (this.choosing && e.button !== 2) { // 0 - m1, 1 middle, 2-m2
      this.choosingActive = true;
      this.handleMouseMove(e);
      return true;
    }
    this.choosing = false;
    if (e.target === this.canvas) {
      this.selecting = true;
      this.getPaletteColorAtPoint(e);
    }
    if (e.target === this.canvasLight || e.target === this.colorRegulator) {
      this.lightSelecting = true;
      this.getColorLightAtClick(e);
    }
    if (e.target === this.canvasAlpha || e.target === this.alphaRegulator) {
      this.alphaSelecting = true;
      this.getAlphaAtClick(e);
    }
    return false;
  }

  handleMouseMove(e) {
    if (this.opened) {
      if (this.selecting) {
        this.getPaletteColorAtPoint(e);
      }
      if (this.lightSelecting) {
        this.getColorLightAtClick(e);
      }
      if (this.alphaSelecting) {
        this.getAlphaAtClick(e);
      }
    } else if (this.choosingActive) {
      const scale = this.main.getScale();
      let x = ((e.clientX - this.main.elLeft()) +
        this.main.scroller.scrollLeft) * scale;
      x = (x < 1 && 1) || x;
      x = (x > this.main.size.w - 1 && this.main.size.w - 1) || x;
      let y = ((e.clientY - this.main.elTop()) +
        this.main.scroller.scrollTop) * scale;
      y = (y < 1 && 1) || y;
      y = (y > this.main.size.h - 1 && this.main.size.h - 1) || y;
      const p = this.main.ctx.getImageData(x, y, 1, 1).data;
      const color = rgbToHex(p[0], p[1], p[2]);
      this.callback({
        alphaColor: HexToRGBA(color, 1),
        lightPosition: this.w - 1,
        alpha: 1,
        palleteColor: color,
        target: this.target,
      });
      if (this.addCallback !== undefined) {
        this.addCallback({
          alphaColor: HexToRGBA(color, 1),
          lightPosition: this.w - 1,
          alpha: 1,
          palleteColor: color,
          target: this.target,
        });
      }
    }
  }

  handleMouseUp() {
    this.selecting = false;
    this.lightSelecting = false;
    this.choosing = false;
    this.choosingActive = false;
    this.alphaSelecting = false;
    this.main.zoomHelper.hideZoomHelper();
  }

  setActiveColor(color, ignoreUpdateText) {
    try {
      this.input.style.color = reversedColor(color);
    } catch (e) {
      return;
    }
    this.input.style['background-color'] = color;
    if (ignoreUpdateText === undefined) {
      this.input.value = color;
    }
    this.color = color;
    this.alphaColor = HexToRGBA(color, this.alpha);
    if (this.callback !== undefined && this.opened) {
      this.callback({
        alphaColor: this.alphaColor,
        lightPosition: this.lightPosition,
        alpha: this.alpha,
        palleteColor: this.color,
        target: this.target,
      });
    }
    if (this.addCallback !== undefined && this.opened) {
      this.addCallback({
        alphaColor: this.alphaColor,
        lightPosition: this.lightPosition,
        alpha: this.alpha,
        palleteColor: this.color,
        target: this.target,
      });
    }
  }

  static html() {
    return '' +
      '<div class="ptro-color-widget-wrapper ptro-common-widget-wrapper ptro-v-middle" hidden>' +
        '<div class="ptro-pallet ptro-color-main ptro-v-middle-in">' +
          '<canvas></canvas>' +
          '<canvas class="ptro-canvas-light"></canvas>' +
          '<span class="ptro-color-light-regulator ptro-bordered-control"></span>' +
          '<canvas class="ptro-canvas-alpha"></canvas>' +
          '<span class="alpha-checkers"></span>' +
          '<span class="ptro-color-alpha-regulator ptro-bordered-control"></span>' +
          '<div class="ptro-colors"></div>' +
          '<div class="ptro-color-edit">' +
            '<button type="button" aria-label="pipette" class="ptro-icon-btn ptro-pipette ptro-color-control" style="float: left; margin-right: 5px">' +
              '<i class="ptro-icon ptro-icon-pipette"></i>' +
            '</button>' +
            '<input class="ptro-input ptro-color" type="text" size="7"/>' +
            '<button type="button" aria-label="close" class="ptro-named-btn ptro-close-color-picker ptro-color-control" >' +
            `${tr('close')}</button>` +
          '</div>' +
        '</div>' +
      '</div>';
  }

  drawLighter() {
    const lightGradient = this.ctxLight.createLinearGradient(0, 0, this.w, 0);
    lightGradient.addColorStop(0, '#ffffff');
    lightGradient.addColorStop(0.05, '#ffffff');
    lightGradient.addColorStop(0.95, this.palleteColor);
    lightGradient.addColorStop(1, this.palleteColor);
    this.ctxLight.fillStyle = lightGradient;
    this.ctxLight.fillRect(0, 0, this.w, 15);
  }

  drawAlpher() {
    this.ctxAlpha.clearRect(0, 0, this.w, 15);
    const lightGradient = this.ctxAlpha.createLinearGradient(0, 0, this.w, 0);
    lightGradient.addColorStop(0, 'rgba(255,255,255,0)');
    lightGradient.addColorStop(0.05, 'rgba(255,255,255,0)');
    lightGradient.addColorStop(0.95, this.color);
    lightGradient.addColorStop(1, this.color);
    this.ctxAlpha.fillStyle = lightGradient;
    this.ctxAlpha.fillRect(0, 0, this.w, 15);
  }
}


================================================
FILE: js/controlbuilder.js
================================================
import { setParam } from './params';

export default class ControlBuilder {
  constructor(main) {
    this.main = main;
  }

  buildFontSizeControl(controlIndex) {
    const action = () => {
      const fontSize =
        this.main.getElemByIdSafe(this.main.activeTool.controls[controlIndex].id).value;
      this.main.textTool.setFontSize(fontSize);
      setParam('defaultFontSize', fontSize);
    };
    const getValue = () => this.main.textTool.fontSize;

    if (this.main.params.availableFontSizes) {
      return ControlBuilder.buildDropDownControl('fontSize', action, getValue, this.main.params.availableFontSizes);
    }
    return ControlBuilder.buildInputControl('fontSize', action, getValue, 1, 200);
  }

  buildEraserWidthControl(controlIndex) {
    const action = () => {
      const width = this.main.getElemByIdSafe(this.main.activeTool.controls[controlIndex].id).value;
      this.main.primitiveTool.setEraserWidth(width);
      setParam('defaultEraserWidth', width);
    };
    const getValue = () => this.main.primitiveTool.eraserWidth;

    if (this.main.params.availableEraserWidths) {
      return ControlBuilder.buildDropDownControl('eraserWidth', action, getValue, this.main.params.availableEraserWidths);
    }
    return ControlBuilder.buildInputControl('eraserWidth', action, getValue, 1, 99);
  }

  buildLineWidthControl(controlIndex) {
    const action = () => {
      const width = this.main.getElemByIdSafe(this.main.activeTool.controls[controlIndex].id).value;
      this.main.primitiveTool.setLineWidth(width);
      setParam('defaultLineWidth', width);
    };
    const getValue = () => this.main.primitiveTool.lineWidth;

    if (this.main.params.availableLineWidths) {
      return ControlBuilder.buildDropDownControl('lineWidth', action, getValue, this.main.params.availableLineWidths);
    }
    return ControlBuilder.buildInputControl('lineWidth', action, getValue, 0, 99);
  }

  buildShadowOnControl(controlIndex) {
    return {
      type: 'bool',
      title: 'shadowOn',
      titleFull: 'shadowOnFull',
      target: 'shadowOn',
      action: () => {
        const btn = this.main.getElemByIdSafe(this.main.activeTool.controls[controlIndex].id);
        const state = !(btn.getAttribute('data-value') === 'true');
        this.main.primitiveTool.setShadowOn(state);
        btn.setAttribute('data-value', state ? 'true' : 'false');
        setParam('defaultPrimitiveShadowOn', state);
      },
      getValue: () => this.main.primitiveTool.shadowOn,
    };
  }

  buildPaintBucketControl(controlIndex) {
    const action = () => {
      const width = document.getElementById(this.main.activeTool.controls[controlIndex].id).value;
      console.log('buildPaintBucketControl width: ' + width);
      // this.main.primitiveTool.setLineWidth(width);
      setParam('activeFillColor', width);
    };
    const getValue = () => this.main.primitiveTool.lineWidth;

    return ControlBuilder.buildInputControl('lineWidth', action, getValue, 1, 99);
  }

  buildArrowLengthControl(controlIndex) {
    const action = () => {
      const width = this.main.getElemByIdSafe(this.main.activeTool.controls[controlIndex].id).value;
      this.main.primitiveTool.setArrowLength(width);
      setParam('defaultArrowLength', width);
    };
    const getValue = () => this.main.primitiveTool.arrowLength;

    if (this.main.params.availableArrowLengths) {
      return ControlBuilder.buildDropDownControl('arrowLength', action, getValue, this.main.params.availableArrowLengths);
    }
    return ControlBuilder.buildInputControl('arrowLength', action, getValue, 1, 99);
  }

  static buildInputControl(name, action, getValue, minVal, maxVal) {
    return {
      type: 'int',
      title: name,
      titleFull: `${name}Full`,
      target: name,
      min: minVal,
      max: maxVal,
      action,
      getValue,
    };
  }

  static buildDropDownControl(name, action, getValue, availableValues) {
    return {
      type: 'dropdown',
      title: name,
      titleFull: `${name}Full`,
      target: name,
      action,
      getValue,
      getAvailableValues: () => availableValues.map(
        x => ({ value: x, name: x.toString(), title: x.toString() })),
    };
  }
}



================================================
FILE: js/customEvents.js
================================================

export default class CustomEvents {
  constructor(element) {
    this.element = element;
  }
  createEvent(name, detail) {
    return new CustomEvent(name, { detail });
  }
  dispatchEvent(name, detail) {
    this.element.dispatchEvent(this.createEvent(name, detail));
  }
  addEventListener(name, callback) {
    this.element.addEventListener(name, callback);
  }

  removeEventListener(name, callback) {
    this.element.removeEventListener(name, callback);
  }

  useCustomEvent(name, details) {
    this.dispatchEvent(name, details);
  }



  
}   



================================================
FILE: js/filters.js
================================================
import { filter } from 'lodash';
import { tr } from './translation.js';
import { KEYS } from './utils.js';

export default class Filters {
    constructor(main) {
        this.main = main;
    
        this.wrapper = main.wrapper.querySelector('.ptro-filters-widget-wrapper');
        this.input = main.wrapper.querySelector('.ptro-filters-widget-wrapper .ptro-filters-input');
        this.applyButton = main.wrapper.querySelector('.ptro-filters-widget-wrapper button.ptro-apply');
        this.closeButton = main.wrapper.querySelector('.ptro-filters-widget-wrapper button.ptro-close');
        this.errorHolder = main.wrapper.querySelector('.ptro-filters-widget-wrapper .ptro-error');
        this.filters = ['grayscale', 'sepia', 'invert', 'brightness', 'contrast',  'saturate', ];
        this.chosenFilter = this.filters[0];
        this.initCtxImageData = null;
        this.filterValue = 0;
        this.filtersForApply = this.createFiltersForApply()
        
    }

    createFiltersForApply() {
        const filtersForApply = {};
        this.filters.forEach((f) => {
            filtersForApply[f] = 0;
        });
        return filtersForApply;
    }

    createFilterString() {
        let filterString = '';
        for (const key in this.filtersForApply) {
            if (this.filtersForApply[key] !== 0) {
                filterString += `${key}(${this.filtersForApply[key]}%) `;
            }
        }
        return filterString;
    }

    getFilters() {
        return this.filters.map((f)=>{
            return {
                value: f,
                name: f,
                title: f,
            };
        });
    }
    setFilter(filter) {
        this.chosenFilter = filter;
        if (this.filtersForApply.hasOwnProperty(filter)) {
           this.filtersForApply[filter] = this.filterValue;
        //    console.log('filtersForApply', this.filtersForApply)
        }
    }

    

    setPercents(value) {
        this.filterValue = value;
        if (this.filtersForApply.hasOwnProperty(this.chosenFilter)) {
            this.filtersForApply[this.chosenFilter] = value;
            // console.log('filtersForApply', this.filtersForApply)

        }
    }

    getFilter() {
        const filterName = this.filters[0];
        return filterName 
    }

    applyFilter() {
        const w = this.main.size.w;
        const h = this.main.size.h;
        const tmpData = this.initCtxImageData;
        const tmpCan = this.main.doc.createElement('canvas');
        tmpCan.width = w;
        tmpCan.height = h;
        tmpCan.getContext('2d').putImageData(tmpData, 0, 0);
        this.main.ctx.save();
        this.main.ctx.filter = this.createFilterString();
        this.main.ctx.drawImage(tmpCan, 0, 0);
        // this.adjustSizeFull();
        // this.ctx.restore();
        this.main.worklog.captureState();
        // this.main.closeActiveTool();
    }

    saveInitImg() {
        this.initCtxImageData = this.main.ctx.getImageData(0, 0, this.main.size.w, this.main.size.h);
        this.createFiltersForApply();
    }

    open() {
        this.wrapper.removeAttribute('hidden');
        this.opened = true;
    }

    close() {
        this.wrapper.setAttribute('hidden', '');
        this.opened = false;
    }

    startClose() {
        this.errorHolder.setAttribute('hidden', '');
        this.main.closeActiveTool();
    }
    getValue(){
        return
    }

    static html (main){
        return '' +
        '<div class="ptro-filters-widget-wrapper ptro-common-widget-wrapper ptro-v-middle" hidden>' +
        '   <div class="ptro-filters-widget-content ptro-common-widget-content">' +
        '       <div class="ptro-filters-widget-title">' + tr('filters') + '</div>' +
        '       <div class="ptro-filters-widget-input-wrapper">' +
        '           <div class="ptro-error ptro-error-hidden">' + tr('wrongFilterValue') + '</div>' + 
        '       </div>' +
        '       <div class="ptro-filters-widget-buttons">' +
        '           <button type="button" class="ptro-apply ptro-color-control">' + tr('apply') + '</button>' +
        '           <button type="button" class="ptro-close ptro-color-control">' + tr('close') + '</button>' +
        '       </div>' +
        '   </div>' +
        '</div>';
    }
   
    }

================================================
FILE: js/inserter.js
================================================
import { tr } from './translation';
import { genId, KEYS, imgToDataURL } from './utils';

export default class Inserter {
  constructor(main) {
    this.main = main;
    const extendObj = {
      extend_top: {
        internalName: 'extend_top',
        handle: (img) => {
          this.tmpImg = img;
          const oldH = this.main.size.h;
          const oldW = this.main.size.w;
          const newH = oldH + img.naturalHeight;
          const newW = Math.max(oldW, img.naturalWidth);
          const tmpData = this.ctx.getImageData(0, 0, this.main.size.w, this.main.size.h);
          this.main.resize(newW, newH);
          this.main.clearBackground();
          this.ctx.putImageData(tmpData, 0, img.naturalHeight);
          this.main.adjustSizeFull();
          if (img.naturalWidth < oldW) {
            const offset = Math.round((oldW - img.naturalWidth) / 2);
            this.main.select.placeAt(offset, 0, offset, oldH, img);
          } else {
            this.main.select.placeAt(0, 0, 0, oldH, img);
          }
          this.worklog.captureState();
        },
      },
      extend_left: {
        internalName: 'extend_left',
        handle: (img) => {
          this.tmpImg = img;
          const oldH = this.main.size.h;
          const oldW = this.main.size.w;
          const newW = oldW + img.naturalWidth;
          const newH = Math.max(oldH, img.naturalHeight);
          const tmpData = this.ctx.getImageData(0, 0, this.main.size.w, this.main.size.h);
          this.main.resize(newW, newH);
          this.main.clearBackground();
          this.ctx.putImageData(tmpData, img.naturalWidth, 0);
          this.main.adjustSizeFull();
          if (img.naturalHeight < oldH) {
            const offset = Math.round((oldH - img.naturalHeight) / 2);
            this.main.select.placeAt(0, offset, oldW, offset, img);
          } else {
            this.main.select.placeAt(0, 0, oldW, 0, img);
          }
          this.worklog.captureState();
        },
      },
      extend_right: {
        internalName: 'extend_right',
        handle: (img) => {
          this.tmpImg = img;
          const oldH = this.main.size.h;
          const oldW = this.main.size.w;
          const newW = oldW + img.naturalWidth;
          const newH = Math.max(oldH, img.naturalHeight);
          const tmpData = this.ctx.getImageData(0, 0, this.main.size.w, this.main.size.h);
          this.main.resize(newW, newH);
          this.main.clearBackground();
          this.ctx.putImageData(tmpData, 0, 0);
          this.main.adjustSizeFull();
          if (img.naturalHeight < oldH) {
            const offset = Math.round((oldH - img.naturalHeight) / 2);
            this.main.select.placeAt(oldW, offset, 0, offset, img);
          } else {
            this.main.select.placeAt(oldW, 0, 0, 0, img);
          }
          this.worklog.captureState();
        },
      },
      extend_down: {
        internalName: 'extend_down',
        handle: (img) => {
          this.tmpImg = img;
          const oldH = this.main.size.h;
          const oldW = this.main.size.w;
          const newH = oldH + img.naturalHeight;
          const newW = Math.max(oldW, img.naturalWidth);
          const tmpData = this.ctx.getImageData(0, 0, this.main.size.w, this.main.size.h);
          this.main.resize(newW, newH);
          this.main.clearBackground();
          this.ctx.putImageData(tmpData, 0, 0);
          this.main.adjustSizeFull();
          if (img.naturalWidth < oldW) {
            const offset = Math.round((oldW - img.naturalWidth) / 2);
            this.main.select.placeAt(offset, oldH, offset, 0, img);
          } else {
            this.main.select.placeAt(0, oldH, 0, 0, img);
          }
          this.worklog.captureState();
        },
      },
    };
    const fitObj = {
      replace_all: {
        internalName: 'fit',
        handle: (img) => {
          if (this.main.params.backplateImgUrl) {
            this.main.params.backplateImgUrl = undefined;
            this.main.tabelCell.style.background = '';
            this.main.canvas.style.backgroundColor = `${this.main.params.backgroundFillColor}ff`;
            this.pasteOptions = Object.assign({}, fitObj, extendObj);
            this.activeOption = this.pasteOptions;
            this.main.wrapper.querySelector('.ptro-paster-select-wrapper').remove();
            this.main.wrapper.insertAdjacentHTML('beforeend', this.html());
            this.init(main);
          }
          this.main.fitImage(img, this.mimetype);
        },
      },
      paste_over: {
        internalName: 'over',
        handle: (img) => {
          this.tmpImg = img;
          const oldH = this.main.size.h;
          const oldW = this.main.size.w;
          if (img.naturalHeight <= oldH && img.naturalWidth <= oldW) {
            this.main.select.placeAt(
              0, 0,
              oldW - img.naturalWidth,
              oldH - img.naturalHeight, img);
          } else if (img.naturalWidth / img.naturalHeight > oldW / oldH) {
            const newH = oldW * (img.naturalHeight / img.naturalWidth);
            this.main.select.placeAt(0, 0, 0, oldH - newH, img);
          } else {
            const newW = oldH * (img.naturalWidth / img.naturalHeight);
            this.main.select.placeAt(0, 0, oldW - newW, 0, img);
          }
          this.worklog.captureState();
        },
      },
    };
    if (this.main.params.backplateImgUrl) {
      this.pasteOptions = Object.assign({}, fitObj);
      this.activeOption = this.pasteOptions;
      return;
    }
    this.pasteOptions = Object.assign({}, fitObj, extendObj);
    this.activeOption = this.pasteOptions;
  }

  init(main) {
    this.CLIP_DATA_MARKER = 'painterro-image-data';
    this.ctx = main.ctx;
    this.main = main;
    this.worklog = main.worklog;
    this.selector = main.wrapper.querySelector('.ptro-paster-select-wrapper');
    this.cancelChoosing();
    this.img = null;
    this.mimetype = null; // mime of pending image
    this.getAvailableOptions().forEach((k) => {
      const o = this.pasteOptions[k];
      this.main.getElemByIdSafe(o.id).onclick = () => {
        if (this.loading) {
          this.doLater = o.handle;
        } else {
          o.handle(this.img);
        }
        this.cancelChoosing();
      };
    });
    this.loading = false;
    this.doLater = null;
  }

  insert(x, y, w, h) {
    this.main.ctx.drawImage(this.tmpImg, x, y, w, h);
    this.main.worklog.reCaptureState();
  }

  cancelChoosing() {
    this.selector.setAttribute('hidden', '');
    this.waitChoice = false;
  }

  loaded(img, mimetype) {
    this.img = img;
    this.mimetype = mimetype;
    this.loading = false;
    if (this.doLater) {
      this.doLater(img);
      this.doLater = null;
    }
  }

  getAvailableOptions() {
    if (this.main.params.how_to_paste_actions) {
      // filter out only to selected
      return Object.keys(this.activeOption).filter(
        actionName => this.main.params.how_to_paste_actions.includes(actionName),
      );
    }
    return Object.keys(this.activeOption);
  }

  handleOpen(src, mimetype) {
    this.startLoading();
    const handleIt = (source) => {
      const img = new Image();
      const empty = this.main.worklog.clean;
      const replaceAllImmediately = empty && this.main.params.replaceAllOnEmptyBackground;
      img.onload = () => {
        if (replaceAllImmediately) {
          this.main.fitImage(img, mimetype);
        } else {
          this.loaded(img, mimetype);
        }
        this.finishLoading();
      };
      img.onerror = () => {
        if (typeof this.main.params.onImageFailedOpen === 'function') {
          this.main.params.onImageFailedOpen();
        }
      };
      // img.crossOrigin = '*'; TODO: try to identify CORS issues earlier?
      img.src = source;
      if (!replaceAllImmediately) {
        const availableOptions = this.getAvailableOptions();
        if (availableOptions.length !== 1) {
          this.selector.removeAttribute('hidden');
          this.waitChoice = true;
        } else {
          this.doLater = this.activeOption[availableOptions[0]].handle;
        }
      }
    };

    if (src.indexOf('data') !== 0) {
      imgToDataURL(src, (dataUrl) => { // if CORS will not allow,
        // better see error in console than have different canvas mode
        handleIt(dataUrl);
      }, () => {
        if (typeof this.main.params.onImageFailedOpen === 'function') {
          this.main.params.onImageFailedOpen();
        }
      });
    } else {
      handleIt(src);
    }
  }

  handleKeyDown(evt) {
    if (this.waitChoice && evt.keyCode === KEYS.esc) {
      this.cancelChoosing();
      return true;
    }
    if (this.waitChoice && event.keyCode === KEYS.enter) {
      return true; // mark as handled - user might expect doing save by enter
    }
    return false;
  }

  startLoading() {
    this.loading = true;
    if (this.main.toolByName.open.buttonId) {
      const btn = this.main.getElemByIdSafe(this.main.toolByName.open.buttonId);
      if (btn) {
        btn.setAttribute('disabled', 'true');
      }
      const icon = this.main.doc.querySelector(`#${this.main.toolByName.open.buttonId} > i`);
      if (icon) {
        icon.className = 'ptro-icon ptro-icon-loading ptro-spinning';
      }
    }
  }

  finishLoading() {
    if (this.main.toolByName.open.buttonId) {
      const btn = this.main.getElemByIdSafe(this.main.toolByName.open.buttonId);
      if (btn) {
        btn.removeAttribute('disabled');
      }
      const icon = this.main.doc.querySelector(`#${this.main.toolByName.open.buttonId} > i`);
      if (icon) {
        icon.className = 'ptro-icon ptro-icon-open';
      }
    }
    if (this.main.params.onImageLoaded) {
      this.main.params.onImageLoaded();
    }
  }

  static get(main) {
    if (main.inserter) {
      return main.inserter;
    }
    main.inserter = new Inserter(main);
    return main.inserter;
  }

  static controlObjToString(o, btnClassName = '') {
    const tempObj = o;
    tempObj.id = genId();
    return `<button type="button" id="${o.id}" class="ptro-selector-btn ptro-color-control ${btnClassName}">` +
    `<div><i class="ptro-icon ptro-icon-paste_${o.internalName}"></i></div>` +
    `<div>${tr(`pasteOptions.${o.internalName}`)}</div>` +
    '</button>';
  }

  html() {
    const bcklOptions = this.main.params.backplateImgUrl;
    let fitControls = '';
    let extendControls = '';
    this.getAvailableOptions().forEach((k) => {
      if (k === 'replace_all' || k === 'paste_over') {
        fitControls += `<div class="ptro-paster-fit">
          ${Inserter.controlObjToString(this.pasteOptions[k], 'ptro-selector-fit')}
        <div class="ptro-paster-wrapper-label">
          ${tr(`pasteOptions.${this.pasteOptions[k].internalName}`)}
        </div></div>`;
      } else {
        extendControls += Inserter.controlObjToString(this.pasteOptions[k], 'ptro-selector-extend');
      }
    });
    return '<div class="ptro-paster-select-wrapper" hidden><div class="ptro-paster-select ptro-v-middle">' +
      '<div class="ptro-in ptro-v-middle-in">' +
      ` <div class="ptro-paster-wrappers-fits">
        ${fitControls}
        ${bcklOptions || !extendControls ? '' : `
          <div class="ptro-paster-select-wrapper-extends">
            <div class="ptro-paster-extends-items">
              ${extendControls}
            </div>
            <div class="ptro-paster-wrapper-label">${tr('pasteOptions.extend')}</div>
          </div>`}
        </div>
      </div></div></div>`;
  }
}



================================================
FILE: js/main.js
================================================
import isMobile from 'ismobilejs';

import '../css/styles.css';
import '../css/bar-styles.css';
import '../css/icons/ptroiconfont.css';

import PainterroSelecter from './selecter';
import WorkLog from './worklog';
import { genId, addDocumentObjectHelpers, KEYS, trim,
  getScrollbarWidth, distance, logError,setPrimitiveToolValue } from './utils';
import PrimitiveTool from './primitive';
import ColorPicker, { HexToRGB, rgbToHex } from './colorPicker';
import { setDefaults, setParam } from './params';
import { tr } from './translation';
import ZoomHelper from './zoomHelper';
import TextTool from './text';
import Resizer from './resizer';
import Inserter from './inserter';
import Settings from './settings';
import ControlBuilder from './controlbuilder';
import PaintBucket from './paintBucket';
import Filters from './filters';
import CustomEvents from './customEvents';
import { set } from 'lodash';

class PainterroProc {
  constructor(params) {
    const element =document.querySelector(`#${params.id}`) || document.getElementById('app');
    this.customEvents = new CustomEvents(element);
    addDocumentObjectHelpers();

    this.getElemByIdSafe = (id) => {
      if (!id) {
        throw new Error(`Can't get element with id=${id}, please create an issue here, we will easily fx it: https://github.com/devforth/painterro/issues/`);
      }
      return document.getElementById(id);
    };

    this.tools = [{
      name: 'select',
      hotkey: 's',
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.toolContainer.style.cursor = 'crosshair';
        this.select.activate();
        this.select.draw();
      },
      close: () => {
        this.select.close();
        this.toolContainer.style.cursor = 'auto';
      },
      eventListner: () => this.select,
    }, {
      name: 'crop',
      hotkey: 'c',
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.select.doCrop();
        this.closeActiveTool();
      },
    }, {
      name: 'pixelize',
      hotkey: 'p',
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.select.doPixelize();
        this.closeActiveTool();
      },
    }, {
      name: 'line',
      hotkey: 'l',
      controls: [
        () => ({
          type: 'color',
          title: 'lineColor',
          target: 'line',
          titleFull: 'lineColorFull',
          action: () => {
            this.colorPicker.open(this.colorWidgetState.line);
          },
        }),
        () => this.controlBuilder.buildLineWidthControl(1),
        () => this.controlBuilder.buildShadowOnControl(2),
      ],
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.toolContainer.style.cursor = 'crosshair';
        this.primitiveTool.activate('line');
      },
      eventListner: () => this.primitiveTool,
    }, {
      name: 'arrow',
      hotkey: 'a',
      controls: [
        () => ({
          type: 'color',
          title: 'lineColor',
          target: 'line',
          titleFull: 'lineColorFull',
          action: () => {
            this.colorPicker.open(this.colorWidgetState.line);
          },
        }),
        () => this.controlBuilder.buildArrowLengthControl(1),
        () => this.controlBuilder.buildShadowOnControl(2),
      ],
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.toolContainer.style.cursor = 'crosshair';
        this.primitiveTool.activate('arrow');
      },
      eventListner: () => this.primitiveTool,
    }, {
      name: 'rect',
      controls: [
        () => ({
          type: 'color',
          title: 'lineColor',
          titleFull: 'lineColorFull',
          target: 'line',
          action: () => {
            this.colorPicker.open(this.colorWidgetState.line);
          },
        }),
        () => ({
          type: 'color',
          title: 'fillColor',
          titleFull: 'fillColorFull',
          target: 'fill',
          action: () => {
            this.colorPicker.open(this.colorWidgetState.fill);
          },
        }),
        () => this.controlBuilder.buildLineWidthControl(2),
        () => this.controlBuilder.buildShadowOnControl(3),
      ],
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.toolContainer.style.cursor = 'crosshair';
        this.primitiveTool.activate('rect');
      },
      eventListner: () => this.primitiveTool,
    }, {
      name:'filters',
      controls: [
        () => ({
          type: 'dropdown',
          title: 'filters',
          titleFull: 'imageFilters',
          action: () => {
            const dropdown = this.activeTool.controls[0].id
            const value = this.getElemByIdSafe(dropdown).value;
            this.filters.setFilter(value);
          },
          getValue: () => this.filters.getFilter(),
          getAvailableValues: () => this.filters.getFilters(),
        }),
        ()=>(
          {
            type: 'int',
            title: 'percents',
            titleFull: 'percentsFull',
            min: 0,
            max: 100,
            options: {eventOnChange: true},
            action: () => {
              const input = this.getElemByIdSafe(this.activeTool.controls[1].id);
              const value = input.value;
              this.filters.setPercents(value);
              this.filters.applyFilter();
            },
            getValue: () => 1,
          }
        )
      ],
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.filters.saveInitImg()
        this.toolContainer.style.cursor = 'crosshair';
      }
    }, 
    {
      name: 'ellipse',
      controls: [
        () => ({
          type: 'color',
          title: 'lineColor',
          titleFull: 'lineColorFull',
          target: 'line',
          action: () => {
            this.colorPicker.open(this.colorWidgetState.line);
          },
        }),
        () => ({
          type: 'color',
          title: 'fillColor',
          titleFull: 'fillColorFull',
          target: 'fill',
          action: () => {
            this.colorPicker.open(this.colorWidgetState.fill);
          },
        }),
        () => this.controlBuilder.buildLineWidthControl(2),
        () => this.controlBuilder.buildShadowOnControl(3),
      ],
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.toolContainer.style.cursor = 'crosshair';
        this.primitiveTool.activate('ellipse');
      },
      eventListner: () => this.primitiveTool,
    }, {
      name: 'brush',
      hotkey: 'b',
      controls: [
        () => ({
          type: 'color',
          title: 'lineColor',
          target: 'line',
          titleFull: 'lineColorFull',
          action: () => {
            this.colorPicker.open(this.colorWidgetState.line);
          },
        }),
        () => this.controlBuilder.buildLineWidthControl(1),
      ],
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.toolContainer.style.cursor = 'crosshair';
        this.primitiveTool.activate('brush');
      },
      eventListner: () => this.primitiveTool,
    }, {
      name: 'eraser',
      controls: [
        () => this.controlBuilder.buildEraserWidthControl(0),
      ],
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.toolContainer.style.cursor = 'crosshair';
        this.primitiveTool.activate('eraser');
      },
      eventListner: () => this.primitiveTool,
    }, {
      name: 'text',
      hotkey: 't',
      controls: [
        () => ({
          type: 'color',
          title: 'textColor',
          titleFull: 'textColorFull',
          target: 'line',
          action: () => {
            this.colorPicker.open(this.colorWidgetState.line, (c) => {
              this.textTool.setFontColor(c.alphaColor);
            });
          },
        }),
        () =>this.controlBuilder.buildFontSizeControl(1),
        () => ({
          type: 'dropdown',
          title: 'fontName',
          titleFull: 'fontNameFull',
          target: 'fontName',
          action: () => {
            const dropdown = this.getElemByIdSafe(this.activeTool.controls[2].id);
            const font = dropdown.value;
            this.textTool.setFont(font);
          },
          getValue: () => this.textTool.getFont(),
          getAvailableValues: () => this.textTool.getFonts(),
        }),
        () => ({
          type: 'bool',
          title: 'fontIsBold',
          titleFull: 'fontIsBoldFull',
          target: 'fontIsBold',
          action: () => {
            const btn = this.getElemByIdSafe(this.activeTool.controls[3].id);
            const state = !(btn.getAttribute('data-value') === 'true');
            this.textTool.setFontIsBold(state);
            setParam('defaultFontBold', state);
            btn.setAttribute('data-value', state ? 'true' : 'false'); // invert
          },
          getValue: () => this.textTool.isBold,
        }),
        () => ({
          type: 'bool',
          title: 'fontIsItalic',
          titleFull: 'fontIsItalicFull',
          target: 'fontIsItalic',
          action: () => {
            const btn = this.getElemByIdSafe(this.activeTool.controls[4].id);
            const state = !(btn.getAttribute('data-value') === 'true'); // invert
            this.textTool.setFontIsItalic(state);
            setParam('defaultFontItalic', state);
            btn.setAttribute('data-value', state ? 'true' : 'false');
          },
          getValue: () => this.textTool.isItalic,
        }),
        () => ({
          type: 'bool',
          title: 'fontStrokeAndShadow',
          titleFull: 'fontStrokeAndShadowFull',
          target: 'fontStrokeAndShadow',
          action: () => {
            const btn = this.getElemByIdSafe(this.activeTool.controls[5].id);
            const nextState = !(btn.getAttribute('data-value') === 'true');
            this.textTool.setStrokeOn(nextState);
            setParam('defaultTextStrokeAndShadow', nextState);
            btn.setAttribute('data-value', nextState ? 'true' : 'false');
          },
          getValue: () => this.textTool.strokeOn,
        }),
      ],
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.textTool.setFontColor(this.colorWidgetState.line.alphaColor);
        // this.textTool.setStrokeColor(this.colorWidgetState.stroke.alphaColor);
        this.toolContainer.style.cursor = 'crosshair';
      },
      close: () => {
        this.textTool.close();
      },
      eventListner: () => this.textTool,
    }, {
      name: 'rotate',
      hotkey: 'r',
      activate: () => {
        if (this.initText) {
          this.wrapper.click();
        }
        const w = this.size.w;
        const h = this.size.h;
        const tmpData = this.ctx.getImageData(0, 0, this.size.w, this.size.h);
        const tmpCan = this.doc.createElement('canvas');
        tmpCan.width = w;
        tmpCan.height = h;
        tmpCan.getContext('2d').putImageData(tmpData, 0, 0);
        this.resize(h, w);
        this.ctx.save();
        this.ctx.translate(h / 2, w / 2);
        this.ctx.rotate((90 * Math.PI) / 180);
        this.ctx.drawImage(tmpCan, -w / 2, -h / 2);
        this.adjustSizeFull();
        this.ctx.restore();
        this.worklog.captureState();
        this.closeActiveTool();
      },
    }, {
      name: 'resize',
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.resizer.open();
      },
      close: () => {
        this.resizer.close();
      },
      eventListner: () => this.resizer,
    },
    {
      name: 'undo',
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.worklog.undoState();
      },
      eventListner: () => this.resizer,
    },
    {
      name: 'redo',
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.worklog.redoState();
      },
      eventListner: () => this.resizer,
    },
    {
      name: 'settings',
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.settings.open();
      },
      close: () => {
        this.settings.close();
      },
      eventListner: () => this.settings,
    },
    {
      name: 'zoomout',
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.zoomButtonActive = true;
        const canvas = this.canvas;
        const gbr = canvas.getBoundingClientRect();
        const e = {
          wheelDelta: -120,
          clientX: gbr.right / 2,
          clientY: gbr.bottom / 2,
        };

        this.curCord = [
          (e.clientX - this.elLeft()) + this.scroller.scrollLeft,
          (e.clientY - this.elTop()) + this.scroller.scrollTop,
        ];

        const scale = this.getScale();
        this.curCord = [this.curCord[0] * scale, this.curCord[1] * scale];

        this.zoomImage(e);
      },
    },
    {
      name: 'zoomin',
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.zoomButtonActive = true;
        const canvas = this.canvas;
        const gbr = canvas.getBoundingClientRect();
        const e = {
          wheelDelta: 120,
          clientX: gbr.right / 2,
          clientY: gbr.bottom / 2,
        };

        this.curCord = [
          (e.clientX - this.elLeft()) + this.scroller.scrollLeft,
          (e.clientY - this.elTop()) + this.scroller.scrollTop,
        ];

        const scale = this.getScale();
        this.curCord = [this.curCord[0] * scale, this.curCord[1] * scale];

        this.zoomImage(e);
      },
    },

    {
      name: 'bucket',
      hotkey: 'f',
      controls: [
        () => ({
          type: 'color',
          title: 'fillColor',
          target: 'fill',
          titleFull: 'fillColorFull',
          action: () => {
            this.colorPicker.open(this.colorWidgetState.fill);
          },
        }),
      ],
      activate: () => {
        // this.clear();
        // this.closeActiveTool();
        this.toolContainer.style.cursor = 'crosshair';
        this.primitiveTool.activate('bucket');
      },
      eventListner: () => this.paintBucket,
    },

    {
      name: 'clear',
      activate: () => {
        this.clear();
        this.closeActiveTool();
      },
    },

    {
      name: 'save',
      right: true,
      hotkey: () => this.params.saveByEnter ? 'enter' : false,
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.save();
        this.closeActiveTool();
      },
    }, {
      name: 'open',
      right: true,
      activate: () => {
        if (this.initText) this.wrapper.click();
        this.closeActiveTool();
        const input = this.getElemByIdSafe(this.fileInputId);
        input.onchange = (event) => {
          const files = event.target.files || event.dataTransfer.files;
          if (!files.length) {
            return;
          }
          this.openFile(files[0]);
          input.value = ''; // to allow reopen
        };
        input.click();
      },
    }, {
      name: 'close',
      hotkey: () => this.params.hideByEsc ? 'esc' : false,
      right: true,
      activate: () => {
        if (this.initText) this.wrapper.click();
        const doClose = () => {
          this.closeActiveTool();
          this.close();
          this.hide();
        };

        if (this.params.onBeforeClose) {
          this.params.onBeforeClose(this.hasUnsaved, doClose);
        } else {
          doClose();
        }
      },
    },...params.customTools?.map((ct)=>{return {name:ct.name,activate:ct.callBack,iconUrl:ct.iconUrl}}) || []];

    this.params = setDefaults(params, this.tools.map(t => t.name));    
    
    this.colorWidgetState = {
      line: {
        target: 'line',
        palleteColor: this.params.activeColor,
        alpha: this.params.activeColorAlpha,
        alphaColor: this.params.activeAlphaColor,
      },
      fill: {
        target: 'fill',
        palleteColor: this.params.activeFillColor,
        alpha: this.params.activeFillColorAlpha,
        alphaColor: this.params.activeFillAlphaColor,
      },
      bg: {
        target: 'bg',
        palleteColor: this.params.backgroundFillColor,
        alpha: this.params.backgroundFillColorAlpha,
        alphaColor: this.params.backgroundFillAlphaColor,
      },
      // stroke: {
      //   target: 'stroke',
      //   palleteColor: this.params.textStrokeColor,
      //   alpha: this.params.textStrokeColorAlpha,
      //   alphaColor: this.params.textStrokeAlphaColor,
      // },
    };
    this.currentBackground = this.colorWidgetState.bg.alphaColor;
    this.currentBackgroundAlpha = this.colorWidgetState.bg.alpha;


    this.controlBuilder = new ControlBuilder(this);

    this.isMobile = isMobile.any;
    this.toolByName = {};
    this.toolByKeyCode = {};
    this.tools.forEach((t) => {
      if (t.controls) {
        t.controls = t.controls.map(t => t());
      }
      this.toolByName[t.name] = t;
      if (t.hotkey instanceof Function) {
        t.hotkey = t.hotkey();
      }

      if (t.hotkey && !this.params.hiddenTools.includes(t.name)) {
        if (!KEYS[t.hotkey]) {
          throw new Error(`Key code for ${t.hotkey} not defined in KEYS`);
        }
        this.toolByKeyCode[KEYS[t.hotkey]] = t;
      }
    });
    this.activeTool = undefined;
    this.zoom = false;
    this.ratioRelation = undefined;
    this.id = this.params.id;
    this.saving = false;

    if (this.id === undefined) {
      this.id = genId();
      this.holderId = genId();
      this.holderEl = document.createElement('div');
      this.holderEl.id = this.holderId;
      this.holderEl.className = 'ptro-holder-wrapper';
      document.body.appendChild(this.holderEl);
      this.holderEl.innerHTML = `<div id='${this.id}' class="ptro-holder"></div>`;
      this.baseEl = this.getElemByIdSafe(this.id);
    } else {
      this.baseEl = this.getElemByIdSafe(this.id);
      this.holderEl = null;
    }
    let bar = '';
    let rightBar = '';
    this.tools.filter(t => !this.params.hiddenTools.includes(t.name)).forEach((b) => {
      const id = genId();
      b.buttonId = id;
      const hotkey = b.hotkey ? ` [${b.hotkey.toUpperCase()}]` : '';
      const btn = b.iconUrl
      ? `<button type="button" aria-label=${b.name} class="ptro-icon-btn ptro-color-control" title="${b.name}" ` +
      `id="${id}" >` +
      `<img width="14" src="${b.iconUrl}" alt="${`${b.name}`}" /></button>`
      : `<button type="button" aria-label=${b.name} class="ptro-icon-btn ptro-color-control" title="${tr(`tools.${b.name}`)}${hotkey}" ` +
        `id="${id}" >` +
        `<i class="ptro-icon ptro-icon-${b.name}"></i></button>`;
      if (b.right) {
        rightBar += btn;
      } else {
        bar += btn;
      }
    });

    this.inserter = Inserter.get(this);

    const cropper = '<div class="ptro-crp-el">' +
      `${PainterroSelecter.code()}${TextTool.code()}</div>`;

    this.loadedName = '';
    this.doc = document;
    this.wrapper = this.doc.createElement('div');
    this.wrapper.id = `${this.id}-wrapper`;
    this.wrapper.className = 'ptro-wrapper';
    this.wrapper.innerHTML =
      '<div class="ptro-scroller">' +
        '<div class="ptro-center-table">' +
          '<div class="ptro-center-tablecell">' +
            `<canvas id="${this.id}-canvas"></canvas>` +
            `<div class="ptro-substrate"></div>${cropper}` +
          '</div>' +
        '</div>' +
      `</div>${
        ColorPicker.html() +
        ZoomHelper.html() +
        Resizer.html() +
        Settings.html(this) +
        Filters.html(this) +
        this.inserter.html()}`;
    this.baseEl.appendChild(this.wrapper);
    this.scroller = this.doc.querySelector(`#${this.id}-wrapper .ptro-scroller`);
    this.bar = this.doc.createElement('div');
    this.bar.id = `${this.id}-bar`;
    this.bar.className = 'ptro-bar ptro-color-main';
    this.fileInputId = genId();
    this.bar.innerHTML =
      `<div>${bar}` +
      '<span class="ptro-tool-controls"></span>' +
      '<span class="ptro-info"></span>' +
      `<span class="ptro-bar-right">${rightBar}</span>` +
      `<input id="${this.fileInputId}" type="file" style="display: none" value="none" accept="image/x-png,image/png,image/gif,image/jpeg" /></div>`;
    if (this.isMobile) {
      this.bar.style['overflow-x'] = 'auto';
    }

    this.baseEl.appendChild(this.bar);
    const style = this.doc.createElement('style');
    style.type = 'text/css';
    style.innerHTML = this.params.styles;
    this.baseEl.appendChild(style);

    // this.baseEl.innerHTML = '<iframe class="ptro-iframe"></iframe>';
    // this.iframe = this.baseEl.getElementsByTagName('iframe')[0];
    // this.doc = this.iframe.contentDocument || this.iframe.contentWindow.document;
    // this.doc.body.innerHTML = html;
    this.saveBtn = this.baseEl.querySelector(`#${this.toolByName.save.buttonId}`);
    if (this.toolByName.save.buttonId && this.saveBtn) {
      this.saveBtn.setAttribute('disabled', 'true');
    }
    this.body = this.doc.body;
    this.info = this.doc.querySelector(`#${this.id}-bar .ptro-info`);
    this.canvas = this.doc.querySelector(`#${this.id}-canvas`);
    this.ctx = this.canvas.getContext('2d');
    this.toolControls = this.doc.querySelector(`#${this.id}-bar .ptro-tool-controls`);
    this.toolContainer = this.doc.querySelector(`#${this.id}-wrapper .ptro-crp-el`);
    this.substrate = this.doc.querySelector(`#${this.id}-wrapper .ptro-substrate`);
    this.zoomHelper = new ZoomHelper(this);
    this.zoomButtonActive = false;
    this.select = new PainterroSelecter(this, (notEmpty) => {
      [this.toolByName.crop, this.toolByName.pixelize].forEach((c) => {
        this.setToolEnabled(c, notEmpty);
      });
    });
    if (this.params.backplateImgUrl) {
      this.tabelCell = this.canvas.parentElement;
      this.tabelCell.style.backgroundImage = `url(${this.params.backplateImgUrl})`;
      this.tabelCell.style.backgroundRepeat = 'no-repeat';
      this.tabelCell.style.backgroundPosition = 'center center';
      const img = new Image();
      img.onload = () => {
        this.resize(img.naturalWidth, img.naturalHeight);
        this.adjustSizeFull();
        this.worklog.captureState();
        this.tabelCell.style.backgroundSize = `${window.getComputedStyle(this.substrate).width} ${window.getComputedStyle(this.substrate).height}`;
      };
      img.src = this.params.backplateImgUrl;
    }
    this.resizer = new Resizer(this);
    this.settings = new Settings(this);
    this.primitiveTool = new PrimitiveTool(this);
    this.primitiveTool.setShadowOn(this.params.defaultPrimitiveShadowOn);
    this.primitiveTool.setLineWidth(this.params.defaultLineWidth);
    this.primitiveTool.setArrowLength(this.params.defaultArrowLength);
    this.primitiveTool.setEraserWidth(this.params.defaultEraserWidth);
    this.primitiveTool.setPixelSize(this.params.defaultPixelSize);
    this.hasUnsaved = false;
    this.worklog = new WorkLog(this, (state) => {
      if (this.saveBtn && !state.initial) {
        this.saveBtn.removeAttribute('disabled');
        this.hasUnsaved = true;
      }
      this.setToolEnabled(this.toolByName.undo, !state.first);
      this.setToolEnabled(this.toolByName.redo, !state.last);
      if (this.params.onChange) {
        this.params.onChange.call(this, {
          image: this.imageSaver,
          operationsDone: this.worklog.current.prevCount,
          realesedMemoryOperations: this.worklog.clearedCount,
        });
      }
    });
    this.inserter.init(this);
    this.paintBucket = new PaintBucket(this);
    this.textTool = new TextTool(this);
    this.filters = new Filters(this);
    this.colorPicker = new ColorPicker(this, (widgetState) => {
      this.colorWidgetState[widgetState.target] = widgetState;
      this.doc.querySelector(
        `#${this.id} .ptro-color-btn[data-id='${widgetState.target}']`).style['background-color'] =
        widgetState.alphaColor;
      const palletRGB = HexToRGB(widgetState.palleteColor);
      if (palletRGB !== undefined) {
        widgetState.palleteColor = rgbToHex(palletRGB.r, palletRGB.g, palletRGB.b);
        if (widgetState.target === 'line') {
          setParam('activeColor', widgetState.palleteColor);
          setParam('activeColorAlpha', widgetState.alpha);
        } else if (widgetState.target === 'fill') {
          setParam('activeFillColor', widgetState.palleteColor);
          setParam('activeFillColorAlpha', widgetState.alpha);
        } else if (widgetState.target === 'bg') {
          setParam('backgroundFillColor', widgetState.palleteColor);
          setParam('backgroundFillColorAlpha', widgetState.alpha);
        } else if (widgetState.target === 'stroke') {
          setParam('textStrokeColor', widgetState.palleteColor);
          setParam('textStrokeColorAlpha', widgetState.alpha);
        }
      }
    });

    this.defaultTool = this.toolByName[this.params.defaultTool];

    this.tools.filter(t => this.params.hiddenTools.indexOf(t.name) === -1).forEach((b) => {
      this.getBtnEl(b).onclick = () => {
        if (b === this.defaultTool && this.activeTool === b) {
          return;
        }
        const currentActive = this.activeTool;
        this.closeActiveTool(true);
        if (currentActive !== b) {
          this.setActiveTool(b);
          this.customEvents.dispatchEvent('changeActiveTool', b);

        } else {
          this.setActiveTool(this.defaultTool);
        }
      };
      this.getBtnEl(b).ontouch = this.getBtnEl(b).onclick;
    });

    this.getBtnEl(this.defaultTool).click();

    this.imageSaver = {
      /**
       * Returns image as base64 data url
       * @param {string} type - type of data url, default image/png
       * @param {string} quality - number from 0 to 1, works for `image/jpeg` or `image/webp`
       */
      asDataURL: (type, quality) => {
        let realType = type;
        if (realType === undefined) {
          if (this.loadedImageType) {
            realType = this.loadedImageType;
          } else {
            realType = 'image/png';
          }
        }
        return this.getAsUri(realType, quality);
      },
      asBlob: (type, quality) => {
        let realType = type;
        if (realType === undefined) {
          if (this.loadedImageType) {
            realType = this.loadedImageType;
          } else {
            realType = 'image/png';
          }
        }
        const uri = this.getAsUri(realType, quality);
        const byteString = atob(uri.split(',')[1]);
        const ab = new ArrayBuffer(byteString.length);
        const ia = new Uint8Array(ab);
        for (let i = 0; i < byteString.length; i += 1) {
          ia[i] = byteString.charCodeAt(i);
        }
        return new Blob([ab], {
          type: realType,
        });
      },
      getOriginalMimeType: () => this.loadedImageType,
      hasAlphaChannel: () => {
        const data = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height).data;
        for (let i = 3, n = data.length; i < n; i += 4) {
          if (data[i] < 255) {
            return true;
          }
        }
        return false;
      },
      suggestedFileName: (type) => {
        let realType = type;
        if (realType === undefined) {
          realType = 'png';
        }
        return `${(this.loadedName || `image-${genId()}`)}.${realType}`;
      },
      getWidth: () => this.size.w,
      getHeight: () => this.size.h,
    };

    this.initEventHandlers();
    this.hide();
    this.zoomFactor = 1;
  }

  setToolEnabled(tool, state) {
    if (tool.buttonId) {
      const btn = this.getElemByIdSafe(tool.buttonId);
      if (state) {
        btn.removeAttribute('disabled');
      } else {
        btn.setAttribute('disabled', 'true');
      }
    }
  }
  getAsUri(type, quality) {
    let realQuality = quality;
    if (realQuality === undefined) {
      realQuality = 0.92;
    }
    return this.canvas.toDataURL(type, realQuality);
  }

  getBtnEl(tool) {
    return this.getElemByIdSafe(tool.buttonId);
  }

  save() {
    if (this.saving) {
      return this;
    }
    this.saving = true;
    const btn = this.baseEl.querySelector(`#${this.toolByName.save.buttonId}`);
    const icon = this.baseEl.querySelector(`#${this.toolByName.save.buttonId} > i`);
    if (this.toolByName.save.buttonId && btn) {
      btn.setAttribute('disabled', 'true');
    }
    this.hasUnsaved = false;

    if (icon) {
      icon.className = 'ptro-icon ptro-icon-loading ptro-spinning';
    }

    if (this.params.saveHandler !== undefined) {
      this.params.saveHandler(this.imageSaver, (hide) => {
        if (hide === true) {
          this.hide();
        }
        if (icon) {
          icon.className = 'ptro-icon ptro-icon-save';
        }
        this.saving = false;
      });
    } else {
      logError('No saveHandler defined, please check documentation');
      if (icon) {
        icon.className = 'ptro-icon ptro-icon-save';
      }
      this.saving = false;
    }
    return this;
  }

  close() {
    if (this.params.onClose !== undefined) {
      this.params.onClose();
    }
  }

  closeActiveTool(doNotSelect) {
    if (this.activeTool !== undefined) {
      if (this.activeTool.close !== undefined) {
        this.activeTool.close();
      }
      this.toolControls.innerHTML = '';
      const btnEl = this.getBtnEl(this.activeTool);
      if (btnEl) {
        btnEl.className =
        this.getBtnEl(this.activeTool).className.replace(' ptro-color-active-control', '');
      }
      this.activeTool = undefined;
    }
    if (doNotSelect !== true) {
      this.setActiveTool(this.defaultTool);
    }
  }

  handleToolEvent(eventHandler, event) {
    if (this.select.imagePlaced || this.select.area.activated) {
      return this.select[eventHandler](event);
    }
    if (this.activeTool && this.activeTool.eventListner) {
      const listner = this.activeTool.eventListner();
      if (listner[eventHandler]) {
        return listner[eventHandler](event);
      }
    }
    
    return false;
  }

  handleClipCopyEvent(evt) {
    let handled = false;
    const clipFormat = 'image/png';
    if (evt.keyCode === KEYS.c && (evt.ctrlKey || evt.metaKey)) {
      if (!this.inserter.waitChoice && !this.select.imagePlaced && this.select.shown) {
        const a = this.select.area;
        const w = a.bottoml[0] - a.topl[0];
        const h = a.bottoml[1] - a.topl[1];
        const tmpCan = this.doc.createElement('canvas');
        tmpCan.width = w;
        tmpCan.height = h;
        const tmpCtx = tmpCan.getContext('2d');
        tmpCtx.drawImage(this.canvas, -a.topl[0], -a.topl[1]);
        tmpCan.toBlob((b) => {
          /* eslint no-undef: "off" */
          navigator.clipboard.write([new ClipboardItem({ [clipFormat]: b })]);
        }, clipFormat, 1.0);
        handled = true;
      } else {
        this.canvas.toBlob((b) => {
          /* eslint no-undef: "off" */
          navigator.clipboard.write([new ClipboardItem({ [clipFormat]: b })]);
        }, clipFormat, 1.0);
        handled = true;
      }
    }
    return handled;
  }
  zoomImage({ wheelDelta, clientX, clientY }, forceWheenDelta, forceZoomForce) {
    let whD = wheelDelta;
    if (forceWheenDelta !== undefined) {
      whD = forceWheenDelta;
    }
    let minFactor = 1;
    if (this.size.w > this.wrapper.documentClientWidth) {
      minFactor = Math.min(minFactor, this.wrapper.documentClientWidth / this.size.w);
    }
    if (this.size.h > this.wrapper.documentClientHeight) {
      minFactor = Math.min(minFactor, this.wrapper.documentClientHeight / this.size.h);
    }
    if (!this.zoom && this.zoomFactor > minFactor) {
      this.zoomFactor = minFactor;
    }
    let zoomForce = 0.2;
    if (forceZoomForce !== undefined) {
      zoomForce = forceZoomForce;
    }

    this.zoomFactor += Math.sign(whD) * zoomForce;
    if (this.zoomFactor < minFactor) {
      this.zoom = false;
      this.zoomFactor = minFactor;
    } else {
      this.zoom = true;
    }
    this.adjustSizeFull();
    this.select.adjustPosition();
    if (this.zoom) {
      this.scroller.scrollLeft = (this.curCord[0] / this.getScale()) -
        (clientX - this.wrapper.documentOffsetLeft);
      this.scroller.scrollTop = (this.curCord[1] / this.getScale()) -
        (clientY - this.wrapper.documentOffsetTop);
    }
  }
  initEventHandlers() {
    this.documentHandlers = {
      mousedown: (e) => {
        if (this.shown) {
          if (this.worklog.empty &&
             (e.target.className.indexOf('ptro-crp-el') !== -1 ||
              e.target.className.indexOf('ptro-icon') !== -1 ||
              e.target.className.indexOf('ptro-named-btn') !== -1)) {
            this.clearBackground(); // clear initText
          }
          if (this.colorPicker.handleMouseDown(e) !== true) {
            this.handleToolEvent('handleMouseDown', e);
          }
        }
      },
      touchstart: (e) => {
        if (e.touches.length === 1) {
          e.clientX = e.changedTouches[0].clientX;
          e.clientY = e.changedTouches[0].clientY;
          this.documentHandlers.mousedown(e);
        } else if (e.touches.length === 2) {
          const fingersDist = distance({
            x: e.touches[0].clientX,
            y: e.touches[0].clientY,
          }, {
            x: e.touches[1].clientX,
            y: e.touches[1].clientY,
          });
          this.lastFingerDist = fingersDist;
        }
      },
      touchend: (e) => {
        e.clientX = e.changedTouches[0].clientX;
        e.clientY = e.changedTouches[0].clientY;
        this.documentHandlers.mouseup(e);
      },
      touchmove: (e) => {
        if (e.touches.length === 1) {
          e.clientX = e.touches[0].clientX;
          e.clientY = e.touches[0].clientY;
          this.documentHandlers.mousemove(e);
        } else if (e.touches.length === 2) {
          const fingersDist = distance({
            x: e.touches[0].clientX,
            y: e.touches[0].clientY,
          }, {
            x: e.touches[1].clientX,
            y: e.touches[1].clientY,
          });

          const center = {
            x: (e.touches[0].clientX + e.touches[1].clientX) / 2,
            y: (e.touches[0].clientY + e.touches[1].clientY) / 2,
          };

          e.clientX = center.x;
          e.clientY = center.y;

          const fingerDistDiff = Math.abs(fingersDist - this.lastFingerDist);
          const zoomForce = (fingersDist > this.lastFingerDist ? 1 : -1);

          this.documentHandlers.wheel(e, zoomForce, true, fingerDistDiff * 0.001);
          
          this.lastFingerDist = fingersDist;
          e.stopPropagation();
          if (!this.zoomButtonActive) e.preventDefault();
        }
      },
      mousemove: (e) => {
        const isEvenFromPtro = e.target.classList[0] === 'ptro-crp-el' || e.target.classList[0] === 'ptro-bar'; 
        if (this.shown) {
          this.handleToolEvent('handleMouseMove', e);
          this.colorPicker.handleMouseMove(e);
          this.zoomHelper.handleMouseMove(e);
          this.curCord = [
            (e.clientX - this.elLeft()) + this.scroller.scrollLeft,
            (e.clientY - this.elTop()) + this.scroller.scrollTop,
          ];
          const scale = this.getScale();
          this.curCord = [this.curCord[0] * scale, this.curCord[1] * scale];
          if (typeof e.target.tagName !== "undefined" && e.target.tagName.toLowerCase() !== 'input'
              && e.target.tagName.toLowerCase() !== 'button' && e.target.tagName.toLowerCase() !== 'i'
              && e.target.tagName.toLowerCase() !== 'select') {
            // prevent default only if we are in paintero area    
            if (!this.zoomButtonActive && isEvenFromPtro) e.preventDefault();
          }
        }
      },
      mouseup: (e) => {
        if (this.shown) {
          this.handleToolEvent('handleMouseUp', e);
          this.colorPicker.handleMouseUp(e);
        }
      },
      wheel: (e, forceWheenDelta, forceCtrlKey, zoomForce) => {
        if (this.shown && !this.params.disableWheelZoom) {
          if (forceCtrlKey !== undefined ? forceCtrlKey : e.ctrlKey) {
            this.zoomImage(e, forceWheenDelta, zoomForce);
            e.preventDefault();
          }
        }
      },
      keydown: (e) => {
        const argetEl = event.target;
        const ignoreForSelectors = ['input', 'textarea', 'div[contenteditable]'];

        if (ignoreForSelectors.some(selector => argetEl.matches(selector))) {
          return; // ignore all elemetents which could be focused
        }
        if (this.shown) {
          if (this.colorPicker.handleKeyDown(e)) {
            return;
          }
          if (this.handleClipCopyEvent(e)) {
            return;
          }
          const evt = window.event ? event : e;
          if (this.handleToolEvent('handleKeyDown', evt)) {
            return;
          }
          if (
            (evt.keyCode === KEYS.y && evt.ctrlKey) ||
            (evt.keyCode === KEYS.z && evt.ctrlKey && evt.shiftKey)) {
            this.worklog.redoState();
            e.preventDefault();
            if (this.params.userRedo) {
              this.params.userRedo.call();
            }
          } else if (evt.keyCode === KEYS.z && evt.ctrlKey) {
            this.worklog.undoState();
            e.preventDefault();
            if (this.params.userUndo) {
              this.params.userUndo.call();
            }
          }
          if (this.toolByKeyCode[event.keyCode]) {
            this.getBtnEl(this.toolByKeyCode[event.keyCode]).click();
            e.stopPropagation();
            e.preventDefault();
          }
          if (this.saveBtn) {
            if (evt.keyCode === KEYS.s && evt.ctrlKey) {
              if (this.initText) this.wrapper.click();
              this.save();
              evt.preventDefault();
            }
          }
        }
      },
      paste: (event) => {
        if (this.initText) this.wrapper.click();
        if (this.shown) {
          const items = (event.clipboardData || event.originalEvent.clipboardData).items;
          Object.keys(items).forEach((k) => {
            const item = items[k];
            if (item.kind === 'file' && item.type.split('/')[0] === 'image') {
              this.openFile(item.getAsFile());
              event.preventDefault();
              event.stopPropagation();
            }
          });
        }
      },
      dragover: (event) => {
        if (this.shown) {
          const mainClass = event.target.classList[0];
          if (mainClass === 'ptro-crp-el' || mainClass === 'ptro-bar') {
            this.bar.className = 'ptro-bar ptro-color-main ptro-bar-dragover';
          }
          event.preventDefault();
        }
      },
      dragleave: () => {
        if (this.shown) {
          this.bar.className = 'ptro-bar ptro-color-main';
        }
      },
      drop: (event) => {
        if (this.shown) {
          this.bar.className = 'ptro-bar ptro-color-main';
          event.preventDefault();
          const file = event.dataTransfer.files[0];
          if (file) {
            this.openFile(file);
          } else {
            const text = event.dataTransfer.getData('text/html');
            const srcRe = /src.*?=['"](.+?)['"]/;
            const srcMatch = srcRe.exec(text);
            this.inserter.handleOpen(srcMatch[1]);
          }
        }
      },
    };

    this.windowHandlers = {
      resize: () => {
        if (this.shown) {
          this.adjustSizeFull();
          this.syncToolElement();
        }
      },
    };
    this.listenersInstalled = false;
  }

  attachEventHandlers() {
    if (this.listenersInstalled) {
      return;
    }
    // passive: false fixes Unable to preventDefault inside passive event
    // listener due to target being treated as passive
    Object.keys(this.documentHandlers).forEach((key) => {
      this.doc.addEventListener(key, this.documentHandlers[key], { passive: false });
    });

    Object.keys(this.windowHandlers).forEach((key) => {
      window.addEventListener(key, this.windowHandlers[key], { passive: false });
    });
    this.listenersInstalled = true;
  }

  removeEventHandlers() {
    if (!this.listenersInstalled) {
      return;
    }
    Object.keys(this.documentHandlers).forEach((key) => {
      this.doc.removeEventListener(key, this.documentHandlers[key]);
    });
    Object.keys(this.windowHandlers).forEach((key) => {
      window.removeEventListener(key, this.windowHandlers[key]);
    });

    this.listenersInstalled = false;
  }

  elLeft() {
    return this.toolContainer.documentOffsetLeft + this.scroller.scrollLeft;
  }

  elTop() {
    return this.toolContainer.documentOffsetTop + this.scroller.scrollTop;
  }

  fitImage(img, mimetype) {
    this.loadedImageType = mimetype;
    this.resize(img.naturalWidth, img.naturalHeight);
    this.ctx.drawImage(img, 0, 0);
    const minValue = Math.min(this.wrapper.documentClientHeight / this.size.h, this.wrapper.documentClientWidth / this.size.w);
    this.zoomFactor = minValue;
    this.adjustSizeFull();
    this.worklog.captureState();
  }

  loadImage(source, mimetype) {
    this.inserter.handleOpen(source, mimetype);
  }

  show(openImage, originalMime) {
    this.shown = true;
    this.scrollWidth = getScrollbarWidth();
    if (this.isMobile) {
      this.origOverflowY = this.body.style['overflow-y'];
      if (this.params.fixMobilePageReloader) {
        this.body.style['overflow-y'] = 'hidden';
      }
    }
    this.baseEl.removeAttribute('hidden');
    if (this.holderEl) {
      this.holderEl.removeAttribute('hidden');
    }
    if (typeof openImage === 'string') {
      this.loadedName = trim(
        (openImage.substring(openImage.lastIndexOf('/') + 1) || '').replace(/\..+$/, ''));

      this.loadImage(openImage, originalMime);
    } else if (openImage !== false) {
      this.clear();
    }
    this.attachEventHandlers();
    return this;
  }

  hide() {
    if (this.isMobile) {
      this.body.style['overflow-y'] = this.origOverflowY;
    }
    this.shown = false;
    this.baseEl.setAttribute('hidden', '');
    if (this.holderEl) {
      this.holderEl.setAttribute('hidden', '');
    }
    this.removeEventHandlers();
    if (this.params.onHide !== undefined) {
      this.params.onHide();
    }
    return this;
  }

  setZoom(zoomPercentage) {
    if (!this.size) {
      return;
    }

    this.zoomFactor = zoomPercentage / 100;

    let minFactor = 1;
    if (this.size.w > this.wrapper.documentClientWidth) {
      minFactor = Math.min(minFactor, this.wrapper.documentClientWidth / this.size.w);
    }
    if (this.size.h > this.wrapper.documentClientHeight) {
      minFactor = Math.min(minFactor, this.wrapper.documentClientHeight / this.size.h);
    }

    if (!this.zoom && this.zoomFactor > minFactor) {
      this.zoomFactor = minFactor;
    }

    if (this.zoomFactor < minFactor) {
      this.zoom = false;
      this.zoomFactor = minFactor;
    } else {
      this.zoom = true;
    }

    this.adjustSizeFull();
    this.select.adjustPosition();

    const canvas = this.canvas;
    const gbr = canvas.getBoundingClientRect();

    this.curCord = [
      (gbr.right / 2 - this.elLeft()) + this.scroller.scrollLeft,
      (gbr.bottom / 2 - this.elTop()) + this.scroller.scrollTop,
    ];

    const scale = this.getScale();
    this.curCord = [this.curCord[0] * scale, this.curCord[1] * scale];

    if (this.zoom) {
      this.scroller.scrollLeft = (this.curCord[0] / this.getScale()) -
          (gbr.right / 2 - this.wrapper.documentOffsetLeft);
      this.scroller.scrollTop = (this.curCord[1] / this.getScale()) -
          (gbr.bottom / 2 - this.wrapper.documentOffsetTop);
    }
  }

  doScale({width, height, scale}) {
    if (scale) {
      if (width || height) {
        throw new Error(`You can't pass width or height and scale at the same time`)
      }
      this.resizer.newW = Math.round(this.size.w * scale);
      this.resizer.newH = Math.round(this.size.h * scale);

    } else {
      if (scale) {
        throw new Error(`You can't pass width or height and scale at the same time`)
      }
      this.resizer.newW = width || Math.round(this.size.w * (height / this.size.h));
      this.resizer.newH = height || Math.round(this.size.h * (width / this.size.w));
    }
    this.resizer.scaleButton.onclick();
  }

  openFile(f) {
    if (!f) {
      return;
    }
    this.loadedName = trim((f.name || '').replace(/\..+$/, ''));
    const dataUrl = (window.URL ? window.URL : window.webkitURL).createObjectURL(f);
    this.loadImage(dataUrl, f.type);
  }

  getScale() {
    return this.canvas.getAttribute('width') / this.canvas.offsetWidth;
  }

  adjustSizeFull() {
    const ratio = this.wrapper.documentClientWidth / this.wrapper.documentClientHeight;
    
    if (this.zoom === false && this.textTool.active === false) {
        if (this.size.w > this.wrapper.documentClientWidth ||
            this.size.h > this.wrapper.documentClientHeight) {
            const newRelation = ratio < this.size.ratio;
            this.ratioRelation = newRelation;
            if (newRelation) {
                this.canvas.style.width = `${this.wrapper.clientWidth}px`;
                this.canvas.style.height = 'auto';
            } else {
                this.canvas.style.width = 'auto';
                this.canvas.style.height = `${this.wrapper.clientHeight}px`;
            }
            
        } else {
            this.canvas.style.width = 'auto';
            this.canvas.style.height = 'auto';
            this.ratioRelation = 0;
        }
        this.scroller.style.overflow = 'hidden';
    } else {
        if (this.zoom === false) {
          this.scroller.style.overflow = 'hidden';
        } else {
          this.scroller.style.overflow = 'scroll';
        }
        this.canvas.style.width = `${this.size.w * this.zoomFactor}px`;
        this.canvas.style.height = `${this.size.h * this.zoomFactor}px`;
        this.ratioRelation = 0;
    }
    this.syncToolElement();
    this.select.draw();
}

  resize(x, y) {
    this.info.innerHTML = `${x}<span>x</span>${y}<br>${(this.originalMime || 'png').replace('image/', '')}`;
    this.size = {
      w: x,
      h: y,
      ratio: x / y,
    };
    this.canvas.setAttribute('width', this.size.w);
    this.canvas.setAttribute('height', this.size.h);
  }

  syncToolElement() {
    const w = Math.round(this.canvas.documentClientWidth);
    const l = this.canvas.offsetLeft;
    const h = Math.round(this.canvas.documentClientHeight);
    const t = this.canvas.offsetTop;
    this.toolContainer.style.left = `${l}px`;
    this.toolContainer.style.width = `${w}px`;
    this.toolContainer.style.top = `${t}px`;
    this.toolContainer.style.height = `${h}px`;
    this.substrate.style.left = `${l}px`;
    this.substrate.style.width = `${w}px`;
    this.substrate.style.top = `${t}px`;
    this.substrate.style.height = `${h}px`;
  }

  clear() {
    const w = this.params.defaultSize.width === 'fill' ? this.wrapper.clientWidth : this.params.defaultSize.width;
    const h = this.params.defaultSize.height === 'fill' ? this.wrapper.clientHeight : this.params.defaultSize.height;
    this.resize(w, h);
    this.clearBackground();
    this.worklog.captureState(true);
    this.worklog.clean = true;
    this.syncToolElement();
    this.adjustSizeFull();

    if (this.params.initText && this.worklog.empty) {
      this.ctx.lineWidth = 3;
      this.ctx.strokeStyle = '#fff';
      const initTexts = this.wrapper.querySelectorAll('.init-text');
      if (initTexts.length > 0) {
        initTexts.forEach((text) => {
          text.remove();
        });
      }
      this.initText = document.createElement('div');
      this.initText.classList.add('init-text');
      this.wrapper.append(this.initText);
      this.initText.innerHTML = '<div style="pointer-events: none;position:absolute;top:50%;width:100%;left: 50%; transform: translate(-50%, -50%)">' +
        `${this.params.initText}</div>`;
      this.initText.style.left = '0';
      this.initText.style.top = '0';
      this.initText.style.right = '0';
      this.initText.style.bottom = '0';
      this.initText.style.pointerEvents = 'none';
      this.initText.style['text-align'] = 'center';
      this.initText.style.position = 'absolute';
      this.initText.style.color = this.params.initTextColor;
      this.initText.style['font-family'] = this.params.initTextStyle.split(/ (.+)/)[1];
      this.initText.style['font-size'] = this.params.initTextStyle.split(/ (.+)/)[0];

      this.wrapper.addEventListener('click', () => {
        this.initText.remove();
        this.initText = null;
      }, { once: true });
    }
  }

  clearBackground() {
    this.ctx.beginPath();
    this.ctx.clearRect(0, 0, this.size.w, this.size.h);
    this.ctx.rect(0, 0, this.size.w, this.size.h);
    this.ctx.fillStyle = this.currentBackground;
    this.ctx.fill();
  }

  setColor(options){
    this.doc.querySelector(
      `#${this.id} .ptro-color-btn[data-id='${options[0]}']`).style['background-color'] =
      options[1].alphaColor;
    this.colorWidgetState = {...this.colorWidgetState, line: {
      target: options[0],
      palleteColor: options[1].palleteColor,
      alpha:options[1].alpha, 
      alphaColor: options[1].alphaColor, 
    }, }
  
  }



  setLineWidth(width) {
    setPrimitiveToolValue(width,this.primitiveTool,'setLineWidth','lineWidth');
  }

  setArrowLength(length) {
    setPrimitiveToolValue(length,this.primitiveTool,'setArrowLength','arrowLength');
  }

  setEraserWidth(width) {
    setPrimitiveToolValue(width,this.primitiveTool,'setEraserWidth','eraserWidth');
  }

  setPixelSize(size) {
    setPrimitiveToolValue(size,this.primitiveTool,'setPixelSize','pixelSize');
  }

  setShadowOn(state) {
    setPrimitiveToolValue(state,this.primitiveTool,'setShadowOn','shadowOn');
  }


  setActiveTool(b) {
    this.activeTool = b;
    
    this.zoomButtonActive = false;
    const btnEl = this.getBtnEl(this.activeTool);
    if (btnEl) {
      btnEl.className += ' ptro-color-active-control';
    }
    let ctrls = '';
    (b.controls || []).forEach((ctl) => {
      ctl.id = genId();
      if (ctl.title) {
        ctrls += `<span class="ptro-tool-ctl-name" title="${tr(ctl.titleFull)}">${tr(ctl.title)}</span>`;
      }
      if (ctl.type === 'btn') {
        ctrls += `<button type="button" ${ctl.hint ? `title="${tr(ctl.hint)}"` : ''} class="ptro-color-control ${ctl.icon ? 'ptro-icon-btn' : 'ptro-named-btn'}" ` +
          `id=${ctl.id}>${ctl.icon ? `<i class="ptro-icon ptro-icon-${ctl.icon}"></i>` : ''}` +
          `<p>${ctl.name || ''}</p></button>`;
      } else if (ctl.type === 'color') {
        ctrls += `<button type="button" id=${ctl.id} data-id='${ctl.target}' ` +
          `style="background-color: ${this.colorWidgetState[ctl.target].alphaColor}" ` +
          'class="color-diwget-btn ptro-color-btn ptro-bordered-btn ptro-color-control"></button>' +
          '<span class="ptro-btn-color-checkers-bar"></span>';
      } else if (ctl.type === 'int') {
        ctrls += `<input id=${ctl.id} class="ptro-input" type="number" min="${ctl.min}" max="${ctl.max}" ` +
          `data-id='${ctl.target}'/>`;
      } else if (ctl.type === 'bool') {
        ctrls += `<button id=${ctl.id} class="ptro-input ptro-check" data-value="false" type="button" ` +
          `data-id='${ctl.target}'></button>`;
      } else if (ctl.type === 'dropdown') {
        let options = '';
        ctl.getAvailableValues().forEach((o) => {
          options += `<option ${o.extraStyle ? `style='${o.extraStyle}'` : ''}` +
            ` value='${o.value}' ${o.title ? `title='${o.title}'` : ''}>${o.name}</option>`;
        });
        ctrls += `<select id=${ctl.id} class="ptro-input" ` +
          `data-id='${ctl.target}'>${options}</select>`;
      }
    });
    this.toolControls.innerHTML = ctrls;
    (b.controls || []).forEach((ctl) => {
      if (ctl.type === 'int') {
        this.getElemByIdSafe(ctl.id).value = ctl.getValue();
        if (ctl.options && ctl.options.eventOnChange){
          this.getElemByIdSafe(ctl.id).onchange = ctl.action;
        } else {
        this.getElemByIdSafe(ctl.id).oninput = ctl.action;
        }
      } else if (ctl.type === 'bool') {
        this.getElemByIdSafe(ctl.id).setAttribute('data-value', ctl.getValue() ? 'true' : 'false');
        this.getElemByIdSafe(ctl.id).onclick = ctl.action;
      } else if (ctl.type === 'dropdown') {
        this.getElemByIdSafe(ctl.id).onchange = ctl.action;
        this.getElemByIdSafe(ctl.id).value = ctl.getValue();
      } else {
        this.getElemByIdSafe(ctl.id).onclick = ctl.action;
      }
    });
    b.activate();
  }
}

export default params => new PainterroProc(params);


================================================
FILE: js/paintBucket.js
================================================
import ColorPicker, { HexToRGB, rgbToHex } from './colorPicker';

export default class PaintBucket {
  constructor(main) {
    this.main = main;
    this.canvasWidth = 600;
    this.canvasHeight = 420;
    this.el = this.main.toolContainer;
    this.input = this.el.querySelector('.ptro-text-tool-input');
  }

  init() {
    this.ctx = this.main.ctx;
    this.canvas = this.main.canvas;
    this.colorLayerData = this.ctx.getImageData(0, 0, this.canvasWidth, this.canvasHeight);
    this.drawingAreaX = 0;
    this.drawingAreaY = 0;
    this.canvasWidth = this.canvas.width
    this.canvasHeight = this.canvas.height
    this.drawingAreaWidth = this.canvasWidth;
    this.drawingAreaHeight = this.canvasHeight;
  }

  handleMouseDown(event) {
    const mainClass = event.target.classList[0];
    const scale = this.main.getScale();
    if (mainClass === 'ptro-crp-el') {
      this.init();
      this.colorLayerData = this.ctx.getImageData(0, 0, this.canvasWidth, this.canvasHeight);

      if (!this.active) {
        this.input.innerHTML = '<br>';
        this.pendingClear = true;
      }

      this.active = true;
      const cord = [
        (event.clientX - this.main.elLeft()) + this.main.scroller.scrollLeft,
        (event.clientY - this.main.elTop()) + this.main.scroller.scrollTop,
      ];
      const cur = {
        x: cord[0] * scale,
        y: cord[1] * scale,
      };
      this.paintAt(cur.x, cur.y);
    }
  }

  paintAt(startX, startY) {
    startX = Math.round(startX);
    startY = Math.round(startY - 1);
    startX = (startX > 0) ? startX : 0;
    startY = (startY > 0) ? startY : 0;

    // get clicked on color
    this.getClickedOnColor(startX, startY);

    var pixelPos = (startY * this.canvasWidth + startX) * 4;
    var pixelColor = this.getPixelColor(this.colorLayerData, pixelPos);
    var r = pixelColor.r;
    var g = pixelColor.g;
    var b = pixelColor.b;
    var a = pixelColor.a;

    const curColor = HexToRGB( this.main.colorWidgetState.fill.palleteColor );
    this.color = this.main.colorWidgetState.fill.palleteColor;
    
    if (r === curColor.r && g === curColor.g && b === curColor.b) {
      // Return because trying to fill with the same color
      return;
    }

    this.floodFill(startX, startY, r, g, b);
    this.ctx.putImageData(this.colorLayerData, 0, 0);
    this.main.worklog.captureState();
  }

  // returns true if the current pixel's color matches the clicked on color.
  matchStartColor(pixelPos) {
    var pixelColor = this.getPixelColor(this.colorLayerData, pixelPos);
    var v = this.matchClickedColor(pixelColor.r, pixelColor.g, pixelColor.b, pixelColor.a);
    return v;
  }

  matchClickedColor(r, g, b, a) {
    const limit = this.main.params.bucketSensivity;
    var matchedR = (Math.abs(r - this.clickedOnColor.r) < limit);
    var matchedG = (Math.abs(g - this.clickedOnColor.g) < limit);
    var matchedB = (Math.abs(b - this.clickedOnColor.b) < limit);
    var matchedA = (Math.abs(a - this.clickedOnColor.a) < limit);
    var v = (matchedR && matchedG && matchedB && matchedA);
    return v;
  }

  getClickedOnColor(x, y) {
    var pixelPos = (y * this.canvasWidth + x) * 4;
    var pixelColor = this.getPixelColor(this.colorLayerData, pixelPos);
    this.clickedOnColor = {r: pixelColor.r, g: pixelColor.g, b: pixelColor.b, a: pixelColor.a};
  }

  floodFill(startX, startY, startR, startG, startB) {
    // console.log('flood: ' + startX + ' ' + startY + ' ' + startR + ' ' + startG + ' ' + startB);
    
    var newPos,
      x,
      y,
      pixelPos,
      reachLeft,
      reachRight,
      drawingBoundLeft = this.drawingAreaX,
      drawingBoundTop = this.drawingAreaY,
      drawingBoundRight = this.drawingAreaX + this.drawingAreaWidth - 1,
      drawingBoundBottom = this.drawingAreaY + this.drawingAreaHeight - 1,
      pixelStack = [[startX, startY]];

    while (pixelStack.length) {

      newPos = pixelStack.pop();
      x = newPos[0];
      y = newPos[1];

      // Get current pixel position
      pixelPos = (y * this.canvasWidth + x) * 4;
      const curColor = HexToRGB(this.color);


      // Go up as long as the color matches and are inside the canvas
      while (y >= drawingBoundTop && this.matchStartColor(pixelPos)) {
        y -= 1;
        pixelPos -= this.canvasWidth * 4;
      }

      pixelPos += this.canvasWidth * 4;
      y += 1;
      reachLeft = false;
      reachRight = false;

      // Go down as long as the color matches and in inside the canvas
      while (y <= drawingBoundBottom && this.matchStartColor(pixelPos)) {
        y += 1;

        this.colorPixel(pixelPos, curColor.r, curColor.g, curColor.b);

        if (x > drawingBoundLeft) {
          if (this.matchStartColor(pixelPos - 4)) {
            if (!reachLeft) {
              // Add pixel to stack
              pixelStack.push([x - 1, y]);
              reachLeft = true;
            }
          } else if (reachLeft) {
            reachLeft = false;
          }
        }

        if (x < drawingBoundRight) {
          if (this.matchStartColor(pixelPos + 4)) {
            if (!reachRight) {
              // Add pixel to stack
              pixelStack.push([x + 1, y]);
              reachRight = true;
            }
          } else if (reachRight) {
            reachRight = false;
          }
        }

        pixelPos += this.canvasWidth * 4;
      }
    }    
  }

  colorPixel(pixelPos, r, g, b, a) {
    this.colorLayerData.data[pixelPos] = r;
    this.colorLayerData.data[pixelPos + 1] = g;
    this.colorLayerData.data[pixelPos + 2] = b;
    this.colorLayerData.data[pixelPos + 3] = a !== undefined ? a : 255;
  }

  getPixelColor(ctx, pixelPos) {
    var r = ctx.data[pixelPos];
    var g = ctx.data[pixelPos + 1];
    var b = ctx.data[pixelPos + 2];
    var a = ctx.data[pixelPos + 3];
    return {r: r, g: g, b: b, a: a};
  }
}


================================================
FILE: js/params.js
================================================
/* eslint-disable */
import { HexToRGBA } from './colorPicker';
import { trim, logError } from './utils';
import Translation, { activate } from './translation';

const STORAGE_KEY = 'painterro-data';

let settings = {};

function loadSettings() {
  try {
    settings = JSON.parse(localStorage.getItem(STORAGE_KEY));
  } catch (e) {
    console.warn(`Unable get from localStorage: ${e}`);
  }
  if (!settings) {
    settings = {};
  }
}

export function setParam(name, val) {
  settings[name] = val;
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
  } catch (e) {
    console.warn(`Unable save to localstorage: ${e}`);
  }
}

function firstDefined(...vals) {
  for (let i = 0; i < vals.length; i += 1) {
    if (vals[i] !== undefined) {
      return vals[i];
    }
  }
  return undefined;
}



export function setDefaults(parameters, allToolsNames) {
  loadSettings();
  const params = parameters || {};
  if (params.language) {
    activate(params.language);
  }
  params.NON_SELECTABLE_TOOLS = ['pixelize', 'crop', 'rotate'];

  params.activeColor = settings.activeColor || params.activeColor || '#ff0000';
  params.activeColorAlpha = firstDefined(settings.activeColorAlpha, params.activeColorAlpha, 1.0);
  params.activeAlphaColor = HexToRGBA(params.activeColor, params.activeColorAlpha);

  params.activeFillColor = settings.activeFillColor || params.activeFillColor || '#000000';
  params.activeFillColorAlpha = firstDefined(settings.activeFillColorAlpha,
    params.activeFillColorAlpha, 0.0);
  params.activeFillAlphaColor = HexToRGBA(params.activeFillColor, params.activeFillColorAlpha);
  params.replace_all_on_empty_background = 

  params.initText = params.initText || null;
  params.initTextColor = params.initTextColor || '#808080';
  params.initTextStyle = params.initTextStyle || '26px \'Open Sans\', sans-serif';
  params.defaultLineWidth = settings.defaultLineWidth || params.defaultLineWidth || 5;
  params.defaultPrimitiveShadowOn = firstDefined(settings.defaultPrimitiveShadowOn,
    params.defaultPrimitiveShadowOn, true);

  params.defaultArrowLength = settings.defaultArrowLength || params.defaultArrowLength || 32;
  params.defaultEraserWidth = firstDefined(settings.defaultEraserWidth,
    params.defaultEraserWidth, 5);
  params.defaultFontSize = firstDefined(settings.defaultFontSize, params.defaultFontSize, 24);
  params.defaultFontBold = firstDefined(settings.defaultFontBold, params.defaultFontBold, false);
  params.defaultFontItalic = firstDefined(settings.defaultFontItalic,
    params.defaultFontItalic, false);
  
  params.replaceAllOnEmptyBackground = firstDefined(params.replaceAllOnEmptyBackground, true);
  params.backgroundFillColor = settings.backgroundFillColor || params.backgroundFillColor || '#ffffff';
  params.backgroundFillColorAlpha = params.backplateImgUrl ? 0 :
    firstDefined(settings.backgroundFillColorAlpha, params.backgroundFillColorAlpha, 1.0);
  params.backgroundFillAlphaColor = HexToRGBA(params.backgroundFillColor,
    params.backgroundFillColorAlpha);

  params.textStrokeColor = settings.textStrokeColor || params.textStrokeColor || '#ffffff';
  params.textStrokeColorAlpha = firstDefined(settings.textStrokeColorAlpha,
    params.textStrokeColorAlpha, 1.0);
  params.textStrokeAlphaColor = HexToRGBA(params.textStrokeColor, params.textStrokeColorAlpha);

  params.shadowScale = firstDefined(params.shadowScale, 1);

  params.defaultTextStrokeAndShadow = firstDefined(settings.defaultTextStrokeAndShadow,
    params.defaultTextStrokeAndShadow, true);

  params.worklogLimit = firstDefined(params.worklogLimit, 100);

  params.hiddenTools = params.hiddenTools || ['redo', 'zoomin', 'zoomout'];
  params.hiddenTools.forEach(t => {
    if (!allToolsNames.includes(t)) {
      logError(`Hidden tool with name ${t}`)
    }
  })

  if (params.defaultTool) {
    if (params.hiddenTools.includes(params.defaultTool)) {
      logError(`Can't hide default tool '${params.defaultTool}', please change default tool to another to hide it`);
      params.hiddenTools.splice(defaultInHiddenIndex, 1);
    } 
  } else {
    params.defaultTool = allToolsNames.filter(t => !params.hiddenTools.includes(t) && !params.NON_SELECTABLE_TOOLS.includes(t))[0];  // select first tool which supports selecting
  }

  params.pixelizePixelSize = settings.pixelizePixelSize || params.pixelizePixelSize || '20%';
  params.colorScheme = params.colorScheme || {};
  params.colorScheme.main = params.colorScheme.main || '#fff';
  params.colorScheme.control = params.colorScheme.control || '#fff';
  params.colorScheme.controlShadow = params.colorScheme.controlShadow || '0px 0px 3px 1px #bbb';
  params.colorScheme.controlContent = params.colorScheme.controlContent || '#000000';
  params.colorScheme.hoverControl = params.colorScheme.hoverControl || params.colorScheme.control;
  params.colorScheme.hoverControlContent = params.colorScheme.hoverControlContent || '#1a3d67';
  params.colorScheme.toolControlNameColor = params.colorScheme.toolControlNameColor || 'rgba(0,0,0,0.07)';

  params.colorScheme.activeControl = params.colorScheme.activeControl || '#7485B1';
  params.colorScheme.activeControlContent = params.colorScheme.activeControlContent ||
    params.colorScheme.main;
  params.colorScheme.inputBorderColor = params.colorScheme.inputBorderColor ||
    params.colorScheme.main;
  params.colorScheme.inputBackground = params.colorScheme.inputBackground || '#ffffff';
  params.colorScheme.inputShadow = params.colorScheme.inputShadow || 'inset 0 0 4px 1px #ccc';

  params.colorScheme.inputText = params.colorScheme.inputText ||
    params.colorScheme.activeControl;
  params.colorScheme.backgroundColor = params.colorScheme.backgroundColor || '#999999';
  params.colorScheme.dragOverBarColor = params.colorScheme.dragOverBarColor || '#899dff';

  params.defaultSize = params.defaultSize || 'fill';
  params.defaultPixelSize = params.defaultPixelSize || 4;
  params.disableWheelZoom = params.disableWheelZoom || false;

  params.extraFonts = params.extraFonts || [];

  params.toolbarHeightPx = params.toolbarHeightPx || 40;
  params.buttonSizePx = params.buttonSizePx || 32;
  params.bucketSensivity = params.bucketSensivity || 100;


  if (typeof params.defaultSize !== 'object') {
    // otherwise its an object from localstorage
    if (params.defaultSize === 'fill') {
      params.defaultSize = {
        width: 'fill',
        height: 'fill',
      };
    } else {
      const wh = params.defaultSize.split('x');
      params.defaultSize = {
        width: trim(wh[0]),
        height: trim(wh[1]),
      };
    }
  }

  params.toolbarPosition = params.toolbarPosition || 'bottom';
  params.fixMobilePageReloader = params.fixMobilePageReloader !== undefined ?
    params.fixMobilePageReloader : true;
  if (params.translation) {
    const name = params.translation.name;
    Translation.get().addTranslation(name, params.translation.strings);
    Translation.get().activate(name);
  }

  params.styles =
    `.ptro-color-main{
      background-color:${params.colorScheme.main};
      color:${params.colorScheme.controlContent};
    }
    .ptro-color-control{
      box-shadow:${params.colorScheme.controlShadow};
      background-color:${params.colorScheme.control};
      color:${params.colorScheme.controlContent}}
    .ptro-tool-ctl-name{
      background-color:${params.colorScheme.toolControlNameColor};
    }
    button.ptro-color-control:hover:not(.ptro-color-active-control):not([disabled]){
        background-color: ${params.colorScheme.hoverControl};
        color:${params.colorScheme.hoverControlContent}}    
    .ptro-bordered-control{border-color: ${params.colorScheme.activeControl}}
    input.ptro-input,.ptro-check,input.ptro-input:focus,select.ptro-input,select.ptro-input:focus {
      border: 1px solid ${params.colorScheme.inputBorderColor};
      background-color: ${params.colorScheme.inputBackground};
      color: ${params.colorScheme.inputText};
      box-shadow:${params.colorScheme.inputShadow};
    }
    .ptro-bar-dragover{background-color:${params.colorScheme.dragOverBarColor}}
    .ptro-color,.ptro-bordered-btn{
      border: 1px solid ${params.colorScheme.inputBorderColor};
    }
    .ptro-color-control:active:enabled {
        background-color: ${params.colorScheme.activeControl};
        color: ${params.colorScheme.activeControlContent}}
    .ptro-color-active-control{
        background-color: ${params.colorScheme.activeControl};
        color:${params.colorScheme.activeControlContent}}
    .ptro-wrapper{
      background-color:${params.colorScheme.backgroundColor};
      bottom:${params.toolbarPosition === 'top' ? '0' : params.toolbarHeightPx}px;
      top:${params.toolbarPosition === 'top' ? params.toolbarHeightPx : '0'}px;
    }
    .ptro-icon-btn {
      height: ${params.buttonSizePx}px;
      width: ${params.buttonSizePx}px;
      margin: 0 0 0 ${(params.toolbarHeightPx - params.buttonSizePx) / 2}px;
    }
    .ptro-bar-right {
      margin-right: ${(params.toolbarHeightPx - params.buttonSizePx) / 2}px;
    }
    .ptro-bar {
      height: ${params.toolbarHeightPx}px;
      ${params.toolbarPosition === 'top' ? 'top' : 'bottom'}: 0;
    }`;

  return params;
}


================================================
FILE: js/primitive.js
================================================
export default class PrimitiveTool {
  constructor(main) {
    this.ctx = main.ctx;
    this.el = main.toolContainer;
    this.main = main;
    this.helperCanvas = document.createElement('canvas');
    this.canvas = main.canvas;
  }

  activate(type) {
    this.type = type;
    this.state = {};
    if (type === 'line' || type === 'brush' || type === 'eraser' || type === 'arrow') {
      this.ctx.lineJoin = 'round';
    } else {
      this.ctx.lineJoin = 'miter';
    }
  }

  setLineWidth(width) {
    if (`${width}`.match(/^\d+$/)) {
      this.lineWidth = +width;
    } else {
      throw Error(`WARN: STR "${width}" is not an int`);
    }
  }

  setShadowOn(state) {
    this.shadowOn = state;
  }

  setArrowLength(length) {
    this.arrowLength = length;
  }

  setEraserWidth(width) {
    this.eraserWidth = width;
  }

  handleMouseDown(event) {
    this.activate(this.type);
    const mainClass = event.target.classList[0];

    this.ctx.lineWidth = this.lineWidth;
    this.ctx.strokeStyle = this.main.colorWidgetState.line.alphaColor;
    this.ctx.fillStyle = this.main.colorWidgetState.fill.alphaColor;
    const scale = this.main.getScale();
    this.ctx.lineCap = 'round';
    if (mainClass === 'ptro-crp-el' || mainClass === 'ptro-zoomer') {
      this.tmpData = this.ctx.getImageData(0, 0, this.main.size.w, this.main.size.h);
      if (this.type === 'brush' || this.type === 'eraser') {
        this.state.cornerMarked = true;
        const cord = [
          (event.clientX - this.main.elLeft()) + this.main.scroller.scrollLeft,
          (event.clientY - this.main.elTop()) + this.main.scroller.scrollTop,
        ];
        const cur = {
          x: cord[0] * scale,
          y: cord[1] * scale,
        };

        this.points = [cur];
        this.drawBrushPath();
      } else {
        this.state.cornerMarked = true;
        this.centerCord = [
          (event.clientX - this.main.elLeft()) + this.main.scroller.scrollLeft,
          (event.clientY - this.main.elTop()) + this.main.scroller.scrollTop,
        ];
        this.centerCord = [this.centerCord[0] * scale, this.centerCord[1] * scale];
      }
    }
  }

  drawBrushPath() {
    const smPoints = this.points;
    let lineFill;
    const origComposition = this.ctx.globalCompositeOperation;
    const isEraser = this.type === 'eraser';
    lineFill = this.main.colorWidgetState.line.alphaColor;
    const bgIsTransparent = this.main.currentBackgroundAlpha !== 1.0;
    for (let i = 1; i <= (isEraser && bgIsTransparent ? 2 : 1); i += 1) {
      if (isEraser) {
        this.ctx.globalCompositeOperation = i === 1 && bgIsTransparent ? 'destination-out' : origComposition;
        lineFill = i === 1 && bgIsTransparent ? 'rgba(0,0,0,1)' : this.main.currentBackground;
      }
      if (smPoints.length === 1) {
        this.ctx.beginPath();
        this.ctx.lineWidth = 0;
        this.ctx.fillStyle = lineFill;
        this.ctx.arc(
          this.points[0].x, this.points[0].y,
          this.lineWidth / 2, this.lineWidth / 2,
          0, 2 * Math.PI);
        this.ctx.fill();
        this.ctx.closePath();
      } else {
        this.ctx.beginPath();
        if (this.type === 'eraser') {
          this.ctx.lineWidth = this.eraserWidth;
        } else {
          this.ctx.lineWidth = this.lineWidth;
        }
        this.ctx.strokeStyle = lineFill;
        this.ctx.fillStyle = this.main.colorWidgetState.fill.alphaColor;

        this.ctx.moveTo(this.points[0].x, this.points[0].y);
        let last;
        smPoints.slice(1).forEach((p) => {
          this.ctx.lineTo(p.x, p.y);
          last = p;
        });
        if (last) {
          this.ctx.moveTo(last.x, last.y);
        }
        this.ctx.stroke();
        this.ctx.closePath();
      }
    }
    this.ctx.globalCompositeOperation = origComposition;
  }

  handleMouseMove(event) {
    const ctx = this.ctx;
    if (this.state.cornerMarked) {
      this.ctx.putImageData(this.tmpData, 0, 0);
      this.curCord = [
        (event.clientX - this.main.elLeft()) + this.main.scroller.scrollLeft,
        (event.clientY - this.main.elTop()) + this.main.scroller.scrollTop,
      ];
      const scale = this.main.getScale();
      this.curCord = [this.curCord[0] * scale, this.curCord[1] * scale];

      if (this.type === 'brush' || this.type === 'eraser') {
        // const prevLast = this.points.slice(-1)[0];
        const cur = {
          x: this.curCord[0],
          y: this.curCord[1],
        };
        this.points.push(cur);
        this.drawBrushPath();
      } else if (this.type === 'line') {
        if (event.ctrlKey || event.shiftKey) {
          const deg = (Math.atan(
            -(this.curCord[1] - this.centerCord[1]) / (this.curCord[0] - this.centerCord[0]),
          ) * 180) / Math.PI;
          if (Math.abs(deg) < 45.0 / 2) {
            this.curCord[1] = this.centerCord[1];
          } else if (Math.abs(deg) > 45.0 + (45.0 / 2)) {
            this.curCord[0] = this.centerCord[0];
          } else {
            const base = (Math.abs(this.curCord[0] - this.centerCord[0])
              - Math.abs(this.centerCord[1] - this.curCord[1])) / 2;

            this.curCord[0] -= base * (this.centerCord[0] < this.curCord[0] ? 1 : -1);
            this.curCord[1] -= base * (this.centerCord[1] > this.curCord[1] ? 1 : -1);
          }
        }
        ctx.beginPath();
        ctx.moveTo(this.centerCord[0], this.centerCord[1]);
        ctx.lineTo(this.curCord[0], this.curCord[1]);
        ctx.closePath();
        const origShadowColor = ctx.shadowColor;
        if (this.shadowOn) {
          ctx.shadowColor = 'rgba(0,0,0,0.7)';
          ctx.shadowBlur = this.lineWidth;
          ctx.shadowOffsetX = this.lineWidth / 2.0;
          ctx.shadowOffsetY = this.lineWidth / 2.0;
        }
        ctx.stroke();
        ctx.shadowColor = origShadowColor;
      } else if (this.type === 'arrow') {
        let deg = (Math.atan(
          -(this.curCord[1] - this.centerCord[1]) / (this.curCord[0] - this.centerCord[0]),
        ) * 180) / Math.PI;
        if (event.ctrlKey || event.shiftKey) {
          if (Math.abs(deg) < 45.0 / 2) {
            this.curCord[1] = this.centerCord[1];
          } else if (Math.abs(deg) > 45.0 + (45.0 / 2)) {
            this.curCord[0] = this.centerCord[0];
          } else {
            const base = (Math.abs(this.curCord[0] - this.centerCord[0])
              - Math.abs(this.centerCord[1] - this.curCord[1])) / 2;

            this.curCord[0] -= base * (this.centerCord[0] < this.curCord[0] ? 1 : -1);
            this.curCord[1] -= base * (this.centerCord[1] > this.curCord[1] ? 1 : -1);
          }
        }
        if (this.curCord[0] < this.centerCord[0]) {
          deg = (180 + deg);
        }
        this.ctx.beginPath();
        const origCap = this.ctx.lineCap;
        const origFill = this.ctx.fillStyle;
        this.ctx.fillStyle = this.main.colorWidgetState.line.alphaColor;
        this.ctx.lineCap = 'square';

        const r = Math.min(this.arrowLength, 0.9 * Math.sqrt(
          ((this.centerCord[0] - this.curCord[0]) ** 2) +
          ((this.centerCord[1] - this.curCord[1]) ** 2)));

        const fromx = this.centerCord[0];
        const fromy = this.centerCord[1];
        const tox = this.curCord[0];
        const toy = this.curCord[1];
        const xCenter = this.curCord[0];
        const yCenter = this.curCord[1];
        let angle;
        let x;
        let y;
        angle = Math.atan2(toy - fromy, tox - fromx);

        x = (r * Math.cos(angle)) + xCenter;
        y = (r * Math.sin(angle)) + yCenter;

        this.ctx.moveTo(x, y);

        angle += (1.0 / 3) * (2 * Math.PI);
        x = (r * Math.cos(angle)) + xCenter;
        y = (r * Math.sin(angle)) + yCenter;
        this.ctx.lineTo(x, y);

        const xTail1 = xCenter + ((x - xCenter) / 3.0);
        const yTail1 = yCenter + ((y - yCenter) / 3.0);
        ctx.lineTo(xTail1, yTail1);

        ctx.lineTo(this.centerCord[0], this.centerCord[1]);

        angle += (1.0 / 3) * (2 * Math.PI);
        x = (r * Math.cos(angle)) + xCenter;
        y = (r * Math.sin(angle)) + yCenter;
        const xTail2 = xCenter + ((x - xCenter) / 3.0);
        const yTail2 = yCenter + ((y - yCenter) / 3.0);
        ctx.lineTo(xTail2, yTail2);

        ctx.lineTo(x, y);
        ctx.closePath();
        const origShadowColor = ctx.shadowColor;
        if (this.shadowOn) {
          ctx.shadowColor = 'rgba(0,0,0,0.7)';
          ctx.shadowBlur = Math.log(r) * this.main.params.shadowScale;
          ctx.shadowOffsetX = Math.log10(r);
          ctx.shadowOffsetY = Math.log10(r);
        }
        ctx.fill();
        ctx.lineCap = origCap;
        ctx.fillStyle = origFill;
        ctx.shadowColor = origShadowColor;
      } else if (this.type === 'rect') {
        ctx.beginPath();

        const tl = [
          this.centerCord[0],
          this.centerCord[1]];

        let w = this.curCord[0] - this.centerCord[0];
        let h = this.curCord[1] - this.centerCord[1];
        if (event.ctrlKey || event.shiftKey) {
          const min = Math.min(Math.abs(w), Math.abs(h));
          w = min * Math.sign(w);
          h = min * Math.sign(h);
        }
        const halfLW = this.lineWidth / 2.0;
        // normalize fix half compensation
        if (w < 0) {
          tl[0] += w;
          w = -w;
        }
        if (h < 0) {
          tl[1] += h;
          h = -h;
        }
        this.ctx.rect(
          tl[0] + halfLW,
          tl[1] + halfLW,
          (w - this.lineWidth),
          (h - this.lineWidth));
        this.ctx.fill();

        const origShadowColor = ctx.shadowColor;
        if (this.shadowOn) {
          ctx.shadowColor = 'rgba(0,0,0,0.7)';
          ctx.shadowBlur = this.lineWidth;
          ctx.shadowOffsetX = this.lineWidth / 2.0;
          ctx.shadowOffsetY = this.lineWidth / 2.0;
        }
        if (this.lineWidth) {
          // TODO: no shadow on unstroked, do we need it?
          this.ctx.strokeRect(tl[0], tl[1], w, h);
        }
        ctx.shadowColor = origShadowColor;

        this.ctx.closePath();
      } else if (this.type === 'ellipse') {
        this.ctx.beginPath();
        const x1 = this.centerCord[0];
        const y1 = this.centerCord[1];
        let w = this.curCord[0] - x1;
        let h = this.curCord[1] - y1;

        if (event.ctrlKey || event.shiftKey) {
          const min = Math.min(Math.abs(w), Math.abs(h));
          w = min * Math.sign(w);
          h = min * Math.sign(h);
        }

        const rX = Math.abs(w);
        const rY = Math.abs(h);

        const tlX = Math.min(x1, x1 + w);
        const tlY = Math.min(y1, y1 + h);

        this.ctx.save();
        let xScale = 1;
        let yScale = 1;
        let radius;
        const hR = rX / 2;
        const vR = rY / 2;
        if (rX > rY) {
          yScale = rX / rY;
          radius = hR;
        } else {
          xScale = rY / rX;
          radius = vR;
        }
        this.ctx.scale(1 / xScale, 1 / yScale);
        this.ctx.arc(
          (tlX + hR) * xScale,
          (tlY + vR) * yScale,
          radius, 0, 2 * Math.PI);
        this.ctx.restore();
        this.ctx.fill();
        const origShadowColor = ctx.shadowColor;
        if (this.shadowOn) {
          ctx.shadowColor = 'rgba(0,0,0,0.7)';
          ctx.shadowBlur = this.lineWidth;
          ctx.shadowOffsetX = this.lineWidth / 2.0;
          ctx.shadowOffsetY = this.lineWidth / 2.0;
        }
        ctx.stroke();
        ctx.shadowColor = origShadowColor;
        this.ctx.beginPath();
      }
    }
  }

  handleMouseUp() {
    if (this.state.cornerMarked) {
      this.state.cornerMarked = false;
      this.main.worklog.captureState();
    }
  }

  setPixelSize(size) {
    this.pixelSize = size;
  }
}


================================================
FILE: js/resizer.js
================================================
import { tr } from './translation';
import { KEYS } from './utils';

export default class Resizer {
  constructor(main) {
    this.main = main;

    this.wrapper = main.wrapper.querySelector('.ptro-resize-widget-wrapper');
    this.inputW = main.wrapper.querySelector('.ptro-resize-widget-wrapper .ptro-resize-width-input');
    this.inputH = main.wrapper.querySelector('.ptro-resize-widget-wrapper .ptro-resize-heigth-input');

    this.inputWLimit = 10000;
    this.inputHLimit = 13000;

    this.linkButton = main.wrapper.querySelector('.ptro-resize-widget-wrapper button.ptro-link');
    this.linkButtonIcon = main.wrapper.querySelector('.ptro-resize-widget-wrapper button.ptro-link i');
    this.closeButton = main.wrapper.querySelector('.ptro-resize-widget-wrapper button.ptro-close');
    this.scaleButton = main.wrapper.querySelector('.ptro-resize-widget-wrapper button.ptro-scale');
    this.resizeButton = main.wrapper.querySelector('.ptro-resize-widget-wrapper button.ptro-resize');
    this.linked = true;
    this.closeButton.onclick = () => {
      this.startClose();
    };

    this.scaleButton.onclick = () => {
      if (!Resizer.validationZeroValue(this.newH, this.newW)) return;
      const origW = this.main.size.w;
      const origH = this.main.size.h;

      const tmpData = this.main.canvas.toDataURL();

      this.main.resize(this.newW, this.newH);

      this.main.ctx.save();
      // this.ctx.translate(h / 2, w / 2);
      this.main.ctx.scale(this.newW / origW, this.newH / origH);
      const img = new Image();
      img.onload = () => {
        this.main.ctx.drawImage(img, 0, 0);
        this.main.adjustSizeFull();
        this.main.ctx.restore();
        this.main.worklog.captureState();
        this.startClose();
      };
      img.src = tmpData;
    };

    this.resizeButton.onclick = () => {
      if (!Resizer.validationZeroValue(this.newH, this.newW)) return;
      const tmpData = this.main.canvas.toDataURL();
      this.main.resize(this.newW, this.newH);
      this.main.clearBackground();
      const img = new Image();
      img.onload = () => {
        this.main.ctx.drawImage(img, 0, 0);
        this.main.adjustSizeFull();
        this.main.worklog.captureState();
        this.startClose();
      };
      img.src = tmpData;
    };

    this.linkButton.onclick = () => {
      this.linked = !this.linked;
      if (this.linked) {
        this.linkButtonIcon.className = 'ptro-icon ptro-icon-linked';
      } else {
        this.linkButtonIcon.className = 'ptro-icon ptro-icon-unlinked';
      }
    };

    this.inputW.oninput = () => {
      const widthVal = Number(this.inputW.value);
      this.validationWidth(widthVal);
      if (this.linked) {
        const ratio = this.main.size.ratio;
        this.newH = Math.round(this.newW / ratio);
        this.validationHeight(this.newH);
        this.inputH.value = this.newH;
      }
    };
    this.inputH.oninput = () => {
      const heightVal = Number(this.inputH.value);
      this.validationHeight(heightVal);
      if (this.linked) {
        const ratio = this.main.size.ratio;
        this.newW = Math.round(this.newH * ratio);
        this.validationWidth(this.newW);
        this.inputW.value = +this.newW;
      }
    };
  }

  validationWidthValue(value) {
    return value <= this.inputWLimit;
  }

  validationHeightValue(value) {
    return value <= this.inputHLimit;
  }

  static validationEmptyValue(value) {
    return value !== '' || value !== '0';
  }

  static validationZeroValue(...args) {
    let isValid = true;
    args.forEach((v) => {
      isValid = !(v === 0) && isValid;
    });
    return isValid;
  }

  validationHeight(value) {
    if (this.validationHeightValue(value)) {
      this.newH = value;
    } else {
      this.inputH.value = this.inputHLimit;
      this.newH = this.inputHLimit;
      return;
    }

    if (Resizer.validationEmptyValue(value)) {
      this.newH = value;
    } else {
      this.inputH.value = 0;
      this.newH = 0;
    }
  }

  validationWidth(value) {
    if (this.validationWidthValue(value)) {
      this.newW = value;
    } else {
      this.inputW.value = this.inputWLimit;
      this.newW = this.inputWLimit;
      return;
    }

    if (Resizer.validationEmptyValue(value)) {
      this.newW = value;
    } else {
      this.inputW.value = '0';
      this.newW = 0;
    }
  }

  open() {
    this.wrapper.removeAttribute('hidden');
    this.opened = true;
    this.newW = this.main.size.w;
    this.newH = this.main.size.h;
    this.inputW.value = +this.newW;
    this.inputH.value = +this.newH;
  }

  close() {
    this.wrapper.setAttribute('hidden', 'true');
    this.opened = false;
  }

  startClose() {
    this.main.closeActiveTool();
  }

  handleKeyDown(event) {
    if (event.keyCode === KEYS.enter) {
      return true; // mark as handled - user might expect doing save by enter
    }
    if (event.keyCode === KEYS.esc) {
      this.startClose();
      return true;
    }
    return false;
  }

  static html() {
    return '' +
      '<div class="ptro-resize-widget-wrapper ptro-common-widget-wrapper ptro-v-middle" hidden>' +
        '<div class="ptro-resize-widget ptro-color-main ptro-v-middle-in">' +
          '<div style="display: inline-block">' +
            '<table>' +
              '<tr>' +
                `<td class="ptro-label ptro-resize-table-left">${tr('width')}</td>` +
                '<td>' +
                  '<input class="ptro-input ptro-resize-width-input" type="number" min="0" max="3000" step="1"/>' +
                '</td>' +
              '</tr>' +
              '<tr>' +
                `<td class="ptro-label ptro-resize-table-left">${tr('height')}</td>` +
                '<td>' +
                  '<input class="ptro-input ptro-resize-heigth-input" type="number" min="0" max="3000" step="1"/>' +
                '</td>' +
              '</tr>' +
            '</table>' +
          '</div>' +
          '<div class="ptro-resize-link-wrapper">' +
            `<button type="button" aria-label="button resize" class="ptro-icon-btn ptro-link ptro-color-control" title="${tr('keepRatio')}">` +
              '<i class="ptro-icon ptro-icon-linked" style="font-size: 18px;"></i>' +
            '</button>' +
          '</div>' +
          '<div></div>' +
          '<div style="margin-top: 40px;">' +
            '<button type="button" aria-label="resize dimentions" class="ptro-named-btn ptro-resize ptro-color-control">' +
                  `${tr('resizeResize')}</button>` +
            '<button type="button" aria-label="resize scale" class="ptro-named-btn ptro-scale ptro-color-control">' +
                  `${tr('resizeScale')}</button>` +
            '<button type="button" aria-label="cancel" class="ptro-named-btn ptro-close ptro-color-control">' +
                  `${tr('cancel')}</button>` +
          '</div>' +
        '</div>' +
      '</div>';
  }
}


================================================
FILE: js/selecter.js
================================================
import { clearSelection, KEYS } from './utils';

export default class PainterroSelecter {
  constructor(main, selectionCallback) {
    this.main = main;
    this.canvas = main.canvas;
    this.wrapper = main.wrapper;
    this.ctx = main.ctx;
    this.areaionCallback = selectionCallback;
    this.shown = false;
    this.area = {
      el: main.toolContainer,
      rect: document.querySelector(`#${main.id} .ptro-crp-rect`),
    };
    this.imagePlaced = false;
    this.areaionCallback(false);
  }

  static code() {
    return '<div class="ptro-crp-rect" hidden>' +
      '<div class="ptro-crp-l select-handler"></div><div class="ptro-crp-r select-handler"></div>' +
      '<div class="ptro-crp-t select-handler"></div><div class="ptro-crp-b select-handler"></div>' +
      '<div class="ptro-crp-tl select-handler"></div><div class="ptro-crp-tr select-handler"></div>' +
      '<div class="ptro-crp-bl select-handler"></div><div class="ptro-crp-br select-handler"></div>' +
      '</div>';
  }

  activate() {
    this.area.activated = true;
    this.areaionCallback(false);
  }

  doCrop() {
    const imgData = this.ctx.getImageData(0, 0, this.main.size.w, this.main.size.h);
    this.main.resize(
      this.area.bottoml[0] - this.area.topl[0],
      this.area.bottoml[1] - this.area.topl[1]);
    this.main.ctx.putImageData(imgData, -this.area.topl[0], -this.area.topl[1]);
    this.main.adjustSizeFull();
    this.main.worklog.captureState();
  }

  doPixelize() {
    const c = this.area.topl;
    const size = [
      this.area.bottoml[0] - c[0], // width
      this.area.bottoml[1] - c[1],
    ];

    this.pixelizePixelSize = this.main.params.pixelizePixelSize;

    if (this.pixelizePixelSize.slice(-1) === '%') {
      this.pixelSize = (Math.min(size[0], size[1]) / (100.0 / this.pixelizePixelSize.slice(0, -1)));
    } else if (this.pixelizePixelSize.slice(-2).toLowerCase() === 'px') {
      this.pixelSize = this.pixelizePixelSize.slice(0, -2);
    } else {
      this.pixelSize = this.pixelizePixelSize;
    }


    if (this.pixelSize < 2) {
      this.pixelSize = 2; // prevent errors
    }

    if (size[1] < size[0]) {
      this.pixelSizeY = this.pixelSize;
      const desiredHorPxs = Math.round(size[0] / this.pixelSizeY);
      this.pixelSizeX = (size[0] * 1.0) / desiredHorPxs;
    } else {
      this.pixelSizeX = this.pixelSize;
      const desiredVerPxs = Math.round(size[1] / this.pixelSizeX);
      this.pixelSizeY = (size[1] * 1.0) / desiredVerPxs;
    }
    const pxData = [];
    const pxSize = [size[0] / this.pixelSizeX, size[1] / this.pixelSizeY];
    for (let i = 0; i < pxSize[0]; i += 1) {
      const row = [];
      for (let j = 0; j < pxSize[1]; j += 1) {
        row.push([0, 0, 0, 0, 0]);
      }
      pxData.push(row);
    }
    const data = this.ctx.getImageData(c[0], c[1], size[0], size[1]);
    for (let i = 0; i < size[0]; i += 1) {
      for (let j = 0; j < size[1]; j += 1) {
        const ii = Math.floor(i / this.pixelSizeX);
        const jj = Math.floor(j / this.pixelSizeY);
        const base = ((j * size[0]) + i) * 4;
        pxData[ii][jj][0] += data.data[base];
        pxData[ii][jj][1] += data.data[base + 1];
        pxData[ii][jj][2] += data.data[base + 2];
        pxData[ii][jj][3] += data.data[base + 3];
        pxData[ii][jj][4] += 1;
      }
    }
    for (let i = 0; i < pxSize[0]; i += 1) {
      for (let j = 0; j < pxSize[1]; j += 1) {
        const s = pxData[i][j][4];
        this.ctx.fillStyle = `rgba(
${Math.round(pxData[i][j][0] / s)}, 
${Math.round(pxData[i][j][1] / s)}, 
${Math.round(pxData[i][j][2] / s)}, 
${Math.round(pxData[i][j][3] / s)})`;
        const baseX = c[0] + (i * this.pixelSizeX);
        const baseY = c[1] + (j * this.pixelSizeY);
        this.ctx.fillRect(baseX, baseY, this.pixelSizeX, this.pixelSizeY);
      }
    }
    this.main.worklog.captureState();
  }

  doClearArea() {
    this.ctx.beginPath();
    this.ctx.clearRect(
      this.area.topl[0], this.area.topl[1],
      this.area.bottoml[0] - this.area.topl[0], this.area.bottoml[1] - this.area.topl[1]);
    this.ctx.rect(this.area.topl[0], this.area.topl[1],
      this.area.bottoml[0] - this.area.topl[0], this.area.bottoml[1] - this.area.topl[1]);
    this.ctx.fillStyle = this.main.currentBackground;
    this.ctx.fill();
    this.main.worklog.captureState();
  }

  selectAll() {
    this.setLeft(0);
    this.setRight(0);
    this.setBottom(0);
    this.setTop(0);
    this.show();
    this.reCalcCropperCords();
    if (this.area.activated) {
      this.areaionCallback(!this.imagePlaced &&
        this.area.rect.clientWidth > 0 &&
        this.area.rect.clientHeight > 0);
    }
  }

  getScale() {
    return this.canvas.clientWidth / this.canvas.getAttribute('width');
  }

  reCalcCropperCords() {
    const ratio = this.getScale();
    this.area.topl = [
      Math.round(((this.rectLeft() - this.main.elLeft())) / ratio),
      Math.round(((this.rectTop() - this.main.elTop())) / ratio)];

    this.area.bottoml = [
      Math.round(this.area.topl[0] + ((this.area.rect.clientWidth + 2) / ratio)),
      Math.round(this.area.topl[1] + ((this.area.rect.clientHeight
Download .txt
gitextract_hkg7u874/

├── .browserslistrc
├── .github/
│   ├── FUNDING.yml
│   └── ISSUE_TEMPLATE/
│       ├── bug_report.md
│       └── feature_request.md
├── .gitignore
├── .npmignore
├── CONTRIBUTORS.md
├── LICENSE
├── README.md
├── Release.md
├── build/
│   ├── contained.html
│   └── index.html
├── css/
│   ├── bar-styles.css
│   ├── icons/
│   │   ├── ptroiconfont.css
│   │   └── ptroiconfont.html
│   └── styles.css
├── example/
│   ├── server.py
│   └── templates/
│       ├── common.html
│       ├── images_list.html
│       ├── paste_as_base64.html
│       ├── paste_as_bin.html
│       └── paste_to_tinymce.html
├── generate_font.js
├── js/
│   ├── colorPicker.js
│   ├── controlbuilder.js
│   ├── customEvents.js
│   ├── filters.js
│   ├── inserter.js
│   ├── main.js
│   ├── paintBucket.js
│   ├── params.js
│   ├── primitive.js
│   ├── resizer.js
│   ├── selecter.js
│   ├── settings.js
│   ├── text.js
│   ├── translation.js
│   ├── utils.js
│   ├── worklog.js
│   └── zoomHelper.js
├── langs/
│   ├── ca.lang.js
│   ├── de.lang.js
│   ├── en.lang.js
│   ├── es.lang.js
│   ├── fa.lang.js
│   ├── fr.lang.js
│   ├── ja.lang.js
│   ├── nl.lang.js
│   ├── pl.lang.js
│   ├── pt-BR.lang.js
│   ├── pt-PT.lang.js
│   ├── ru.lang.js
│   └── uk.lang.js
├── package.json
├── publish.sh
├── res/
│   └── font/
│       └── font-css.hbs
└── webpack.config.js
Download .txt
SYMBOL INDEX (249 symbols across 19 files)

FILE: example/server.py
  function get_tmp_dir (line 14) | def get_tmp_dir():
  function base64_page (line 22) | def base64_page():
  function bin_page (line 30) | def bin_page():
  function paste_page (line 38) | def paste_page():
  function base64_saver (line 43) | def base64_saver():
  function binary_saver (line 54) | def binary_saver():
  function get_file (line 63) | def get_file(filename):

FILE: js/colorPicker.js
  function HexToRGB (line 4) | function HexToRGB(hex) {
  function HexToRGBA (line 23) | function HexToRGBA(hex, alpha) {
  function format2Hex (line 28) | function format2Hex(val) {
  function rgbToHex (line 33) | function rgbToHex(r, g, b) {
  function reversedColor (line 37) | function reversedColor(color) {
  class ColorPicker (line 43) | class ColorPicker {
    method constructor (line 44) | constructor(main, callback) {
    method open (line 104) | open(state, addCallback) {
    method close (line 120) | close() {
    method getPaletteColorAtPoint (line 125) | getPaletteColorAtPoint(e) {
    method regetColor (line 138) | regetColor() {
    method regetAlpha (line 144) | regetAlpha() {
    method getColorLightAtClick (line 150) | getColorLightAtClick(e) {
    method getAlphaAtClick (line 159) | getAlphaAtClick(e) {
    method handleKeyDown (line 168) | handleKeyDown(event) {
    method handleMouseDown (line 179) | handleMouseDown(e) {
    method handleMouseMove (line 201) | handleMouseMove(e) {
    method handleMouseUp (line 243) | handleMouseUp() {
    method setActiveColor (line 252) | setActiveColor(color, ignoreUpdateText) {
    method html (line 284) | static html() {
    method drawLighter (line 307) | drawLighter() {
    method drawAlpher (line 317) | drawAlpher() {

FILE: js/controlbuilder.js
  class ControlBuilder (line 3) | class ControlBuilder {
    method constructor (line 4) | constructor(main) {
    method buildFontSizeControl (line 8) | buildFontSizeControl(controlIndex) {
    method buildEraserWidthControl (line 23) | buildEraserWidthControl(controlIndex) {
    method buildLineWidthControl (line 37) | buildLineWidthControl(controlIndex) {
    method buildShadowOnControl (line 51) | buildShadowOnControl(controlIndex) {
    method buildPaintBucketControl (line 68) | buildPaintBucketControl(controlIndex) {
    method buildArrowLengthControl (line 80) | buildArrowLengthControl(controlIndex) {
    method buildInputControl (line 94) | static buildInputControl(name, action, getValue, minVal, maxVal) {
    method buildDropDownControl (line 107) | static buildDropDownControl(name, action, getValue, availableValues) {

FILE: js/customEvents.js
  class CustomEvents (line 2) | class CustomEvents {
    method constructor (line 3) | constructor(element) {
    method createEvent (line 6) | createEvent(name, detail) {
    method dispatchEvent (line 9) | dispatchEvent(name, detail) {
    method addEventListener (line 12) | addEventListener(name, callback) {
    method removeEventListener (line 16) | removeEventListener(name, callback) {
    method useCustomEvent (line 20) | useCustomEvent(name, details) {

FILE: js/filters.js
  class Filters (line 5) | class Filters {
    method constructor (line 6) | constructor(main) {
    method createFiltersForApply (line 22) | createFiltersForApply() {
    method createFilterString (line 30) | createFilterString() {
    method getFilters (line 40) | getFilters() {
    method setFilter (line 49) | setFilter(filter) {
    method setPercents (line 59) | setPercents(value) {
    method getFilter (line 68) | getFilter() {
    method applyFilter (line 73) | applyFilter() {
    method saveInitImg (line 90) | saveInitImg() {
    method open (line 95) | open() {
    method close (line 100) | close() {
    method startClose (line 105) | startClose() {
    method getValue (line 109) | getValue(){
    method html (line 113) | static html (main){

FILE: js/inserter.js
  class Inserter (line 4) | class Inserter {
    method constructor (line 5) | constructor(main) {
    method init (line 145) | init(main) {
    method insert (line 169) | insert(x, y, w, h) {
    method cancelChoosing (line 174) | cancelChoosing() {
    method loaded (line 179) | loaded(img, mimetype) {
    method getAvailableOptions (line 189) | getAvailableOptions() {
    method handleOpen (line 199) | handleOpen(src, mimetype) {
    method handleKeyDown (line 245) | handleKeyDown(evt) {
    method startLoading (line 256) | startLoading() {
    method finishLoading (line 270) | finishLoading() {
    method get (line 286) | static get(main) {
    method controlObjToString (line 294) | static controlObjToString(o, btnClassName = '') {
    method html (line 303) | html() {

FILE: js/main.js
  class PainterroProc (line 26) | class PainterroProc {
    method constructor (line 27) | constructor(params) {
    method setToolEnabled (line 826) | setToolEnabled(tool, state) {
    method getAsUri (line 836) | getAsUri(type, quality) {
    method getBtnEl (line 844) | getBtnEl(tool) {
    method save (line 848) | save() {
    method close (line 884) | close() {
    method closeActiveTool (line 890) | closeActiveTool(doNotSelect) {
    method handleToolEvent (line 908) | handleToolEvent(eventHandler, event) {
    method handleClipCopyEvent (line 922) | handleClipCopyEvent(evt) {
    method zoomImage (line 950) | zoomImage({ wheelDelta, clientX, clientY }, forceWheenDelta, forceZoom...
    method initEventHandlers (line 986) | initEventHandlers() {
    method attachEventHandlers (line 1191) | attachEventHandlers() {
    method removeEventHandlers (line 1207) | removeEventHandlers() {
    method elLeft (line 1221) | elLeft() {
    method elTop (line 1225) | elTop() {
    method fitImage (line 1229) | fitImage(img, mimetype) {
    method loadImage (line 1239) | loadImage(source, mimetype) {
    method show (line 1243) | show(openImage, originalMime) {
    method hide (line 1268) | hide() {
    method setZoom (line 1284) | setZoom(zoomPercentage) {
    method doScale (line 1332) | doScale({width, height, scale}) {
    method openFile (line 1350) | openFile(f) {
    method getScale (line 1359) | getScale() {
    method adjustSizeFull (line 1363) | adjustSizeFull() {
    method resize (line 1399) | resize(x, y) {
    method syncToolElement (line 1410) | syncToolElement() {
    method clear (line 1425) | clear() {
    method clearBackground (line 1467) | clearBackground() {
    method setColor (line 1475) | setColor(options){
    method setLineWidth (line 1490) | setLineWidth(width) {
    method setArrowLength (line 1494) | setArrowLength(length) {
    method setEraserWidth (line 1498) | setEraserWidth(width) {
    method setPixelSize (line 1502) | setPixelSize(size) {
    method setShadowOn (line 1506) | setShadowOn(state) {
    method setActiveTool (line 1511) | setActiveTool(b) {

FILE: js/paintBucket.js
  class PaintBucket (line 3) | class PaintBucket {
    method constructor (line 4) | constructor(main) {
    method init (line 12) | init() {
    method handleMouseDown (line 24) | handleMouseDown(event) {
    method paintAt (line 49) | paintAt(startX, startY) {
    method matchStartColor (line 79) | matchStartColor(pixelPos) {
    method matchClickedColor (line 85) | matchClickedColor(r, g, b, a) {
    method getClickedOnColor (line 95) | getClickedOnColor(x, y) {
    method floodFill (line 101) | floodFill(startX, startY, startR, startG, startB) {
    method colorPixel (line 173) | colorPixel(pixelPos, r, g, b, a) {
    method getPixelColor (line 180) | getPixelColor(ctx, pixelPos) {

FILE: js/params.js
  constant STORAGE_KEY (line 6) | const STORAGE_KEY = 'painterro-data';
  function loadSettings (line 10) | function loadSettings() {
  function setParam (line 21) | function setParam(name, val) {
  function firstDefined (line 30) | function firstDefined(...vals) {
  function setDefaults (line 41) | function setDefaults(parameters, allToolsNames) {

FILE: js/primitive.js
  class PrimitiveTool (line 1) | class PrimitiveTool {
    method constructor (line 2) | constructor(main) {
    method activate (line 10) | activate(type) {
    method setLineWidth (line 20) | setLineWidth(width) {
    method setShadowOn (line 28) | setShadowOn(state) {
    method setArrowLength (line 32) | setArrowLength(length) {
    method setEraserWidth (line 36) | setEraserWidth(width) {
    method handleMouseDown (line 40) | handleMouseDown(event) {
    method drawBrushPath (line 75) | drawBrushPath() {
    method handleMouseMove (line 123) | handleMouseMove(event) {
    method handleMouseUp (line 347) | handleMouseUp() {
    method setPixelSize (line 354) | setPixelSize(size) {

FILE: js/resizer.js
  class Resizer (line 4) | class Resizer {
    method constructor (line 5) | constructor(main) {
    method validationWidthValue (line 94) | validationWidthValue(value) {
    method validationHeightValue (line 98) | validationHeightValue(value) {
    method validationEmptyValue (line 102) | static validationEmptyValue(value) {
    method validationZeroValue (line 106) | static validationZeroValue(...args) {
    method validationHeight (line 114) | validationHeight(value) {
    method validationWidth (line 131) | validationWidth(value) {
    method open (line 148) | open() {
    method close (line 157) | close() {
    method startClose (line 162) | startClose() {
    method handleKeyDown (line 166) | handleKeyDown(event) {
    method html (line 177) | static html() {

FILE: js/selecter.js
  class PainterroSelecter (line 3) | class PainterroSelecter {
    method constructor (line 4) | constructor(main, selectionCallback) {
    method code (line 19) | static code() {
    method activate (line 28) | activate() {
    method doCrop (line 33) | doCrop() {
    method doPixelize (line 43) | doPixelize() {
    method doClearArea (line 112) | doClearArea() {
    method selectAll (line 124) | selectAll() {
    method getScale (line 138) | getScale() {
    method reCalcCropperCords (line 142) | reCalcCropperCords() {
    method adjustPosition (line 153) | adjustPosition() {
    method placeAt (line 165) | placeAt(l, t, r, b, img) {
    method finishPlacing (line 203) | finishPlacing() {
    method cancelPlacing (line 214) | cancelPlacing() {
    method handleKeyDown (line 221) | handleKeyDown(evt) {
    method handleMouseDown (line 247) | handleMouseDown(event) {
    method setLeft (line 316) | setLeft(v) {
    method setRight (line 321) | setRight(v) {
    method setTop (line 326) | setTop(v) {
    method setBottom (line 331) | setBottom(v) {
    method handleMouseMove (line 336) | handleMouseMove(event) {
    method leftKeepRatio (line 436) | leftKeepRatio() {
    method topKeepRatio (line 444) | topKeepRatio() {
    method bottomKeepRatio (line 451) | bottomKeepRatio() {
    method rightKeepRatio (line 459) | rightKeepRatio() {
    method show (line 467) | show() {
    method handleMouseUp (line 472) | handleMouseUp() {
    method close (line 487) | close() {
    method hide (line 495) | hide() {
    method draw (line 501) | draw() {
    method rectLeft (line 513) | rectLeft() {
    method rectTop (line 517) | rectTop() {
    method fixCropperLeft (line 522) | fixCropperLeft(left) {
    method fixCropperRight (line 537) | fixCropperRight(right) {
    method fixCropperTop (line 553) | fixCropperTop(top) {
    method fixCropperBottom (line 568) | fixCropperBottom(bottom) {

FILE: js/settings.js
  class Settings (line 5) | class Settings {
    method constructor (line 6) | constructor(main) {
    method handleKeyDown (line 59) | handleKeyDown(event) {
    method open (line 70) | open() {
    method close (line 79) | close() {
    method startClose (line 84) | startClose() {
    method html (line 90) | static html(main) {

FILE: js/text.js
  class TextTool (line 5) | class TextTool {
    method constructor (line 6) | constructor(main) {
    method getFont (line 33) | getFont() {
    method getFonts (line 37) | getFonts() {
    method setFont (line 65) | setFont(font) {
    method setStrokeOn (line 76) | setStrokeOn(state) {
    method setFontIsBold (line 81) | setFontIsBold(state) {
    method setFontIsItalic (line 95) | setFontIsItalic(state) {
    method setFontSize (line 108) | setFontSize(size) {
    method setStrokeParams (line 117) | setStrokeParams() {
    method setFontColor (line 129) | setFontColor(color) {
    method inputLeft (line 135) | inputLeft() {
    method inputTop (line 139) | inputTop() {
    method reLimit (line 143) | reLimit() {
    method handleMouseDown (line 161) | handleMouseDown(event) {
    method apply (line 203) | apply() {
    method close (line 241) | close() {
    method code (line 246) | static code() {

FILE: js/translation.js
  class Translation (line 16) | class Translation {
    method constructor (line 17) | constructor() {
    method get (line 35) | static get() {
    method addTranslation (line 43) | addTranslation(name, dict) {
    method activate (line 47) | activate(trans) {
    method tr (line 56) | tr(sentense) {
  function activate (line 69) | function activate(a) {
  function tr (line 72) | function tr(n) {

FILE: js/utils.js
  function genId (line 1) | function genId() {
  function addDocumentObjectHelpers (line 11) | function addDocumentObjectHelpers() {
  function clearSelection (line 54) | function clearSelection() {
  function distance (line 71) | function distance(p1, p2) {
  function trim (line 77) | function trim(s) {
  constant KEYS (line 81) | const KEYS = {
  function copyToClipboard (line 108) | function copyToClipboard(text) {
  function getScrollbarWidth (line 131) | function getScrollbarWidth() {
  function imgToDataURL (line 147) | function imgToDataURL(url, callback, failedCb) {
  function logError (line 166) | function logError(error) {
  function checkIn (line 170) | function checkIn(what, where) {
  function setPrimitiveToolValue (line 174) | function setPrimitiveToolValue(value, primitiveTool, method, param) {

FILE: js/worklog.js
  class WorkLog (line 1) | class WorkLog {
    method constructor (line 2) | constructor(main, changedHandler) {
    method getWorklogAsString (line 11) | getWorklogAsString(params) {
    method loadWorklogFromString (line 34) | loadWorklogFromString(str) {
    method changed (line 44) | changed(initial) {
    method captureState (line 59) | captureState(initial) {
    method reCaptureState (line 86) | reCaptureState() {
    method applyState (line 93) | applyState(state) {
    method undoState (line 100) | undoState() {
    method redoState (line 119) | redoState() {

FILE: js/zoomHelper.js
  class ZoomHelper (line 1) | class ZoomHelper {
    method constructor (line 2) | constructor(main) {
    method handleMouseMove (line 33) | handleMouseMove(e) {
    method hideZoomHelper (line 88) | hideZoomHelper() {
    method html (line 94) | static html() {

FILE: webpack.config.js
  function webpackConfig (line 8) | function webpackConfig(target, mode) {
Condensed preview — 57 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (289K chars).
[
  {
    "path": ".browserslistrc",
    "chars": 8,
    "preview": "defaults"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 614,
    "preview": "# These are supported funding model platforms\n\ngithub: # [devforth]\npatreon: devforth\nopen_collective: # Replace with a "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 553,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 324,
    "preview": "---\nname: Feature request\nabout: Create a feature which makes Painterro better\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n"
  },
  {
    "path": ".gitignore",
    "chars": 936,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directo"
  },
  {
    "path": ".npmignore",
    "chars": 932,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directo"
  },
  {
    "path": "CONTRIBUTORS.md",
    "chars": 209,
    "preview": "\n\nPainterro contributors\n============================================\n\n* **[Jesfery](https://github.com/Jesfery)**\n\n  * "
  },
  {
    "path": "LICENSE",
    "chars": 1080,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2017 Ivan Borshchov\n\nPermission is hereby granted, free of charge, to any person ob"
  },
  {
    "path": "README.md",
    "chars": 30763,
    "preview": "<img src=\"https://raw.githubusercontent.com/devforth/painterro/master/res/painterro.png\" align=\"right\" style=\"padding:5p"
  },
  {
    "path": "Release.md",
    "chars": 109,
    "preview": "\n\nnpm login\n\nTocken from github:\n\nGH_PASS=`cat ~/.ghtoken`\n\nPassword from WP:\nWP_PASSWORD=`cat ~/.wppassword`"
  },
  {
    "path": "build/contained.html",
    "chars": 1089,
    "preview": "<html>\n  <head>\n    <title>Painterro demo</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1"
  },
  {
    "path": "build/index.html",
    "chars": 4386,
    "preview": "<html>\n  <head>\n    <title>Painterro demo</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1"
  },
  {
    "path": "css/bar-styles.css",
    "chars": 3018,
    "preview": ".color-diwget-btn {\n    height: 32px;\n    width: 32px;\n    cursor: pointer;\n    z-index: 1;\n}\n\n.color-diwget-btn-substra"
  },
  {
    "path": "css/icons/ptroiconfont.css",
    "chars": 2322,
    "preview": "@font-face {\n\tfont-family: \"ptroiconfont\";\n\tsrc: url(\"ptroiconfont.woff?9d16276326db52747d3405a7ca0e1306\") format(\"woff\""
  },
  {
    "path": "css/icons/ptroiconfont.html",
    "chars": 8110,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<title>ptroiconfont</title>\n\t<style>\n\t\tbody {\n\t\t\tfont-f"
  },
  {
    "path": "css/styles.css",
    "chars": 12031,
    "preview": ".ptro-wrapper {\r\n    position: absolute;\r\n    top: 0;\r\n    left: 0;\r\n    right: 0;\r\n    text-align: center;\r\n    z-index"
  },
  {
    "path": "example/server.py",
    "chars": 2223,
    "preview": "import base64\nimport os\nimport tempfile\nfrom time import time\nfrom flask import Flask, jsonify\nfrom flask.globals import"
  },
  {
    "path": "example/templates/common.html",
    "chars": 280,
    "preview": "<div>\n  Painterro demo: <a href=\"/\">Upload as base64 DEMO</a> | <a href=\"/bin/\">Upload as binary DEMO</a> |\n  <a href=\"/"
  },
  {
    "path": "example/templates/images_list.html",
    "chars": 305,
    "preview": "<div>\n  {% for f in files %}\n  <div class=\"img-browse\">\n    <a href=\"/image/{{f}}\"><img src=\"/image/{{f}}\"/></a>\n  </div"
  },
  {
    "path": "example/templates/paste_as_base64.html",
    "chars": 693,
    "preview": "<html>\n  <head>\n    <script src=\"http://localhost:8080/painterro.min.js\"></script>\n  </head>\n  <body>\n    {% include 'co"
  },
  {
    "path": "example/templates/paste_as_bin.html",
    "chars": 628,
    "preview": "<html>\n<head>\n  <script src=\"http://localhost:8080/painterro.min.js\"></script>\n</head>\n<body>\n  {% include 'common.html'"
  },
  {
    "path": "example/templates/paste_to_tinymce.html",
    "chars": 1160,
    "preview": "<html>\n  <script src=\"http://localhost:8080/painterro.min.js\"></script>\n  <script src=\"https://cloud.tinymce.com/stable/"
  },
  {
    "path": "generate_font.js",
    "chars": 811,
    "preview": "const webfontsGenerator = require('@vusion/webfonts-generator');\nconst fs = require('fs');\n\nfs.readdir('res/font', funct"
  },
  {
    "path": "js/colorPicker.js",
    "chars": 10969,
    "preview": "import { tr } from './translation';\nimport { KEYS } from './utils';\n\nexport function HexToRGB(hex) {\n  let parse = /^#?("
  },
  {
    "path": "js/controlbuilder.js",
    "chars": 4203,
    "preview": "import { setParam } from './params';\n\nexport default class ControlBuilder {\n  constructor(main) {\n    this.main = main;\n"
  },
  {
    "path": "js/customEvents.js",
    "chars": 555,
    "preview": "\nexport default class CustomEvents {\n  constructor(element) {\n    this.element = element;\n  }\n  createEvent(name, detail"
  },
  {
    "path": "js/filters.js",
    "chars": 4285,
    "preview": "import { filter } from 'lodash';\nimport { tr } from './translation.js';\nimport { KEYS } from './utils.js';\n\nexport defau"
  },
  {
    "path": "js/inserter.js",
    "chars": 11519,
    "preview": "import { tr } from './translation';\nimport { genId, KEYS, imgToDataURL } from './utils';\n\nexport default class Inserter "
  },
  {
    "path": "js/main.js",
    "chars": 52749,
    "preview": "import isMobile from 'ismobilejs';\n\nimport '../css/styles.css';\nimport '../css/bar-styles.css';\nimport '../css/icons/ptr"
  },
  {
    "path": "js/paintBucket.js",
    "chars": 5842,
    "preview": "import ColorPicker, { HexToRGB, rgbToHex } from './colorPicker';\n\nexport default class PaintBucket {\n  constructor(main)"
  },
  {
    "path": "js/params.js",
    "chars": 9199,
    "preview": "/* eslint-disable */\nimport { HexToRGBA } from './colorPicker';\nimport { trim, logError } from './utils';\nimport Transla"
  },
  {
    "path": "js/primitive.js",
    "chars": 11760,
    "preview": "export default class PrimitiveTool {\n  constructor(main) {\n    this.ctx = main.ctx;\n    this.el = main.toolContainer;\n  "
  },
  {
    "path": "js/resizer.js",
    "chars": 6888,
    "preview": "import { tr } from './translation';\nimport { KEYS } from './utils';\n\nexport default class Resizer {\n  constructor(main) "
  },
  {
    "path": "js/selecter.js",
    "chars": 18044,
    "preview": "import { clearSelection, KEYS } from './utils';\n\nexport default class PainterroSelecter {\n  constructor(main, selectionC"
  },
  {
    "path": "js/settings.js",
    "chars": 5112,
    "preview": "import { tr } from './translation';\nimport { trim, KEYS } from './utils';\nimport { setParam } from './params';\n\nexport d"
  },
  {
    "path": "js/text.js",
    "chars": 7692,
    "preview": "import { KEYS } from './utils';\nimport { tr } from './translation';\nimport domtoimage from 'dom-to-image';\n\nexport defau"
  },
  {
    "path": "js/translation.js",
    "chars": 1598,
    "preview": "import de from \"../langs/de.lang\";\nimport en from \"../langs/en.lang\";\nimport es from \"../langs/es.lang\";\nimport ca from "
  },
  {
    "path": "js/utils.js",
    "chars": 5035,
    "preview": "export function genId() {\n  let text = \"ptro\";\n  const possible =\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw"
  },
  {
    "path": "js/worklog.js",
    "chars": 3577,
    "preview": "export default class WorkLog {\n  constructor(main, changedHandler) {\n    this.main = main;\n    this.current = null;\n    "
  },
  {
    "path": "js/zoomHelper.js",
    "chars": 3410,
    "preview": "export default class ZoomHelper {\n  constructor(main) {\n    this.main = main;\n    this.zomer = main.wrapper.querySelecto"
  },
  {
    "path": "langs/ca.lang.js",
    "chars": 2155,
    "preview": "export default {\n  lineColor: 'L',\n  lineColorFull: 'Color de línia',\n  fillColor: 'O',\n  fillColorFull: 'Color per ompl"
  },
  {
    "path": "langs/de.lang.js",
    "chars": 2170,
    "preview": "export default {\n  lineColor: 'L',\n  lineColorFull: 'Linienfarbe',\n  fillColor: 'F',\n  fillColorFull: 'Füllfarbe',\n  alp"
  },
  {
    "path": "langs/en.lang.js",
    "chars": 2255,
    "preview": "export default {\n  lineColor: 'L',\n  lineColorFull: 'Line color',\n  fillColor: 'F',\n  fillColorFull: 'Fill color',\n  alp"
  },
  {
    "path": "langs/es.lang.js",
    "chars": 2012,
    "preview": "export default {\n  lineColor: 'L',\n  lineColorFull: 'Color de linea',\n  fillColor: 'O',\n  fillColorFull: 'Color de relle"
  },
  {
    "path": "langs/fa.lang.js",
    "chars": 2778,
    "preview": " export default { //Persian traslation file \n        name: 'fa',//Iran-Farsi\n        strings: {\n            lineColor: '"
  },
  {
    "path": "langs/fr.lang.js",
    "chars": 2265,
    "preview": "export default {\n  lineColor: 'CL',\n  lineColorFull: 'Couleur de la ligne',\n  fillColor: 'CR',\n  fillColorFull: 'Couleur"
  },
  {
    "path": "langs/ja.lang.js",
    "chars": 1621,
    "preview": "export default {\n  lineColor: '線',\n  lineColorFull: '線の色',\n  fillColor: '塗',\n  fillColorFull: '塗りつぶしの色',\n  alpha: 'A',\n "
  },
  {
    "path": "langs/nl.lang.js",
    "chars": 2426,
    "preview": "export default {\n  lineColor: 'L',\n  lineColorFull: 'Lijnkleur',\n  fillColor: 'V',\n  fillColorFull: 'Vulkleur',\n  alpha:"
  },
  {
    "path": "langs/pl.lang.js",
    "chars": 2050,
    "preview": "export default {\n  lineColor: 'K',\n  lineColorFull: 'Kolor linii',\n  fillColor: 'W',\n  fillColorFull: 'Kolor wypełnienia"
  },
  {
    "path": "langs/pt-BR.lang.js",
    "chars": 2013,
    "preview": "export default {\n  lineColor: 'L',\n  lineColorFull: 'Cor da linha',\n  fillColor: 'O',\n  fillColorFull: 'Cor do preenchim"
  },
  {
    "path": "langs/pt-PT.lang.js",
    "chars": 2017,
    "preview": "export default {\n  lineColor: 'L',\n  lineColorFull: 'Cor da linha',\n  fillColor: 'O',\n  fillColorFull: 'Cor do preenchim"
  },
  {
    "path": "langs/ru.lang.js",
    "chars": 2414,
    "preview": "export default {\n  lineColor: 'Л',\n  lineColorFull: 'Цвет линии',\n  fillColor: 'З',\n  fillColorFull: 'Цвет заливки',\n  a"
  },
  {
    "path": "langs/uk.lang.js",
    "chars": 2637,
    "preview": "export default {\n  lineColor: \"КЛ\",\n  lineColorFull: \"Колір лінії\",\n  fillColor: \"КЗ\",\n  fillColorFull: \"Колір заливки\","
  },
  {
    "path": "package.json",
    "chars": 1843,
    "preview": "{\n  \"name\": \"painterro\",\n  \"version\": \"1.2.92\",\n  \"description\": \"HTML5 image editing widget (js paint plugin)\",\n  \"main"
  },
  {
    "path": "publish.sh",
    "chars": 2379,
    "preview": "#!/usr/bin/env bash\n\n# Auto-release script for https://github.com/devforth/painterro\n# Creates new release, builds asset"
  },
  {
    "path": "res/font/font-css.hbs",
    "chars": 515,
    "preview": "@font-face {\n\tfont-family: \"{{fontName}}\";\n\tsrc: {{{src}}};\n\tfont-weight: normal;\n    font-style: normal;\n}\n\n{{baseSelec"
  },
  {
    "path": "webpack.config.js",
    "chars": 2761,
    "preview": "'use strict';\nconst path = require('path');\nconst webpack = require('webpack');\nconst BundleAnalyzerPlugin = require('we"
  }
]

About this extraction

This page contains the full source code of the devforth/painterro GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 57 files (265.0 KB), approximately 74.3k tokens, and a symbol index with 249 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!